基于Reactive的Spring Security(Spring Webflux)

HttpSecurity (Servlet API)

@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)。

  • 底层协议:基于 HttpServletRequestHttpServletResponse(Servlet API),每个请求都在一个独立的线程里处理。

  • 典型用法:在传统 MVC/Web 应用中进行登录表单、会话管理、拦截/放行等安全配置。

ServerHttpSecurity (Reactive API) 

    @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 HandlerSpring 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 异常处理配置详解

在 Spring Security WebFlux 中,.exceptionHandling() 方法用于配置安全异常处理行为。下面我将详细讲解它的使用方法。

基本概念

exceptionHandling() 是 ServerHttpSecurity 配置中的一个重要部分,它允许你自定义当安全相关异常发生时(如认证失败、访问被拒绝等)的处理方式。

基本配置结构

@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
    return http
        // 其他配置...
        .exceptionHandling(exceptionHandling -> exceptionHandling
            .authenticationEntryPoint(...)
            .accessDeniedHandler(...)
        )
        // 其他配置...
        .build();
}

主要配置选项

(1)authenticationEntryPoint

配置认证入口点,处理未认证用户尝试访问受保护资源时的响应。

.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())
)

(2)accessDeniedHandler

配置访问拒绝处理器,处理已认证但权限不足的用户尝试访问受保护资源时的响应。

.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();
}
高级用法

高级用法 

(1)根据请求类型返回不同响应

.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();
        }
    })
)

(2)记录异常日志

.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();
    })
)

(2)组合多个处理器

.exceptionHandling(exceptionHandling -> exceptionHandling
    .authenticationEntryPoint(new DelegatingServerAuthenticationEntryPoint(
        Arrays.asList(
            new BearerTokenServerAuthenticationEntryPoint(),
            new HttpBasicServerAuthenticationEntryPoint(),
            new RedirectServerAuthenticationEntryPoint("/login")
        )
    ))
)

注意事项

  • 在响应式编程中,确保你的处理器返回的是 Mono

  • 避免在处理器中进行阻塞操作

  • 考虑使用 ServerWebExchange 的完整能力来构建响应

  • 对于生产环境,建议将错误信息标准化

你可能感兴趣的:(Spring,Security,spring,java,后端)