使用不记名令牌 (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 字段中。
我还认为注入我的 ReactiveAuthorizationManager
的 ReactiveJwtDecoder
将尝试针对身份提供者解码测试 JWT,这将失败。
我可以嘲笑 ReactiveAuthorizationManager
或 ReativeJwtDecoder
。
有什么我遗漏的吗?
也许有一种方法可以使用身份服务 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 为我指明有用文档的方向。
我有一个 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 字段中。
我还认为注入我的 ReactiveAuthorizationManager
的 ReactiveJwtDecoder
将尝试针对身份提供者解码测试 JWT,这将失败。
我可以嘲笑 ReactiveAuthorizationManager
或 ReativeJwtDecoder
。
有什么我遗漏的吗? 也许有一种方法可以使用身份服务 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 为我指明有用文档的方向。