Spring多租户场景下获取请求携带的OAuth2/OIDC Access Token问题
问题背景
我正尝试将原单用户本地系统改造为云环境下的多租户系统,已基于OAuth2、OIDC和PKCE实现跳转至外部认证提供商PingOne的功能。现在希望每个请求携带Access Token,以便根据令牌返回对应数据,但Spring默认仅传递ID Token,怀疑这与系统同时作为客户端和资源服务器有关。
已尝试操作:
- 通过Postman验证ID Token、Refresh Token和Access Token均可正常发放;
- 尝试通过
@AuthenticationPrincipal OidcUser oidcUser从请求中获取令牌,但仅能拿到ID Token; - 通过
OAuth2AuthorizedClientService获取令牌,同样只能得到ID Token。
相关依赖:spring-boot-starter-web、spring-boot-starter-security、spring-boot-starter-thymeleaf、spring-boot-starter-web-services、spring-boot-starter-oauth2-resource-server
核心原因
当Spring应用同时承担OAuth2客户端(处理登录跳转)和资源服务器(验证请求令牌)角色时,默认客户端配置只会将ID Token存入认证上下文,Access Token需要显式配置才能被保留和传递。
解决方案步骤
1. 配置OAuth2客户端保存完整授权信息
修改Spring Security配置类,确保客户端请求所需权限范围,同时让授权客户端服务保存包含Access Token的完整信息:
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) .oauth2Login(oauth2 -> oauth2 .authorizedClientService(authorizedClientService()) .userInfoEndpoint(userInfo -> userInfo.oidcUserService(oidcUserService()))) .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))); return http.build(); } @Bean public OAuth2AuthorizedClientService authorizedClientService( ClientRegistrationRepository clientRegistrationRepository) { return new DefaultOAuth2AuthorizedClientService(clientRegistrationRepository); } @Bean public OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() { final OidcUserService delegate = new OidcUserService(); return userRequest -> { // 加载包含Access Token的授权客户端信息 OAuth2AuthorizedClient authorizedClient = authorizedClientService().loadAuthorizedClient( userRequest.getClientRegistration().getRegistrationId(), userRequest.getPrincipal().getName()); // 获取默认用户信息 OidcUser oidcUser = delegate.loadUser(userRequest); // 将Access Token存入用户属性,方便后续调取 oidcUser.getAttributes().put("access_token", authorizedClient.getAccessToken().getTokenValue()); return oidcUser; }; } }
2. 在请求中携带Access Token
前端页面(Thymeleaf)
从认证用户属性中取出Access Token,在API请求头中携带:
<script> const accessToken = /*[[${#authentication.principal.attributes['access_token']}]]*/ ''; // 发起接口请求时添加Authorization头 fetch('/api/tenant-data', { headers: { 'Authorization': `Bearer ${accessToken}` } }); </script>
后端内部调用
通过OAuth2AuthorizedClientService直接获取Access Token:
@Service public class TenantDataService { private final OAuth2AuthorizedClientService authorizedClientService; public TenantDataService(OAuth2AuthorizedClientService authorizedClientService) { this.authorizedClientService = authorizedClientService; } public String getTenantSpecificData(Authentication authentication) { OAuth2AuthorizedClient authorizedClient = authorizedClientService.loadAuthorizedClient( "pingone", // 你的PingOne客户端注册ID authentication.getName()); String accessToken = authorizedClient.getAccessToken().getTokenValue(); // 用Access Token调用下游服务或查询租户数据 return fetchDataWithToken(accessToken); } }
3. 配置资源服务器解析Access Token
确保资源服务器能正确解析PingOne颁发的Access Token,调整JWT转换器适配PingOne的Token字段:
@Bean public JwtAuthenticationConverter jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_"); grantedAuthoritiesConverter.setAuthoritiesClaimName("roles"); // 对应PingOne Token中的权限字段 JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); converter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); return converter; }
4. 检查客户端注册范围
在配置文件中确认客户端注册包含openid及业务所需的权限范围(比如read、write):
spring: security: oauth2: client: registration: pingone: client-id: 你的PingOne客户端ID client-secret: 你的PingOne客户端密钥 scope: openid, read, write authorization-grant-type: authorization_code redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" provider: pingone: issuer-uri: https://你的PingOne颁发者域名.com user-name-attribute: sub
内容的提问来源于stack exchange,提问作者thies

