You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Security从ID Token获取用户邮箱用于审计的最优方案

最优解决方案分析

方案一:通过Cognito Pre Token Generation Lambda添加自定义声明到Access Token

这是最合规且低维护成本的方案,直接在令牌生成阶段将邮箱注入Access Token,无需额外请求或存储。

实现步骤:

  1. 创建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用户池的应用客户端设置中,开启"允许自定义声明"选项。

  2. 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
        }
    }
    
  3. 调整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配置的场景,但复杂度更高。

实现步骤:

  1. 修改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("/")
                }
        }
    }
    
  2. 自定义过滤器解析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)
        }
    }
    
  3. 注册过滤器到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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 22:24:55