使用不记名令牌 (JWT) 和自定义声明的 Webflux 安全授权测试

Webflux security authorisation test with bearer token (JWT) and custom claim

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

我正在改变 WebTestClient 以包含具有适当声明的 JWT (myClaim),但是在我的自定义 ReactiveAuthorizationManager 请求中没有持有者令牌 header,因此没有任何可解码或声称验证请求失败的授权,因为它应该。
因此,我的测试设置是:

@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() 未放入请求 Authorization header 字段中。

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

我可以嘲笑 ReactiveAuthorizationManagerReativeJwtDecoder

有什么我遗漏的吗? 也许有一种方法可以使用身份服务 JWK 集 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();
    }
}

粗略地说,事实证明我实际上在做的是使用自定义声明作为“权限”,也就是说“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 绑定到服务器,但是 apply(springSecurity()) (在评论 3 处)在使用 [=22 时 return 不是 apply 的合适类型=]

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

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

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