当为不受限制的端点发送授权标头时,Springboot webflux 抛出 401

Posted

技术标签:

【中文标题】当为不受限制的端点发送授权标头时,Springboot webflux 抛出 401【英文标题】:Springboot webflux throwing 401 when Authorization header sent for unrestricted endpoint 【发布时间】:2021-05-27 16:15:50 【问题描述】:

我有一个受 spring security oauth2 保护的 springboot webflux 应用程序。我在应用程序中有受限制和不受限制的端点。将授权标头传递给不受限制的端点时,应用程序抛出 401。当我不为不受限制的端点传递授权标头时,它工作正常。我可以看到 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.UNAUTHORIZED)))
                .accessDeniedHandler((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();
    

我不确定可能是什么问题。我的安全配置是否配置错误?任何帮助将不胜感激。

【问题讨论】:

你传递了一个无效的令牌,所以它会失败。某些东西不受限制的事实并不意味着安全标头不会被解析...... 你有 .anyExchange() 我怀疑这会覆盖你之前的声明 @Toerktumlare 我尝试了不同的设置,但没有帮助 @M.Deinum 我同意。我在这里要说明的一点是为什么 authenticationManager 正在为不受限制的端点执行 .authorizeExchange().anyExchange().authenticated() 你调用authorizeExchange 两次,这意味着你正在覆盖前一个。 【参考方案1】:

我终于设法用不同的方法解决了它。我不再使用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() 的帮助下对受限端点执行。

现在,即使我们为不受限制的端点传递授权标头,它也可以工作。会出现的问题是为什么要通过。但我们不希望我们的 API 因意外的标头而中断。所以我们采用了这种方法。

这个实现帮助我们解决了这个问题。但是以前的方法仍然存在问题。

我已经用工作代码更新了 github 项目demo-security。

【讨论】:

【参考方案2】:

首先提供一些有用的信息:

身份验证和授权在 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();
        
    

【讨论】:

是的,它应该为无效令牌抛出异常。但我不希望 authenticationManager 为不受限制的端点执行。这就是问题所在。 它确实被执行了。如果没有身份验证,您需要做的是返回 Mono.empty() 。我将用我的代码更新上面的答案

以上是关于当为不受限制的端点发送授权标头时,Springboot webflux 抛出 401的主要内容,如果未能解决你的问题,请参考以下文章

使用AWS Android SDK发送JWT令牌

如何使用papercrop gem将纵横比设置为不受限制的比例?

仅在生产中缺少授权标头

Swift:未设置请求授权标头

ASP.NET Core 在授权失败时发送 Access-Control-Allow-Origin 标头

AngularJS 不发送授权或任何额外的键/值标头