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

Spring Security类型安全访问自定义OIDC声明的最佳方案

问题背景

我们的Web应用基于OpenID Connect + Spring Security 5实现用户认证,借助框架开箱即用的OIDC能力,仅需通过oauth2Login配置即可快速完成OAuth2认证集成。

身份提供方(IdP)通过userInfo端点暴露了业务逻辑中高频使用的属性,其中包含不少富对象列表,使用通用Map<String,Object>结构访问存在诸多不便,我们需要以类型安全的方式访问这些属性。

默认实现中,OidcUserService会将userInfo端点返回的属性与ID令牌中的属性合并,存入DefaultOidcUser,作为OAuth2AuthenticationToken的组成部分,目前默认获取用户属性有三种途径:

  • 通过@AuthenticationPrincipal DefaultOidcUser defaultOidcUser注入,获取以Map结构存储全量属性的DefaultOidcUser实例
  • 注入java.security.Principal,获取OAuth2AuthenticationToken实例,其内部持有的principal即为上述DefaultOidcUser
  • 通过SecurityContextHolder.getContext().getAuthentication()获取,返回值同样是OAuth2AuthenticationToken实例

现有方案的缺陷

目前已尝试的三种属性访问方案都存在明显不足:

  1. 对Principal或SecurityContextHolder获取的认证对象做类型强转
    示例代码:

    public void doSomething(Principal principal) {
       OAuth2AuthenticationToken oAuth2AuthenticationToken = (OAuth2AuthenticationToken)principal;
       Map<String, Object> attributes = oAuth2AuthenticationToken.getPrincipal().getAttributes();
      ...
    }
    

    缺点是需要手动做类型强转,且Map转POJO的逻辑散落在各业务代码中,无法做到一次转换全局复用。

  2. 通过@AuthenticationPrincipal注解直接注入DefaultOidcUser访问属性
    示例代码:

    public void doSomething(@AuthenticationPrincipal DefaultOidcUser defaultOidcUser) {
        Map<String, Object> attributes = defaultOidcUser.getAttributes();
        ...
    }
    

    该方案省去了手动强转的步骤,但仍无法直接获取POJO类型的自定义属性。

  3. 自定义OAuth2UserService钩子返回自定义OidcUser实现,暴露类型安全属性
    示例代码:

    private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
        final OidcUserService delegate = new OidcUserService();
    
        return (userRequest) -> {
            // 加载默认生成的用户对象
            OidcUser oidcUser = delegate.loadUser(userRequest);
    
            // 解析自定义声明
            CustomTypedAttributes customTypedAttributes = objectMapper.convertValue(oidcUser.getClaims(), CustomTypedAttributes.class);
    
            // 封装为自定义用户对象返回
            return new CustomOidcUser(oidcUser.getAuthorities(), oidcUser.getIdToken(), oidcUser.getUserInfo(), customTypedAttributes);
        };
    }
    

    该方案的优势是用户登录时仅需做一次属性转换,后续即可通过customOidcUser.getCustomTypedAttributes()实现类型安全的属性访问,但仅为暴露类型安全属性就封装完整OidcUser实现的方式不够简洁优雅。

我们需要的替代方案需要满足三个核心要求:

  • 支持便捷、类型安全地访问自定义OIDC属性
  • 基于Spring Security标准原生组件实现
  • 对测试友好,可直接适配Spring Security测试框架

推荐实现方案

直接利用Spring Security原生支持的AuthenticationPrincipal自定义解析能力实现,不需要封装自定义OidcUser,侵入性极低,完全符合要求。

实现步骤

  1. 定义自定义注入注解,通过SpEL声明属性解析逻辑

    @Target({ElementType.PARAMETER, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @AuthenticationPrincipal(expression = "@oidcAttributeMapper.map(#this)")
    public @interface CurrentUser {}
    

    注解中SpEL表达式的#this指代当前认证对象中的principal(即默认的DefaultOidcUser实例),表达式会直接调用Spring容器中名为oidcAttributeMapper的Bean的转换方法,返回最终的POJO对象。

  2. 实现统一属性转换Mapper,注册为Spring Bean

    @Component
    public class OidcAttributeMapper {
        private final ObjectMapper objectMapper;
    
        public OidcAttributeMapper(ObjectMapper objectMapper) {
            this.objectMapper = objectMapper;
        }
    
        public CustomTypedAttributes map(OidcUser oidcUser) {
            // 可按需添加缓存,避免同一会话内重复转换
            return objectMapper.convertValue(oidcUser.getClaims(), CustomTypedAttributes.class);
        }
    }
    

    如果业务场景中用户属性不会在会话周期内动态变更,也可以将转换结果按用户唯一标识(sub声明)做缓存,进一步降低开销,普通业务场景下直接转换的性能损耗完全可以忽略。

  3. 业务代码中直接注入使用即可

    @RestController
    public class BusinessController {
        @GetMapping("/profile")
        public CustomTypedAttributes getProfile(@CurrentUser CustomTypedAttributes currentUser) {
            // 直接拿到类型安全的POJO,无强转、无零散Map解析逻辑
            return currentUser;
        }
    }
    

方案优势

  • 完全基于Spring Security原生的AuthenticationPrincipalArgumentResolver实现,没有引入非标准自定义组件
  • 不需要修改原有OidcUser、OidcUserService的核心逻辑,侵入性极低
  • 测试适配成本极低:使用Spring Security Test框架时,既可以直接Mock转换后的POJO对象注入,也可以通过默认的@WithMockUser、测试SecurityContext构造逻辑快速构造测试场景,不需要额外适配自定义OidcUser的构造逻辑
  • 属性转换逻辑全局统一维护,后续调整映射规则时仅需修改Mapper代码,业务代码完全无感知

如果需要更细粒度的属性注入,比如仅需要注入单个声明字段而非完整POJO,甚至不需要编写Mapper,直接在注解中写SpEL读取对应claim即可,例如@AuthenticationPrincipal(expression = "claims['phone_number']")可以直接注入用户手机号字符串。


内容的提问来源于stack exchange,提问作者ddewaele

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.02 00:39:35