【问题标题】:Webflux security authorisation test with bearer token (JWT) and custom claim使用不记名令牌 (JWT) 和自定义声明进行 Webflux 安全授权测试
【发布时间】:2021-01-06 10:21:18
【问题描述】:

我有一个充当资源服务器的 Spring Boot (2.3.6.RELEASE) 服务,它已使用 Webflux 实现,客户端 jwt 由第三方身份服务器提供。 我正在尝试使用 JUnit 5 和@SpringBootTest 测试端点的安全性。 (据记录,在手动测试期间,安全性似乎可以按要求工作)

我正在改变 WebTestClient 以包含一个带有适当声明 (myClaim) 的 JWT,但是在我的自定义 ReactiveAuthorizationManager 中,请求标头中没有承载令牌,因此无需解码或声明验证请求授权失败,这是应该的。
因此,我的测试设置是:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
class ControllerTest {

    @Autowired
    private ApplicationContext applicationContext;

    private WebTestClient webTestClient;

    @BeforeEach
    void init() {
        webTestClient = WebTestClient
                .bindToApplicationContext(applicationContext)
                .apply(springSecurity())
                .configureClient()
                .build();
    }

    @Test
    void willAllowAccessForJwtWithValidClaim() {
        webTestClient.mutateWith(mockJwt().jwt(jwt -> jwt.claim("myClaim", "{myValue}")))
                .get()
                .uri("/securedEndpoint")
                .exchange()
                .expectStatus()
                    .isOk();
    }
}

我一直在尝试关注这个guide 为了以防万一,我已经尝试过使用和不使用.filter(basicAuthentication()) 的客户端:)

在我看来,mockJwt() isint 被放入请求 Authorization 标头字段中。

我还认为注入我的ReactiveAuthorizationManagerReactiveJwtDecoder 将尝试针对身份提供者解码测试JWT,这将失败。

我可以模拟ReactiveAuthorizationManagerReativeJwtDecoder

我有什么遗漏吗? 也许有一种方法可以使用 Identity Services JWK set uri 创建“测试”JWT?

其他细节: ReactiveAuthorizationManager 和安全配置的详细信息

public class MyReactiveAuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
    private static final AuthorizationDecision UNAUTHORISED = new AuthorizationDecision(false);

    private final ReactiveJwtDecoder jwtDecoder;

    public JwtRoleReactiveAuthorizationManager(final ReactiveJwtDecoder jwtDecoder) {
        this.jwtDecoder = jwtDecoder;
    }

    @Override
    public Mono<AuthorizationDecision> check(final Mono<Authentication> authentication, final AuthorizationContext context) {
        final ServerWebExchange exchange = context.getExchange();
        if (null == exchange) {
            return Mono.just(UNAUTHORISED);
        }

        final List<String> authorisationHeaders = exchange.getRequest().getHeaders().getOrEmpty(HttpHeaders.AUTHORIZATION);
        if (authorisationHeaders.isEmpty()) {
            return Mono.just(UNAUTHORISED);
        }

        final String bearer = authorisationHeaders.get(0);

        return jwtDecoder.decode(bearer.replace("Bearer ", ""))
                .flatMap(jwt -> determineAuthorisation(jwt.getClaimAsStringList("myClaim")));
    }

    private Mono<AuthorizationDecision> determineAuthorisation(final List<String> claimValues) {
        if (Objects.isNull(claimValues)) {
            return Mono.just(UNAUTHORISED);
        } else {
            return Mono.just(new AuthorizationDecision(!Collections.disjoint(claimValues, List.of("myValues")));
        }
    }
}
@EnableWebFluxSecurity
public class JwtSecurityConfig {

    @Bean
    public SecurityWebFilterChain configure(final ServerHttpSecurity http,
                                            final ReactiveAuthorizationManager reactiveAuthorizationManager) {
        http
                .csrf().disable()
                .logout().disable()
                .authorizeExchange().pathMatchers("/securedEndpoint").access(reactiveAuthorizationManager)
                .anyExchange().permitAll()
                .and()
                .oauth2ResourceServer()
                .jwt();

        return http.build();
    }
}

【问题讨论】:

  • .bindToApplicationContext(applicationContext) 用于当你有一个模拟的ApplicationContext 另一方面你似乎正在运行一个完整的服务器@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 这意味着你应该使用bindToServer() 而不是docs.spring.io/spring-framework/docs/current/javadoc-api/org/…跨度>
  • 或者你省略bindTo...,它将默认配置docs.spring.io/spring-boot/docs/current/reference/htmlsingle/…
  • @Toerktumlare 感谢 cmets :) 有趣的是,如果我尝试使用 bindToServer 我不能 apply(springSecurity() 它的类型错误,但值得在这里更深入地玩一下。如果我自动连接WebTestClient,我会得到NullPointerException。我怀疑存在错误,或者功能尚不存在,因为我还没有看到任何有关处理自定义声明的文档。
  • @Toerktumlare 谢谢你,这很有帮助。当有这么多不同的解决方案时,很难知道正确的方法。我会看看GrantedAuthoritiesMapperJwtAuthenticationConverter,这可能更适合我的用例。
  • @WUJ 有趣的想法。我不知道 JWSSigner。我想我现在已经整理好了(著名的遗言),但我肯定会检查 JWSSigner,它可能在未来证明是有用的,谢谢 :)

标签: testing jwt spring-webflux spring-security-oauth2


【解决方案1】:

粗略地说,事实证明我实际上在做的是使用自定义声明作为“授权”,也就是说“myClaim”必须包含“x”值才能允许访问给定路径。 这与作为简单自定义声明的声明略有不同,即令牌中的额外数据位(可能是用户首选的配色方案)。
考虑到这一点,我意识到我在测试中观察到的行为可能是正确的,所以我没有实现ReactiveAuthorizationManager,而是选择配置ReactiveJwtAuthenticationConverter

    @Bean
    public ReactiveJwtAuthenticationConverter jwtAuthenticationConverter() {
        final JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();
        converter.setAuthorityPrefix("");       // 1
        converter.setAuthoritiesClaimName("myClaim");

        final Converter<Jwt, Flux<GrantedAuthority>> rxConverter = new ReactiveJwtGrantedAuthoritiesConverterAdapter(converter);

        final ReactiveJwtAuthenticationConverter jwtAuthenticationConverter = new ReactiveJwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(rxConverter);
        return jwtAuthenticationConverter;
    }

(注释 1;JwtGrantedAuthoritiesConverter 在声明值前添加“SCOPE_”,这可以使用 setAuthorityPrefix see 进行控制)

这需要对 SecurityWebFilterChain 配置进行调整:

        http
                .csrf().disable()
                .logout().disable()
                .authorizeExchange().pathMatchers("securedEndpoint").hasAnyAuthority("myValue)
                .anyExchange().permitAll()
                .and()
                .oauth2ResourceServer()
                .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter));

测试

@SpringBootTest
class ControllerTest {
    private WebTestClient webTestClient;

    @Autowired
    public void setUp(final ApplicationContext applicationContext) {
        webTestClient = WebTestClient
                .bindToApplicationContext(applicationContext) // 2
                .apply(springSecurity())  // 3
                .configureClient()
                .build();
    }

    @Test
    void myTest() {
        webTestClient
                .mutateWith(mockJwt().authorities(new SimpleGrantedAuthority("myValue")))  // 4
                .build()
                .get()
                .uri("/securedEndpoint")
                .exchange()
                .expectStatus()
                    .isOk()
    } 
}

为了使测试“正常工作,WebTestClient 似乎需要绑定到应用程序上下文(在评论 2 中)。
理想情况下,我宁愿将WebTestClient 绑定到服务器,但是在使用bindToServerapply(springSecurity())(在评论3)不会为apply 返回适当的类型

在测试时有许多不同的方法可以“模拟”JWT,其中一种(在评论 4 中)用于替代方案,请参阅 spring 文档here

我希望这对将来的其他人有所帮助,安全性和 OAuth2 可能会令人困惑:)

感谢 @Toerktumlare 为我指明有用文档的方向。

【讨论】:

    猜你喜欢
    • 2021-10-05
    • 2021-08-17
    • 1970-01-01
    • 2019-06-04
    • 2019-02-05
    • 1970-01-01
    • 2020-07-18
    • 2021-09-25
    • 2017-07-25
    相关资源
    最近更新 更多