如何正确扩展Azure AD UserPrincipal适配Spring Security
可落地的自定义用户模型实现方案
针对你描述的场景,完全不需要硬耦合SDK中的UserPrincipal类,也不用重写整套JWT认证逻辑,以下两种都是生产环境中常用的适配方案:
方案一:替换JWT认证的主体解析逻辑
核心逻辑是在JWT校验通过后的解析节点,直接生成你的自定义用户类型,替换SDK默认创建的UserPrincipal,让Security Context直接返回自定义类型的对象。
- 首先定义你的自定义用户类,直接实现Spring Security原生的
AuthenticatedPrincipal接口,不需要继承SDK的UserPrincipal:
import org.springframework.security.core.AuthenticatedPrincipal; import java.util.Map; public class MyCustomUser implements AuthenticatedPrincipal { // 存储从JWT中解析出的全量声明 private final Map<String, Object> claims; public MyCustomUser(Map<String, Object> claims) { this.claims = claims; } // 接口要求的主体名称方法,直接取JWT里的用户UPN即可 @Override public String getName() { return (String) claims.get("upn"); } // 自定义业务方法,直接从声明里取员工ID public String getEmployeeId() { return String.valueOf(claims.get("emplid")); } // 保留全量声明的获取能力,兼容原有业务的声明读取逻辑 public Map<String, Object> getClaims() { return claims; } }
- 然后实现自定义的JWT转换器,替代SDK默认把JWT转成
UserPrincipal的逻辑:
import org.springframework.core.convert.converter.Converter; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtGrantedAuthoritiesConverter; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.stereotype.Component; import java.util.Collection; @Component public class CustomJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> { @Override public AbstractAuthenticationToken convert(Jwt jwt) { // 直接基于JWT的全量声明构造自定义用户,不依赖SDK的UserPrincipal MyCustomUser principal = new MyCustomUser(jwt.getClaims()); // 权限解析逻辑和SDK保持一致,从JWT的roles声明中提取权限 Collection<GrantedAuthority> authorities = new JwtGrantedAuthoritiesConverter().convert(jwt); return new JwtAuthenticationToken(principal, authorities); } }
- 最后在Spring Security配置里注入这个自定义转换器,替换默认的解析逻辑:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; @Configuration public class SecurityConfig { @Autowired private CustomJwtAuthenticationConverter customJwtConverter; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) .oauth2ResourceServer() .jwt() .jwtAuthenticationConverter(customJwtConverter); return http.build(); } }
配置完成后,SecurityContextHolder中存储的principal直接就是MyCustomUser类型,不需要原来的外观包装类做二次转换,甚至可以直接通过@AuthenticationPrincipal MyCustomUser currentUser注解在接口、服务层方法参数中直接注入当前用户。
方案二:不修改默认认证流程的轻量适配
如果不想改动SDK默认的JWT校验、权限解析逻辑,可以在认证对象生成完成后,拦截替换其中存储的principal类型,全程不侵入SDK的默认流程:
- 实现Spring的对象后处理器,拦截JWT认证令牌的创建过程,替换里面的principal:
import com.azure.spring.autoconfigure.aad.UserPrincipal; import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.stereotype.Component; @Component public class CustomPrincipalPostProcessor implements ObjectPostProcessor<JwtAuthenticationToken> { @Override public <O extends JwtAuthenticationToken> O postProcess(O object) { // 拿到SDK默认创建的UserPrincipal,提取全量声明 UserPrincipal originalPrincipal = (UserPrincipal) object.getPrincipal(); MyCustomUser customUser = new MyCustomUser(originalPrincipal.getClaims()); // 用自定义用户重新构造认证令牌,替换原有的principal return (O) new JwtAuthenticationToken(customUser, object.getAuthorities(), customUser.getName()); } }
这种方式的改动量极小,完全兼容SDK的原有行为,SDK版本升级时也不会因为构造逻辑变更出现兼容问题。
实现注意事项
- 不要硬继承SDK的
UserPrincipal:从源码可见该类的构造方法强依赖JWT内部解析对象,没有对外暴露通用构造入口,硬继承会把你的代码和SDK内部实现强绑定,后续SDK版本迭代很容易出现不兼容问题。 - 不需要额外实现
UserDetails接口:你的场景是纯基于JWT声明的无状态校验,没有本地用户库查询的需求,实现AuthenticatedPrincipal接口就足够满足Spring Security的要求,硬套UserDetails只会引入不必要的权限判断冗余。 - 原有的
MyAppSecurityContext工具类可以简化为直接从SecurityContext读取强转后的principal,不需要再持有UserPrincipal的成员引用。
内容的提问来源于stack exchange,提问作者Josh
相关产品推荐
相关产品推荐

