Spring Boot OAuth2ResourceServer如何支持1-2个不同签发方的JWT令牌?
目前难以找到适配自身场景的解决方案,相关文档匮乏,仅找到一篇略微相关的Stack Overflow帖子。
场景概述
我们所有Spring Boot微服务均配置了基于内部签发方(Provider A)的OAuth2ResourceServer,仅允许其他配置了OAuth2 WebClient的微服务或员工访问,这部分运行正常。
但问题在于,部分微服务端点需要额外的Provider B令牌来授权客户(例如访问或修改其数据)。所有发送至微服务的HTTP请求均携带Authorization头(对应Provider A),部分请求还会携带CUSTOMER_TOKEN头(对应Provider B)。
我的问题
如何配置Spring Boot,使OAuth2ResourceServer支持来自两个不同签发方的1个或2个JWT令牌?是否有相关指引或更优方案?
已尝试方案
目前我尝试了几种方案,但都感觉不够规范,怀疑未找到正确方向。我想出的“最佳”方案包含配置自定义Customizer<OAuth2ResourceServerConfigurer<HttpSecurity>>、自定义AuthenticationManagerResolver、自定义AccessDeniedHandler、自定义AuthenticationEntrypoint,以及两个分别配置对应JWK Set URI和Issuer URI的JwtDecoder:
@Bean protected SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { return httpSecurity.csrf() .disable() .cors() .configurationSource(corsConfigurationSource()) .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers(WHITELIST) .permitAll() .and() .authorizeRequests() .anyRequest() .authenticated() .and() .oauth2ResourceServer(configurer -> configurer .authenticationManagerResolver(authenticationManagerResolver()) .authenticationEntryPoint(authenticationEntryPoint()) .accessDeniedHandler(accessDeniedHandler()); ) .build(); } @Bean public AccessDeniedHandler accessDeniedHandler(){ return (request, response, accessDeniedException) -> { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setCharacterEncoding(StandardCharsets.UTF_8.name()); PrintWriter writer = response.getWriter(); writer.write("\"message\": \"access denied\""); writer.flush(); }; } @Bean public AuthenticationEntryPoint authenticationEntryPoint(){ return (request, response, authenticationException) -> { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setCharacterEncoding(StandardCharsets.UTF_8.name()); PrintWriter writer = response.getWriter(); writer.write("\"message\": \"unauthorized\""); writer.flush(); }; } @Bean protected AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver() { return request -> authentication -> { List<GrantedAuthority> authorities = new ArrayList<>(); // merged authorities Map<String, Object> attributes = new HashMap<>(); // merged attributes Authentication providerA = authenticate(request, OAuth2Provider.PROVIDER_A, authorities, attributes); if (request.getHeader(OAuth2Provider.PROVIDER_B.getHeader()) == null) { // If Provider B token is not in the request, the user won't have the appropriate role // to access endpoints annotated with @RoleAllowed(CUSTOMER_ROLE) return providerA; } return authenticate(request, OAuth2Provider.PROVIDER_B, authorities, attributes); }; } public final Authentication authenticate( HttpServletRequest request, OAuth2Provider oAuth2Provider, List<GrantedAuthority> authorities, Map<String, Object> attributes) { Jwt jwt = decodeAccessToken(request, oAuth2Provider); // JwtDecoder + error handling authorities.add(new SimpleGrantedAuthority(oAuth2Provider.getRole())); authorities.addAll(getTokenAuthorities(jwt)); Map<String, Object> providerAttributes = new HashMap<>(jwt.getClaims()); providerAttributes.put("token", jwt); attributes.put(oAuth2Provider.getName(), providerAttributes); OAuth2AuthenticatedPrincipal principal = new DefaultOAuth2AuthenticatedPrincipal(attributes, authorities); OAuth2AccessToken accessToken = new OAuth2AccessToken( TokenType.BEARER, jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt()); return new BearerTokenAuthentication(principal, accessToken, authorities); }
该方案虽能运行,但感觉不够规范,因为需要手动返回401或403状态码,这在安全处理中并非理想做法。此外,Spring 3.x已弃用HttpServletRequest,更让我觉得该方案不够合理,希望找到更优解决方案。
内容的提问来源于stack exchange,提问作者G.T.

