Spring Security 6.3.3 WebFlux CORS

问题描述 投票:0回答:1

设置

  • Spring Boot 3.3
  • 春季安全6.3.3
  • 角度18

服务器和客户端都运行在不同的主机上。 Spring Boot 应用程序在 http://api.example.com 上运行。 Angular 应用程序在 http://client.example.com.

上运行

问题 使用下面的配置,当我使用有效的 JWT 令牌向 http://api.example.com/users/info 发出请求时,请求将通过 CORS WebFilter 和 AUTHENTICATION WebFilter,并且我会同时获得用户信息 json 对象在邮递员和浏览器中(chrome,我不认为这是一个问题,但以防万一)。

当我尝试使用无效的 JWT 令牌(假设是过期的令牌)时,我会在浏览器控制台上收到 Unauthorized (401) 状态代码,预计会发生 CORS 错误,这很奇怪(据我所知),因为有效的请求通过并且没有错误主体在浏览器上,而我在邮递员中收到“正确”的错误正文。

Spring Boot 日志没有引用任何有关 CORS 的缺失标头。我只有几行日志来满足此请求:

  • HTTP GET“/用户/信息”
  • 已完成 401 未经授权

Spring Boot日志信息

logging:
  level:
    org.springframework.web: DEBUG
    org.springframework.security: DEBUG
    io.r2dbc.spi: DEBUG

我不是一个经验丰富的 Spring Boot 开发人员,所以不要在实现上对我太苛刻。尽管更正值得赞赏。

希望我提供了解决此问题所需的所有信息。有什么建议吗?预先感谢。

安全配置.java

@Configuration
@EnableWebFluxSecurity
@EnableConfigurationProperties(ResourceServiceSecurityProperties.class)
@RequiredArgsConstructor
public class SecurityConfiguration {

    private static final String[] AUTH_WHITELIST = {
        "/users/login"
    };

    private final ResourceServiceSecurityProperties resourceServiceSecurityProperties;

    private final JwtAuthFilter jwtAuthFilter;

    private final CorsFilter corsFilter;

    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {

        http
                // Disabled basic authentication
                .httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)

                // Disabled CSRF
                .csrf(ServerHttpSecurity.CsrfSpec::disable)

                // Rest services don't have a login form
                .formLogin(ServerHttpSecurity.FormLoginSpec::disable)

                .logout(ServerHttpSecurity.LogoutSpec::disable)

                // Disable Spring Security CORS we will use a CORS web filter.
                .cors(ServerHttpSecurity.CorsSpec::disable)

                .authorizeExchange(authorizeRequests -> authorizeRequests
                                .pathMatchers(HttpMethod.OPTIONS, "/**")
                                    .permitAll()
                                .pathMatchers(HttpMethod.POST, AUTH_WHITELIST)
                                    .permitAll()
                                .anyExchange()
                                    .authenticated()
                )

                .addFilterAt(jwtAuthFilter, SecurityWebFiltersOrder.AUTHENTICATION)
                .addFilterAt(corsFilter, SecurityWebFiltersOrder.CORS)

                // Do not allow sessions
               .securityContextRepository(NoOpServerSecurityContextRepository.getInstance())

                .oauth2ResourceServer(oauth2ResourceServerCustomizer -> oauth2ResourceServerCustomizer
                        .jwt(Customizer.withDefaults())
                );

        return http.build();
    }

    @Bean
    public ReactiveJwtDecoder reactiveJwtDecoder() {
        return ReactiveJwtDecoders.fromIssuerLocation(resourceServiceSecurityProperties.getIssuerUri());
    }

CorsFilter.java

@Component
public class CorsFilter implements WebFilter {

    @Override
    @NonNull
    public Mono<Void> filter(@NonNull ServerWebExchange exchange, @NonNull WebFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();

        if (request.getMethod().equals(HttpMethod.OPTIONS)) {
            response.setStatusCode(HttpStatus.ACCEPTED);
            return chain.filter(exchange);
        }

        request.mutate()
                .header("Access-Control-Allow-Origin", http://client.example.com")
                .header("Access-Control-Allow-Credentials", "true")
                .header("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE")
                .header("Access-Control-Max-Age", "3600")
                .header("Access-Control-Allow-Headers", "Content-Type, authorization")
                .build();

        return chain.filter(exchange.mutate().request(request).build());
    }
}

也尝试过不改变请求和交换。

JwtAuthFilter.java

@Component
@AllArgsConstructor
public class JwtAuthFilter implements WebFilter {

    private final JwtUtil jwtUtil;

    private static final String[] AUTH_WHITELIST = {
            "/users/login"
    };

    @Override
    @NonNull
    public Mono<Void> filter(@NonNull ServerWebExchange exchange, @NonNull WebFilterChain chain) {
        if (Arrays.asList(AUTH_WHITELIST).contains(exchange.getRequest().getPath().value())) {
            return chain.filter(exchange);
        }

        if (exchange.getRequest().getMethod().equals(HttpMethod.OPTIONS)) {
            exchange.getResponse().setStatusCode(HttpStatus.ACCEPTED);
            return chain.filter(exchange);
        }

        return Mono.justOrEmpty(exchange.getRequest().getHeaders().getFirst("Authorization"))
                .filter(authHeader -> authHeader.startsWith("Bearer "))
                .switchIfEmpty(
                    Mono.error(
                            new InvalidBearerTokenException("The token provided is expired, malformed, revoked, or invalid for other reasons.")
                    )
                )
                .map(authHeader -> authHeader.substring(7))
                .map(jwtUtil::isExpired)
                .flatMap(isExpired -> {
                    if (isExpired) {
                        return Mono.error(new InvalidBearerTokenException("The token provided is expired, malformed, revoked, or invalid for other reasons."));
                    }
                    return chain.filter(exchange);
                });
    }
}

ExceptionHandlerAdvice.java

@RestControllerAdvice
@AllArgsConstructor
//@Priority(0)
@Order(-2)
public class ExceptionHandlerAdvice implements ErrorWebExceptionHandler {

    private final ExceptionErrorFactory exceptionErrorFactory;

    private final ObjectMapper objectMapper;

    @ExceptionHandler(WebExchangeBindException.class)
    public ResponseEntity<ErrorDto> handleValidationExceptions(final WebExchangeBindException ex) {
        return new ResponseEntity<>(
                this.exceptionErrorFactory.createError(ex.getClass().getName(), ex),
                ex.getStatusCode());
    }

    @ExceptionHandler(InvalidBearerTokenException.class)
    public ResponseEntity<ErrorDto> handleInvalidBearerTokenException(final InvalidBearerTokenException ex) {
        return new ResponseEntity<>(
                this.exceptionErrorFactory.createError(ex.getClass().getName()),
                HttpStatus.UNAUTHORIZED
        );
    }

    @ExceptionHandler(ExpiredJwtException.class)
    public ResponseEntity<ErrorDto> handleExpiredBearerTokenException(final ExpiredJwtException ex) {
        return new ResponseEntity<>(
                this.exceptionErrorFactory.createError(ex.getClass().getName()),
                HttpStatus.UNAUTHORIZED
        );
    }

    @ExceptionHandler(UnauthorisedException.class)
    public ResponseEntity<ErrorDto> handleUnauthorizedException(UnauthorisedException ex) {
        return new ResponseEntity<>(
                this.exceptionErrorFactory.createError(ex.getClass().getName(), ex),
                HttpStatus.UNAUTHORIZED
        );
    }

    @Override
    @NonNull
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {

        DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory();

        exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);

        ErrorDto errorResponse = null;

        if (ex.getClass().equals(ExpiredJwtException.class)) {
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            errorResponse = this.exceptionErrorFactory.createError(ex.getClass().getName());
        } else if (ex.getClass().equals(InvalidBearerTokenException.class)) {
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            errorResponse = this.exceptionErrorFactory.createError(ex.getClass().getName());
        }

        DataBuffer dataBuffer = null;
        try {
            dataBuffer = bufferFactory.wrap(objectMapper.writeValueAsBytes(errorResponse));
        } catch (Exception e) {
            dataBuffer = bufferFactory.wrap("".getBytes());
        }

        return exchange.getResponse().writeWith(Flux.just(dataBuffer));
// Also tried this
// return exchange.getResponse().writeWith(Mono.just(dataBuffer));

    }
}

我尝试像这样实现 CORS 过滤器,但没有成功。此实现的唯一区别是我可以在浏览器上看到预检请求(选项)。

    @Bean
    CorsConfigurationSource corsConfiguration() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.addAllowedOrigin("http://client.example.com");
        configuration.setAllowCredentials(true);
        configuration.setMaxAge(3600L);
        configuration.addAllowedMethod("GET");
        configuration.addAllowedMethod("PUT");
        configuration.addAllowedMethod("POST");
        configuration.addAllowedMethod("DELETE");
        configuration.addAllowedMethod("OPTIONS");
        configuration.addAllowedHeader("Content-Type");
        configuration.addAllowedHeader("authorization");

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

    @Bean
    public CorsWebFilter corsWebFilter() {
        return new CorsWebFilter(corsConfiguration());
    }
spring-boot spring-security cors
1个回答
0
投票

我想我找到了解决方案。 @Toerktumlare感谢您指出正确的方向。正如 Toerktumlare 指出的那样,我的过滤器、CorsFilter 和 JwtAuthFilter 正在绕过 Spring Security 的默认功能。尽管文档中描述了 CorsFilter,但文档中的最佳实践始终是 CorsConfigurationSourceCorsWebFilter beans。

这是对我有用的最终配置。再次欢迎指出任何不好的做法。

安全配置.java

@Configuration
@EnableWebFluxSecurity
@EnableConfigurationProperties(ResourceServiceSecurityProperties.class)
@RequiredArgsConstructor
public class SecurityConfiguration {

    private static final String[] AUTH_WHITELIST = {
            "/users/login"
    };

    private final ResourceServiceSecurityProperties resourceServiceSecurityProperties;

    private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;

    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {

        http
                // Disabled basic authentication
                .httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)

                // Disabled CSRF
                .csrf(ServerHttpSecurity.CsrfSpec::disable)

                // Rest services don't have a login form
                .formLogin(ServerHttpSecurity.FormLoginSpec::disable)

                .logout(ServerHttpSecurity.LogoutSpec::disable)

                .authorizeExchange(authorizeRequests -> authorizeRequests
                        .pathMatchers(HttpMethod.OPTIONS, "/**")
                        .permitAll()
                        .pathMatchers(HttpMethod.POST, AUTH_WHITELIST)
                        .permitAll()
                        .anyExchange()
                        .authenticated()
                )

                // Do not allow sessions
                .securityContextRepository(NoOpServerSecurityContextRepository.getInstance())

                .oauth2ResourceServer(oauth2ResourceServerCustomizer -> oauth2ResourceServerCustomizer
                        .jwt(Customizer.withDefaults()).authenticationEntryPoint(customAuthenticationEntryPoint)
                );

        return http.build();
    }

    @Bean
    public ReactiveJwtDecoder reactiveJwtDecoder() {
        return ReactiveJwtDecoders.fromIssuerLocation(resourceServiceSecurityProperties.getIssuerUri());
    }

    @Bean
    CorsConfigurationSource corsConfiguration() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.addAllowedOrigin("http://client.example.com");
        configuration.setAllowCredentials(true);
        configuration.setMaxAge(3600L);
        configuration.addAllowedMethod("GET");
        configuration.addAllowedMethod("PUT");
        configuration.addAllowedMethod("POST");
        configuration.addAllowedMethod("DELETE");
        configuration.addAllowedMethod("OPTIONS");
        configuration.addAllowedHeader("Content-Type");
        configuration.addAllowedHeader("authorization");

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

    @Bean
    public CorsWebFilter corsWebFilter() {
        return new CorsWebFilter(corsConfiguration());
    }
}

对 ExceptionHandlerAdvice.java 没有任何更改

添加了 CustomAuthenticationEntryPoint.java

@Component
@AllArgsConstructor
public class CustomAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {

    private final ExceptionErrorFactory exceptionErrorFactory;

    private final ObjectMapper objectMapper;

    @Override
    public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException exception) {
        DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory();

        ErrorDto errorResponse = this.exceptionErrorFactory.createError(exception.getClass().getName());

        DataBuffer dataBuffer = null;
        try {
            dataBuffer = bufferFactory.wrap(objectMapper.writeValueAsBytes(errorResponse));
        } catch (Exception e) {
            dataBuffer = bufferFactory.wrap("Error".getBytes());
        }

        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        return response.writeWith(Mono.just(dataBuffer));
    }
}

当我删除两个自定义过滤器并添加 CustomAuthenticationEntryPoint 时,一切都按预期工作。 CORS错误丢失,浏览器成功渲染错误体。

© www.soinside.com 2019 - 2024. All rights reserved.