配置Spring资源服务器自动获取Auth0用户完整信息
问题:资源服务器如何自动获取Auth0 userinfo并存入SecurityContextHolder
我已配置针对Auth0认证服务器验证JWT令牌的资源服务器,通过issuer-uri指定了Auth0地址。用户在公共客户端通过Auth0认证后获取JWT令牌,用该令牌请求资源服务器可成功授权,但SecurityContextHolder仅包含JWT解析出的基础数据,无用户姓名、邮箱等完整信息。Auth0提供的userinfo端点可获取这些信息,我希望将用户邮箱、姓名存入SecurityContextHolder,想知道是否可通过配置自动实现,或最佳实现方式是什么?
现有配置代码
安全过滤器链配置
@Bean fun filterChain(http: HttpSecurity): SecurityFilterChain { http.authorizeRequests().anyRequest().permitAll() .and() .oauth2ResourceServer().jwt(); return http.build() }
JWT解码器Bean
@Bean fun jwtDecoder(): JwtDecoder? { val jwtDecoder = JwtDecoders.fromOidcIssuerLocation<JwtDecoder>(issuer) as NimbusJwtDecoder val audienceValidator: OAuth2TokenValidator<Jwt> = AudienceValidator(audience) val withIssuer = JwtValidators.createDefaultWithIssuer(issuer) val withAudience: OAuth2TokenValidator<Jwt> = DelegatingOAuth2TokenValidator(withIssuer, audienceValidator) jwtDecoder.setJwtValidator(withAudience) return jwtDecoder }
application.properties
spring.security.oauth2.resourceserver.jwt.issuer-uri=my-domain.com spring.security.oauth2.resourceserver.jwt.audience=my-audience
从Auth0获取的JWT负载
{ "iss": "https://dev-abcdefgh.us.auth0.com/", "sub": "google-oauth2|353335637216442227159", "aud": [ "my-audience", "https://dev-3ag8q43b.us.auth0.com/userinfo" ], "iat": 1663100248, "exp": 1663186648, "azp": "m01yBdKdQd5erBxriQde24ogfsdAsYvD", "scope": "openid profile email" }
解决方案
方案一:自定义JWT转换器,调用userinfo端点补充用户信息
通过自定义JwtAuthenticationConverter,在JWT转换为Authentication对象的过程中,调用Auth0的userinfo端点获取用户完整信息,并将其存入Authentication的详情中。
1. 配置WebClient用于调用userinfo端点
@Bean fun webClient(): WebClient { return WebClient.builder().build() }
2. 实现自定义JwtAuthenticationConverter
@Component class CustomJwtAuthenticationConverter( private val webClient: WebClient, @Value("\${spring.security.oauth2.resourceserver.jwt.issuer-uri}") private val issuerUri: String ) : JwtAuthenticationConverter() { override fun convert(jwt: Jwt): AbstractAuthenticationToken { val baseAuth = super.convert(jwt) as JwtAuthenticationToken // 调用Auth0 userinfo端点获取用户信息 val userInfo = webClient.get() .uri("${issuerUri}userinfo") .headers { it.setBearerAuth(jwt.tokenValue) } .retrieve() .bodyToMono<Map<String, Any>>() .block() // 同步调用,若需异步可调整为非阻塞方式 // 将用户信息合并到Authentication详情中 val mergedDetails = mutableMapOf<String, Any>().apply { putAll(baseAuth.details as Map<String, Any>) userInfo?.let { putAll(it) } } return JwtAuthenticationToken( jwt, baseAuth.authorities, baseAuth.name ).apply { details = mergedDetails } } }
3. 配置过滤器链使用自定义转换器
@Bean fun filterChain(http: HttpSecurity, customConverter: CustomJwtAuthenticationConverter): SecurityFilterChain { http.authorizeRequests().anyRequest().permitAll() .and() .oauth2ResourceServer() .jwt() .jwtAuthenticationConverter(customConverter); // 绑定自定义转换器 return http.build() }
方案二:配置Auth0将用户信息嵌入JWT(推荐)
每次请求调用userinfo会增加网络开销和响应延迟,更高效的方式是在Auth0控制台配置,让JWT负载直接包含用户邮箱、姓名等信息。
操作步骤:
- 登录Auth0控制台,进入目标应用的Actions > Flows > Login
- 添加一个自定义Action,编写代码将用户属性注入JWT:
exports.onExecutePostLogin = async (event, api) => { const namespace = 'https://your-app-namespace/'; // 自定义命名空间,避免字段冲突 api.idToken.setCustomClaim(`${namespace}email`, event.user.email); api.idToken.setCustomClaim(`${namespace}name`, event.user.name); api.idToken.setCustomClaim(`${namespace}picture`, event.user.picture); };
- 保存并启用该Action,用户后续获取的JWT将包含这些自定义字段。
解析JWT中的自定义字段
此时只需修改JwtAuthenticationConverter,解析JWT中的自定义字段并存入Authentication:
@Component class CustomJwtAuthenticationConverter : JwtAuthenticationConverter() { private val namespace = "https://your-app-namespace/" override fun extractAuthorities(jwt: Jwt): Collection<GrantedAuthority> { // 保留原有权限逻辑,或自定义权限解析 return super.extractAuthorities(jwt) } override fun convert(jwt: Jwt): AbstractAuthenticationToken { val auth = super.convert(jwt) as JwtAuthenticationToken val userDetails = mapOf( "email" to jwt.claims["${namespace}email"], "name" to jwt.claims["${namespace}name"] ) auth.details = userDetails return auth } }
方案对比
- 方案一:无需修改Auth0配置,快速实现,但每次请求都发起额外网络调用,性能开销较大,适合临时场景。
- 方案二:无额外网络请求,性能更优,是生产环境的推荐方案,但需要在Auth0控制台进行配置。
内容的提问来源于stack exchange,提问作者Denis Stephanov
相关产品推荐
相关产品推荐

