Spring Security从ID Token获取用户邮箱用于审计的最优方案
方案一:通过Cognito Pre Token Generation Lambda添加自定义声明到Access Token
这是最合规且低维护成本的方案,直接在令牌生成阶段将邮箱注入Access Token,无需额外请求或存储。
实现步骤:
创建Cognito Pre Token Generation Lambda触发器
在AWS Cognito用户池的"触发器"选项中,添加Pre Token Generation类型的Lambda函数,在函数中把用户邮箱写入Access Token:exports.handler = (event, context, callback) => { const email = event.request.userAttributes.email; event.response = { claimsOverrideDetails: { claimsToAddOrOverride: { 'email': email }, groupOverrideDetails: {} } }; callback(null, event); };同时要在Cognito用户池的应用客户端设置中,开启"允许自定义声明"选项。
Spring Security配置自定义JwtAuthenticationConverter
修改Resource Server配置,自定义转换器提取邮箱声明并存入Authentication,供AuditorAware使用:@Configuration class ResourceServerConfig : WebSecurityConfigurerAdapter() { override fun configure(http: HttpSecurity) { http .authorizeRequests() .anyRequest().authenticated() .and() .oauth2ResourceServer() .jwt() .jwtAuthenticationConverter(customJwtAuthenticationConverter()) } private fun customJwtAuthenticationConverter(): JwtAuthenticationConverter { val converter = JwtAuthenticationConverter() converter.setJwtGrantedAuthoritiesConverter(JwtGrantedAuthoritiesConverter()) // 自定义Principal,同时保留用户ID和邮箱 converter.setPrincipalExtractor { jwt: Jwt -> mutableMapOf( "id" to jwt.subject, "email" to jwt.claims["email"] ) } return converter } }调整AuditorAware逻辑
从自定义Principal中提取所需信息:class PrincipalIdExtractor : AuditorAware<UUID> { override fun getCurrentAuditor(): Optional<UUID> { val authentication = SecurityContextHolder.getContext().authentication return if (authentication == null || !authentication.isAuthenticated) { Optional.empty() } else { val principal = authentication.principal as Map<*, *> val userId = principal["id"] as String // 如需获取邮箱,直接取principal["email"] as String即可 Optional.ofNullable(UUID.fromString(userId)) } } }
方案二:存储ID Token到Cookie并通过过滤器解析
此方案适合无法修改Cognito配置的场景,但复杂度更高。
实现步骤:
修改OAuth2 Client配置,存储ID Token到Cookie
登录成功后,将Cognito返回的ID Token存入安全Cookie:@Configuration class OAuth2ClientConfig : WebSecurityConfigurerAdapter() { override fun configure(http: HttpSecurity) { http .oauth2Login() .successHandler { request, response, authentication -> val oauth2Token = authentication as OAuth2AuthenticationToken val idToken = oauth2Token.principal.attributes["id_token"] as String val cookie = Cookie("id_token", idToken).apply { isHttpOnly = true secure = true sameSite = Cookie.SameSite.STRICT.value path = "/" } response.addCookie(cookie) response.sendRedirect("/") } } }自定义过滤器解析ID Token并存入SecurityContext
创建过滤器从Cookie取出ID Token,验证后将邮箱添加到Authentication:@Component class IdTokenParsingFilter(private val jwtDecoder: JwtDecoder) : OncePerRequestFilter() { override fun doFilterInternal( request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain ) { val authentication = SecurityContextHolder.getContext().authentication if (authentication is JwtAuthenticationToken && authentication.details !is Map<*, *>) { request.cookies?.find { it.name == "id_token" }?.let { cookie -> try { val jwt = jwtDecoder.decode(cookie.value) val details = mutableMapOf("email" to jwt.claims["email"] as String) (authentication as AbstractAuthenticationToken).details = details } catch (e: Exception) { // 解析失败,忽略或按需处理 } } } filterChain.doFilter(request, response) } }注册过滤器到Spring Security链
在WebSecurity配置中添加该过滤器,确保在BearerTokenAuthenticationFilter之后执行:@Configuration class WebSecurityConfig : WebSecurityConfigurerAdapter() { @Autowired private lateinit var idTokenParsingFilter: IdTokenParsingFilter override fun configure(http: HttpSecurity) { http .addFilterAfter(idTokenParsingFilter, BearerTokenAuthenticationFilter::class.java) // 其他配置... } }
方案对比与最优选择
- 方案一优势:符合OAuth2协议设计,Access Token专为Resource Server提供用户信息,无需额外存储与解析逻辑,安全性高,维护成本低,性能更优。
- 方案二劣势:需要额外管理ID Token的Cookie存储,每次请求需解析验证ID Token,增加复杂度与性能开销,且ID Token的设计初衷是供客户端使用,而非Resource Server。
综上,方案一(自定义Cognito Access Token添加声明)为最优解决方案。
内容的提问来源于stack exchange,提问作者Leonardo

