@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(request -> {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(List.of("*")); // 允许所有源
configuration.setAllowedMethods(List.of("*")); // 允许所有方法
configuration.setAllowedHeaders(List.of("*")); // 允许所有请求头
configuration.setAllowCredentials(true); // 注意:使用 * 时必须设置为 false
return configuration;
}))
.csrf(AbstractHttpConfigurer::disable)//对于一些无状态的 API 服务(例如,使用 JWT 认证的 RESTful API),CSRF 保护可以被禁用,因为这些服务通常通过 HTTP Header(如 Authorization)来认证请求,而不依赖 Cookie。对于这样的服务,CSRF 攻击的风险较低,通常可以通过禁用 CSRF 来提高性能。
.formLogin(AbstractHttpConfigurer::disable)
// 使用新的授权配置方式
.authorizeHttpRequests(authz -> authz
.requestMatchers( "/auth/login").permitAll() // 使用requestMatchers替代antMatchers
.anyRequest().authenticated()
)
// 会话管理配置保持不变
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.addFilterBefore(jwtAuthenticationFilter, AnonymousAuthenticationFilter.class) ; // 将 JwtAuthenticationFilter 加入过滤链
return http.build();
}
适用场景:Servlet 容器下(Tomcat、Jetty等)的 Spring Boot Web 项目,或者传统的 Spring MVC 应用。
对应的依赖:spring-boot-starter-web
+ spring-boot-starter-security
(针对 servlet)。
底层协议:基于 HttpServletRequest
和 HttpServletResponse
(Servlet API),每个请求都在一个独立的线程里处理。
典型用法:在传统 MVC/Web 应用中进行登录表单、会话管理、拦截/放行等安全配置。
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
// CORS 配置
.cors(cors -> cors.configurationSource(request -> {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(List.of("*")); // 允许所有源
configuration.setAllowedMethods(List.of("*")); // 允许所有方法
configuration.setAllowedHeaders(List.of("*")); // 允许所有请求头
configuration.setAllowCredentials(true); // 注意:使用 * 时必须设置为 false
return configuration;
}))
// 禁用 CSRF 和表单登录
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.formLogin(ServerHttpSecurity.FormLoginSpec::disable) // 禁用表单登录
.httpBasic(ServerHttpSecurity.HttpBasicSpec::disable) // 禁用 HTTP Basic 认证
.logout(ServerHttpSecurity.LogoutSpec::disable) // 禁用默认的注销端点
// 使用新的授权配置方式
.authorizeExchange(authz -> authz
.pathMatchers("/order/**").permitAll() // 放行登录接口
.anyExchange().authenticated() // 其他请求需要认证
)
// 添加 JWT 认证过滤器
.addFilterBefore(jwtAuthenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION); // 将 JwtAuthenticationFilter 加入过滤链
return http.build();
}
适用场景:Reactive(反应式)Web 环境,比如使用 Spring WebFlux 或者 Spring Cloud Gateway。
对应的依赖:spring-boot-starter-webflux
+ spring-boot-starter-security
(针对 reactive)。
底层协议:基于非阻塞式(Netty 等)的反应式流处理。
典型用法:在使用 WebFlux Handler 或 Spring Cloud Gateway 时,通过 ServerHttpSecurity
配置响应式的安全策略。
为什么 Spring Cloud Gateway 往往会看到 ServerHttpSecurity
?
因为 Spring Cloud Gateway 基于 WebFlux/reactor-netty 实现,是一个典型的非阻塞、响应式的网关。Spring Security 针对这种响应式请求的链式处理方式,需要使用 ServerHttpSecurity
来配置。
而传统的基于 Servlet 的网关(例如 Zuul 1.x)就还是用 HttpSecurity。
ServerHttpSecurity中添加的过滤器需要实现WebFilter(与Spring MVC不同)
import com.aqian.common.enums.ResponseCodeEnum; import com.aqian.common.exception.BaseException; import com.aqian.gatewayserver.util.JwtUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; import reactor.util.annotation.NonNull; @Component public class JwtAuthenticationWebFilter implements WebFilter { @Autowired private JwtUtil jwtUtil; @Override public @NonNull Mono
filter(@NonNull ServerWebExchange exchange, @NonNull WebFilterChain chain) { // 从请求头中获取JWT String token = getJwtFromRequest(exchange); System.out.println("经过了webfilter"); // 如果JWT存在且有效,则进行验证 if (token != null) { try { // token 是否有效 / 是否过期 / 是否匹配对应用户 Integer userId = Integer.parseInt(jwtUtil.extractUserId(token)); boolean valid = jwtUtil.validateToken(token, userId); // 如果 Token 无效或过期,直接抛出异常 if (!valid) { // 这里可以区分是过期还是无效,根据业务逻辑不同抛不同的异常 if (jwtUtil.isTokenExpired(token)) { throw new BaseException(ResponseCodeEnum.TOKEN_EXPIRED); } throw new BaseException(ResponseCodeEnum.TOKEN_INVALID); } // 如果 token 合法,设置到 SecurityContext UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userId, null, null); SecurityContextHolder.getContext().setAuthentication(authentication); // 继续执行过滤链 return chain.filter(exchange); } catch (BaseException e) { // 捕获自定义异常,并设置响应状态码 exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } catch (Exception e) { // 处理其他异常 exchange.getResponse().setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); return exchange.getResponse().setComplete(); } } // 如果没有JWT,继续执行过滤链 return chain.filter(exchange); } /** * 从HTTP请求头中提取JWT令牌 * @param exchange ServerWebExchange * @return JWT令牌 */ private String getJwtFromRequest(ServerWebExchange exchange) { String bearerToken = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION); // 从Authorization头部获取JWT if (bearerToken != null && bearerToken.startsWith("Bearer ")) { // 如果以"Bearer "开头 return bearerToken.substring(7); // 去掉"Bearer ",返回JWT部分 } return null; // 如果没有JWT令牌,返回null } }
从表象上,许多开发者就会这么区分:“HttpSecurity 在 Spring Boot Web/MVC 场景下用;ServerHttpSecurity 多见于 Spring Cloud Gateway”。但更准确的表述是:
Servlet 模型 (Spring MVC、Tomcat、Jetty 等) →
HttpSecurity
Reactive 模型 (Spring WebFlux、Netty、Gateway 等) →
ServerHttpSecurity
Spring Boot 本身并不强行规定你用 Servlet 还是 Reactive。你可以在 Spring Boot 中使用任何一种模型,只要你的依赖是对应的
spring-boot-starter-web
(MVC) 或者spring-boot-starter-webflux
(WebFlux)。
Spring Boot Servlet 项目一般默认是
HttpSecurity
。Spring Boot WebFlux 项目则需要
ServerHttpSecurity
。Spring Cloud Gateway 是响应式的,用
ServerHttpSecurity
。如果你在写的服务最终是非响应式的(Servlet 模型),就用 HttpSecurity;如果你的服务是响应式的(Reactive 模型,如 Spring Cloud Gateway 项目),就用 ServerHttpSecurity。
在 Spring Security WebFlux 中,.exceptionHandling()
方法用于配置安全异常处理行为。下面我将详细讲解它的使用方法。
基本概念
exceptionHandling()
是 ServerHttpSecurity
配置中的一个重要部分,它允许你自定义当安全相关异常发生时(如认证失败、访问被拒绝等)的处理方式。
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
// 其他配置...
.exceptionHandling(exceptionHandling -> exceptionHandling
.authenticationEntryPoint(...)
.accessDeniedHandler(...)
)
// 其他配置...
.build();
}
配置认证入口点,处理未认证用户尝试访问受保护资源时的响应。
.exceptionHandling(exceptionHandling -> exceptionHandling
.authenticationEntryPoint((exchange, ex) -> {
// 自定义响应
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
DataBuffer buffer = response.bufferFactory().wrap("{\"error\":\"Unauthorized\"}".getBytes());
return response.writeWith(Mono.just(buffer));
})
)
或者使用预定义的入口点:
.exceptionHandling(exceptionHandling -> exceptionHandling
.authenticationEntryPoint(new HttpBasicServerAuthenticationEntryPoint())
)
配置访问拒绝处理器,处理已认证但权限不足的用户尝试访问受保护资源时的响应。
.exceptionHandling(exceptionHandling -> exceptionHandling
.accessDeniedHandler((exchange, ex) -> {
// 自定义响应
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.FORBIDDEN);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
DataBuffer buffer = response.bufferFactory().wrap("{\"error\":\"Access Denied\"}".getBytes());
return response.writeWith(Mono.just(buffer));
})
)
完整示例
下面是一个完整的配置示例:
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.authorizeExchange(exchanges -> exchanges
.pathMatchers("/public/**").permitAll()
.pathMatchers("/admin/**").hasRole("ADMIN")
.anyExchange().authenticated()
)
.httpBasic(withDefaults())
.formLogin(withDefaults())
.exceptionHandling(exceptionHandling -> exceptionHandling
.authenticationEntryPoint((exchange, ex) -> {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
DataBuffer buffer = response.bufferFactory()
.wrap("{\"message\":\"Authentication required\"}".getBytes());
return response.writeWith(Mono.just(buffer));
})
.accessDeniedHandler((exchange, ex) -> {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.FORBIDDEN);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
DataBuffer buffer = response.bufferFactory()
.wrap("{\"message\":\"Insufficient privileges\"}".getBytes());
return response.writeWith(Mono.just(buffer));
})
)
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.build();
}
高级用法
.exceptionHandling(exceptionHandling -> exceptionHandling
.authenticationEntryPoint((exchange, ex) -> {
ServerHttpResponse response = exchange.getResponse();
if (exchange.getRequest().getHeaders().getAccept().contains(MediaType.APPLICATION_JSON)) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
DataBuffer buffer = response.bufferFactory()
.wrap("{\"error\":\"Unauthorized\"}".getBytes());
return response.writeWith(Mono.just(buffer));
} else {
response.setStatusCode(HttpStatus.FOUND);
response.getHeaders().setLocation(URI.create("/login"));
return response.setComplete();
}
})
)
.exceptionHandling(exceptionHandling -> exceptionHandling
.accessDeniedHandler((exchange, ex) -> {
log.warn("Access denied for user {} to path {}",
exchange.getPrincipal().block().getName(),
exchange.getRequest().getPath());
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.FORBIDDEN);
return response.setComplete();
})
)
.exceptionHandling(exceptionHandling -> exceptionHandling
.authenticationEntryPoint(new DelegatingServerAuthenticationEntryPoint(
Arrays.asList(
new BearerTokenServerAuthenticationEntryPoint(),
new HttpBasicServerAuthenticationEntryPoint(),
new RedirectServerAuthenticationEntryPoint("/login")
)
))
)
注意事项
在响应式编程中,确保你的处理器返回的是 Mono
避免在处理器中进行阻塞操作
考虑使用 ServerWebExchange
的完整能力来构建响应
对于生产环境,建议将错误信息标准化