我有一个项目,使用 spring gateway 作为 spring 授权服务器的 oauth 客户端。 除了注销之外,在 oidc 身份验证方面一切都工作正常。由于 CORS,注销不起作用,因为在默认 spring 注销页面上按注销按钮后,原点为 null。如果我将“null”原点添加到允许的原点列表中,或禁用 cors,它会按预期工作。
以下是 Spring Gateway 客户端的配置:
@Configuration
@EnableWebFluxSecurity
public class SecurityConfiguration {
@Autowired
public SecurityConfiguration(ReactiveClientRegistrationRepository clientRegistrationRepository) {
this.clientRegistrationRepository = clientRegistrationRepository;
}
ReactiveClientRegistrationRepository clientRegistrationRepository;
private ServerLogoutSuccessHandler serverLogoutSuccessHandler() {
OidcClientInitiatedServerLogoutSuccessHandler successHandler = new OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository);
successHandler.setPostLogoutRedirectUri("{baseUrl}/welcome");
return successHandler;
}
@Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
String[] unprotectedPaths = new String[]{"/api-docs/**", "/swagger-ui.html", "/webjars/swagger-ui/**", "/actuator/**",
"/oidc/**","/welcome/**", "/customer/registration", "/customer/registration-confirmation/**"};
http.cors(withDefaults()).csrf(csrfSpec -> csrfSpec.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new SpaServerCsrfTokenRequestHandler()))
.authorizeExchange(authorizeExchangeSpec -> authorizeExchangeSpec.pathMatchers(unprotectedPaths).permitAll()
.pathMatchers("/notification/ws-connect").hasAuthority("SCOPE_notification.read")
.anyExchange().authenticated())
.oauth2Login(withDefaults())
.oauth2ResourceServer(oAuth2ResourceServerSpec -> oAuth2ResourceServerSpec.jwt(withDefaults()))
.logout(httpSecurityLogoutConfigurer -> httpSecurityLogoutConfigurer.logoutSuccessHandler(serverLogoutSuccessHandler()));
return http.build();
}
static final class SpaServerCsrfTokenRequestHandler extends ServerCsrfTokenRequestAttributeHandler {
private final ServerCsrfTokenRequestAttributeHandler delegate = new XorServerCsrfTokenRequestAttributeHandler();
@Override
public void handle(ServerWebExchange exchange, Mono<CsrfToken> csrfToken) {
// Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of the CsrfToken when it is rendered in the response body.
this.delegate.handle(exchange, csrfToken);
}
@Override
public Mono<String> resolveCsrfTokenValue(ServerWebExchange exchange, CsrfToken csrfToken) {
final var hasHeader = exchange.getRequest().getHeaders().get(csrfToken.getHeaderName()) != null;
return hasHeader ? super.resolveCsrfTokenValue(exchange, csrfToken) : this.delegate.resolveCsrfTokenValue(exchange, csrfToken);
}
}
@Bean
public WebFilter csrfCookieWebFilter() {
return (exchange, chain) -> {
exchange.getAttributeOrDefault(CsrfToken.class.getName(), Mono.empty()).subscribe(o -> ((CsrfToken) o).getToken());
return chain.filter(exchange);
};
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("http://localhost:63342"));
configuration.setAllowedMethods(List.of(CorsConfiguration.ALL));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(List.of(CorsConfiguration.ALL));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
以下是 api-gateway 日志:
2024-07-16T22:37:22.392+03:00 DEBUG 90681 --- [api-gateway] [ctor-http-nio-6] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : Trying to match using PathMatcherServerWebExchangeMatcher{pattern='/logout', method=GET}
2024-07-16T22:37:22.392+03:00 DEBUG 90681 --- [api-gateway] [ctor-http-nio-6] athPatternParserServerWebExchangeMatcher : Checking match of request : '/logout'; against '/logout'
2024-07-16T22:37:22.392+03:00 DEBUG 90681 --- [api-gateway] [ctor-http-nio-6] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : matched
2024-07-16T22:37:22.395+03:00 TRACE 90681 --- [api-gateway] [ctor-http-nio-6] o.s.w.s.adapter.HttpWebHandlerAdapter : [130c2a73-11] Completed 200 OK, headers={masked}
2024-07-16T22:37:22.395+03:00 TRACE 90681 --- [api-gateway] [ctor-http-nio-6] o.s.h.s.r.ReactorHttpHandlerAdapter : [130c2a73-5, L:/[0:0:0:0:0:0:0:1]:9990 - R:/[0:0:0:0:0:0:0:1]:63469] Handling completed
2024-07-16T22:37:37.036+03:00 TRACE 90681 --- [api-gateway] [ctor-http-nio-6] o.s.w.s.adapter.HttpWebHandlerAdapter : [130c2a73-12] HTTP POST "/logout", headers={masked}
2024-07-16T22:37:37.038+03:00 DEBUG 90681 --- [api-gateway] [ctor-http-nio-6] o.s.w.c.reactive.DefaultCorsProcessor : Reject: 'null' origin is not allowed
2024-07-16T22:37:37.038+03:00 TRACE 90681 --- [api-gateway] [ctor-http-nio-6] o.s.w.s.adapter.HttpWebHandlerAdapter : [130c2a73-12] Completed 403 FORBIDDEN, headers={masked}
2024-07-16T22:37:37.039+03:00 TRACE 90681 --- [api-gateway] [ctor-http-nio-6] o.s.h.s.r.ReactorHttpHandlerAdapter : [130c2a73-6, L:/[0:0:0:0:0:0:0:1]:9990 - R:/[0:0:0:0:0:0:0:1]:63469] Handling completed
RP 发起的注销(打开链接并阅读规范)从向依赖方(使用
oauth2Login
配置的 Spring 应用程序)/logout
端点发出 POST 请求开始。
对于配置有
oauth2Login
的应用程序的任何请求,都会通过会话进行授权(请求必须保存会话 cookie)。因此,发送此请求的前端必须与 RP 具有相同的来源:是 RP 上托管的 Thymeleaf 模板,或者通过相同的反向代理(可以是网关本身)提供服务。所以,对RP的POST请求不应该是跨域请求。对于 SPA,您需要的是反向代理,而不是 CORS 配置。
对于通过会话授权的任何 POST、PUT、PATCH 和 DELETE 请求,应保护其免受 CSRF(包含 CSRF 令牌)。对于单页应用程序,此令牌是从 cookie 中读取的(http-only false)并设置为标头。
POST 请求的答案(网关会话结束后)应包含一个
Location
标头,其中包含用于结束授权服务器上的会话的 URI。如果授权服务器允许提供 SPA 的源(反向代理),则可以让运行 SPA 的浏览器跟踪此位置。否则,您可能必须将网关响应的状态从 302
更改为 2xx
范围内的状态,以便 Javascript 代码可以观察响应、读取 Location
,然后设置 window.location.href
(更改浏览器选项卡源而不是发送跨源请求)。
spring-cloud-gateway
的详细
工作示例(包括注销)用作 SPA 的 OAuth2 BFF(带有
oauth2Login
和 TokenRelay
过滤器)我写的这篇 Baeldung 文章。