设置
服务器和客户端都运行在不同的主机上。 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 的缺失标头。我只有几行日志来满足此请求:
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());
}
我想我找到了解决方案。 @Toerktumlare感谢您指出正确的方向。正如 Toerktumlare 指出的那样,我的过滤器、CorsFilter 和 JwtAuthFilter 正在绕过 Spring Security 的默认功能。尽管文档中描述了 CorsFilter,但文档中的最佳实践始终是 CorsConfigurationSource 和 CorsWebFilter 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错误丢失,浏览器成功渲染错误体。