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

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

我有一个受 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();
    }
}

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

java spring-boot spring-webflux spring-security-oauth2
3个回答
2
投票

我终于设法用不同的方法解决了这个问题。我不再使用

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


0
投票

首先一些有用的信息:

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

0
投票

我创建了一个自定义身份验证管理器,其中排除了不受限制的端点的身份验证

@Component
@Primary
public class CustomAuthenticationManager implements 
ReactiveAuthenticationManager {
private final ReactiveAuthenticationManager delegate;
private final PathMatcher pathMatcher;

@Value("#{'${gateway.restricted.path.patterns}'.split(',')}")
private List<String> restrictedPathPatterns;

@Autowired
public CustomAuthenticationManager(
        @Qualifier("CustomReactiveAuthenticationManager")
                ReactiveAuthenticationManager delegate,
        @Qualifier("AntPathMatcher") PathMatcher pathMatcher) {
    this.delegate = delegate;
    this.pathMatcher = pathMatcher;
}

/**
 * Authenticate method
 *
 * @param authentication the {@link Authentication} to test
 * @return
 * @throws AuthenticationException
 */
@Override
public Mono<Authentication> authenticate(Authentication authentication)
        throws AuthenticationException {
    return Mono.deferContextual(
            contextView -> {
                ServerWebExchange exchange = contextView.get(ServerWebExchange.class);
                String path = exchange.getRequest().getURI().getPath();
                if (shouldAuthenticate(path)) {
                    return delegate.authenticate(authentication);
                } else {
                    return Mono.just(authentication);
                }
            });
}

/**
 * Returns if the given endpoint should be authenticated or not
 *
 * @param endpoint
 * @return
 */
private boolean shouldAuthenticate(String endpoint) {
    return restrictedPathPatterns.stream()
            .anyMatch(pattern -> pathMatcher.match(pattern, endpoint));
 }
}

这就是我在 Config 中定义 bean 的方式

@Bean(name = "CustomReactiveAuthenticationManager")
public ReactiveAuthenticationManager customReactiveAuthenticationManager(
        @Qualifier("AntPathMatcher") PathMatcher pathMatcher) {
    UserDetailsRepositoryReactiveAuthenticationManager delegate =
            new UserDetailsRepositoryReactiveAuthenticationManager(reactiveUserDetailsService);
    return new CustomAuthenticationManager(delegate, pathMatcher);
}

这就是我的安全过滤器链的样子

@Bean
public SecurityWebFilterChain securityWebFilterChain(
        ServerHttpSecurity http,
        ReactiveAuthenticationManager customReactiveAuthenticationManager) {
    http.csrf(ServerHttpSecurity.CsrfSpec::disable)
            .authorizeExchange(
                    authorize ->
                            authorize
                                    .pathMatchers("/actuator/**")
                                    .authenticated()
                                    .anyExchange()
                                    .permitAll())
            .httpBasic(Customizer.withDefaults())
            .authenticationManager(customReactiveAuthenticationManager);
    return http.build();
}

这是您如何配置 spring boot webflux 安全性以跳过对包含

Authorization
标头的未经身份验证端点的身份验证

最新问题
© www.soinside.com 2019 - 2024. All rights reserved.