我正在使用 Spring Security OAuth2 支持将多个 API 作为资源服务器从单个 Web 应用程序公开。我们希望每个 API 都需要不同的“受众”,以便不同的程序可以访问不同的 API。我为每个 API 设置单独的 SecurityFilterChains(相同的颁发者 URI,但不同的受众)。通过这种配置,每个安全过滤器链都有不同的承载令牌过滤器。我看到的问题是,任何 OAuth2 传入请求都会被安全过滤器链之一拾取(基于顺序),并且由于正确的相应身份验证类型,它会尝试处理。但是,如果受众与该 API 的值不匹配,则会引发错误,并且不会尝试其他 API。看来承载令牌过滤器位于请求匹配器之前,因此它正在处理与另一个 API URL 匹配的消息。
经过一些研究,似乎需要 AuthenticationManagerResolver 的替代实现(而不是 JwtIssuerAuthenticationManagerResolver)来查找受众匹配。或者,我想我们可以考虑在承载令牌过滤器之前放置一个请求匹配。
有什么想法或建议吗?
我认为你最初的想法是使用一个
SecurityFilterChain
bean par“API”是正确的。也许您只是缺少每个 securityMatcher
(但是 @Order
中的最后一个,因此它充当默认值)。
您很可能可以基于路径前缀定义安全匹配器。也许,如果所有访问令牌都是 JWT,您可以尝试匹配
aud
声明上的请求而不是请求路径,但第一种方法肯定更容易。
这是具有两种不同方法的示例:
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
SecurityFilterChain
audxFilterChain(HttpSecurity http, @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") URI issuer, @Value("${audx}") String audx)
throws Exception {
http.securityMatcher("/api/x/**");
http.oauth2ResourceServer(oauth2 -> {
oauth2.jwt(jwt -> {
final var issValidator = JwtValidators.createDefaultWithIssuer(issuer.toString());
final var audValidator = new JwtClaimValidator<List<String>>(JwtClaimNames.AUD, (aud) -> aud != null && aud.contains(audx));
final var validator = new DelegatingOAuth2TokenValidator<>(List.of(issValidator, audValidator));
final var decoder = NimbusJwtDecoder.withIssuerLocation(issuer.toString()).build();
decoder.setJwtValidator(validator);
jwt.decoder(decoder);
});
});
http.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.csrf(csrf -> csrf.disable());
return http.build();
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
SecurityFilterChain
audyFilterChain(HttpSecurity http, @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") URI issuer, @Value("${audy}") String audy)
throws Exception {
http.securityMatcher((HttpServletRequest request) -> {
return Optional.ofNullable(request.getHeader(HttpHeaders.AUTHORIZATION))
.filter(StringUtils::hasText)
.filter(auth -> auth.toLowerCase().startsWith("bearer "))
.map(auth -> auth.substring(7))
.map(bearer -> {
try {
final var jwt = JWTParser.parse(bearer);
return jwt.getJWTClaimsSet().getAudience() != null && jwt.getJWTClaimsSet().getAudience().contains(audy);
} catch (ParseException e) {
return false;
}
}).orElse(false);
});
http.oauth2ResourceServer(oauth2 -> {
// audience is already validated in the matcher
oauth2.jwt(jwt -> {
});
});
http.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.csrf(csrf -> csrf.disable());
return http.build();
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE + 2)
SecurityFilterChain defaultFilterChain(HttpSecurity http, @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") URI issuer) throws Exception {
// it's generally a good idea to define a default filter-chain for requests that were matched
// by none of the security matchers from higher @Order filter-chains
http.authorizeHttpRequests(requests -> requests.anyRequest().denyAll());
http.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.csrf(csrf -> csrf.disable());
return http.build();
}