如何解决当为不受限制的端点发送授权标头时,Springboot webflux 抛出 401
我有一个受 spring security oauth2 保护的 springboot webflux 应用程序。我在应用程序中有受限制和不受限制的端点。将 Authorization 标头传递给不受限制的端点时,应用程序会抛出 401。当我不为不受限制的端点传递 Authorization 标头时,它工作正常。我可以看到,当传递 Authorization 标头时,AuthenticationManager
正在为受限制和不受限制的端点执行。
SecurityWebFilterChain
bean 配置如下。
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity serverHttpSecurity) {
return serverHttpSecurity
.requestCache()
.requestCache(NoOpServerRequestCache.getInstance())
.and()
.securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
.exceptionHandling()
.authenticationEntryPoint((swe,e) -> Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED)))
.accessDeniedHandler((swe,e) -> Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN)))
.and().csrf().disable()
.authorizeExchange()
.pathMatchers("/api/unrestricted").permitAll()
.and()
.authorizeExchange().anyExchange().authenticated()
.and()
.oauth2ResourceServer()
.jwt(jwtSpec -> jwtSpec.authenticationManager(authenticationManager()))
.authenticationEntryPoint((swe,e) -> Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN)))
.and().build();
}
AuthenticationManager
代码如下。
private ReactiveAuthenticationManager authenticationManager() {
return authentication -> {
log.info("executing authentication manager");
return Mono.justOrEmpty(authentication)
.filter(auth -> auth instanceof BearerTokenAuthenticationToken)
.cast(BearerTokenAuthenticationToken.class)
.filter(token -> RSAHelper.verifySigning(token.getToken()))
.switchIfEmpty(Mono.error(new BadCredentialsException("Invalid token")))
.map(token -> (Authentication) new UsernamePasswordAuthenticationToken(
token.getToken(),token.getToken(),Collections.emptyList()
));
};
}
当我们的一个 API 使用者为不受限制的端点发送虚拟授权标头时,我们发现了这个问题。
我可以在 SpringMVC Oauth2 中找到针对类似问题的 Spring MVC 解决方案。
我在 github 项目 demo-security 中有一个工作示例。我已经编写了几个集成测试来解释这个问题。
@AutoConfigureWebTestClient
@SpringBootTest
public class DemoIT {
@Autowired
private WebTestClient webTestClient;
@Test
void testUnrestrictedEndpointWithAuthorizationHeader() {
webTestClient.get()
.uri("/api/unrestricted")
.header(HttpHeaders.AUTHORIZATION,"Bearer token") // fails when passing token
.exchange()
.expectStatus().isOk();
}
@Test
void testUnrestrictedEndpoint() {
webTestClient.get()
.uri("/api/unrestricted")
.exchange()
.expectStatus().isOk();
}
@SneakyThrows
@Test
void testRestrictedEndpoint() {
webTestClient.get()
.uri("/api/restricted")
.header(HttpHeaders.AUTHORIZATION,"Bearer " + RSAHelper.getJWSToken())
.exchange()
.expectStatus().isOk();
}
}
我不确定可能是什么问题。我的安全配置是否配置错误?任何帮助将不胜感激。
解决方法
首先是一些有用的信息:
身份验证和授权在 Spring Security 中的单独过滤器中完成:AuthenticationWebFilter 和 AuthorizationWebFilter。
首先身份验证检查任何传入的凭据并将它们放入安全上下文中。稍后授权过滤器会根据您的 ServerHttpSecurity 设置检查是否允许访问。
在您的情况下,我不是 100% 确定,但我认为问题可能是如果没有有效的身份验证,您的 AuthenticationManager 会返回错误
.switchIfEmpty(Mono.error(new BadCredentialsException("Invalid token")))
如果身份验证失败,对我有用的是返回 Mono.empty():
public Mono<Authentication> authenticate(Authentication authentication) {
String jwt = authentication.getCredentials().toString();
if (StringUtils.hasText(jwt) && this.tokenProvider.validateToken(jwt)) {
return Mono.just(this.tokenProvider.getAuthentication(jwt));
}
else {
return Mono.empty();
}
}
,
我终于设法用不同的方法解决了它。我不再使用 oauth2ResourceServer()
方法。
更新后的 SecurityWebFilterChain
bean 配置如下。
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity serverHttpSecurity,BearerTokenConverter bearerTokenConverter) {
return serverHttpSecurity
.requestCache()
.requestCache(NoOpServerRequestCache.getInstance()) // disable cache
.and()
.securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
.exceptionHandling()
.authenticationEntryPoint((swe,e) -> Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED)))
.accessDeniedHandler((swe,e) -> Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN)))
.and()
.csrf().disable().authorizeExchange()
.pathMatchers("/api/unrestricted")
.permitAll()
.anyExchange().access((mono,authorizationContext) -> mono.map(authentication -> new AuthorizationDecision(authentication.isAuthenticated())))
.and()
.addFilterAt(authenticationWebFilter(bearerTokenConverter),SecurityWebFiltersOrder.AUTHENTICATION)
.build();
}
我没有使用 oauth2ResourceServer()
,而是在链中添加了自定义 AuthenticationWebFilter
。
AuthenticationWebFilter
代码如下。
private AuthenticationWebFilter authenticationWebFilter(BearerTokenConverter bearerTokenConverter) {
AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(authenticationManager());
authenticationWebFilter.setServerAuthenticationConverter(bearerTokenConverter);
authenticationWebFilter.setRequiresAuthenticationMatcher(new NegatedServerWebExchangeMatcher(pathMatchers("/api/unrestricted")));
authenticationWebFilter.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler((swe,e) -> Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED))));
return authenticationWebFilter;
}
AuthenticationWebFilter 将在 authenticationWebFilter.setRequiresAuthenticationMatcher()
的帮助下仅对受限端点执行。
现在,即使我们为不受限制的端点传递 Authorization 标头,它也能工作。会出现的问题是为什么要通过。但是我们不希望我们的 API 因意外的标头而中断。所以我们采用了这种方法。
这个实现帮助我们解决了这个问题。但问题仍然存在于以前的方法中。
我已使用工作代码更新了 github 项目 demo-security。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。