Spring Security 对接 Entra 时为何找不到 preferred_username?
完成登录时,业务用户名不必固定改成 email,仍可以使用 ID token 中的 preferred_username。建议将 provider 的 user-name-attribute 配置为 sub,先让默认 UserInfo 加载成功,再从 ID token 读取 preferred_username。如果需要让 OidcUser.getName() 返回该值,可以在自定义 OidcUserService 中重新构造用户对象。
通过自定义 OidcUserService 映射 Entra 的 roles 是合适的扩展方式,但要处理缺失值,并保留需要的原有 authorities。
为什么 token 中有属性,Spring 仍然报缺失?
这里涉及两份数据:
- ID token:OIDC 登录时返回的身份令牌,可以包含
preferred_username、roles等 claims。 - UserInfo 响应:使用 access token 调用
https://graph.microsoft.com/oidc/userinfo得到的用户信息。
Entra 的 UserInfo endpoint 返回固定范围的用户信息。给 token 添加 Optional Claim 不会让 UserInfo 响应增加该字段,也不能通过 Optional Claim 自定义 UserInfo 的返回内容。Microsoft UserInfo 文档
对于题目描述的调用路径,OidcUserService 会委托 DefaultOAuth2UserService 获取 UserInfo。后者构造用户对象时,会按照 user-name-attribute 检查 UserInfo attributes。如果配置为 preferred_username,而响应中没有该属性,就会在组合 ID token 和 UserInfo 之前失败。Spring Security 用户服务说明、DefaultOAuth2UserService 实现
OAuth2UserRequestEntityConverter 负责构造请求;发送请求、处理响应和构造用户对象由用户服务完成。
题目只说明“token 中存在该字段”,还需要确认它存在于本次登录返回的 ID token 中。某个 access token 包含该字段,并不能保证 OidcUser 能读取到它。
先将 UserInfo 的用户名属性设为 sub
将以下配置合并到现有 YAML 中,保留原来的 registration、issuer-uri 和资源服务器配置:
spring:
security:
oauth2:
client:
provider:
entra:
user-name-attribute: sub
sub 是 OIDC 的主体标识。Spring 的 OIDC 用户服务会检查 UserInfo 的 sub 是否存在,以及是否与 ID token 的 sub 一致。不要为了绕过用户名错误而删除这项校验。OidcUserService 实现
题目中的 YAML 没有显示层级缩进。实际配置必须保持正确缩进,但不能仅凭粘贴内容认定运行环境存在缩进问题。
如果只需要在页面展示用户名,完成这一步后可以直接读取:
String preferredUsername = oidcUser.getIdToken()
.getClaimAsString("preferred_username");
这不要求 oidcUser.getName() 也返回同一个值。
同时设置 principal name 并映射角色
如果业务代码依赖 getName() 返回 preferred_username,可以在默认加载完成后创建一个 DefaultOidcUser。以下方法可放入现有安全配置类中,相关类型需添加对应 import。
@Bean
public OAuth2UserService<OidcUserRequest, OidcUser> entraOidcUserService() {
OidcUserService delegate = new OidcUserService();
return userRequest -> {
OidcUser user = delegate.loadUser(userRequest);
Set<GrantedAuthority> authorities =
new HashSet<>(user.getAuthorities());
Object rolesClaim = user.getIdToken().getClaims().get("roles");
if (rolesClaim instanceof Collection<?>) {
for (Object role : (Collection<?>) rolesClaim) {
if (role instanceof String
&& !((String) role).trim().isEmpty()) {
authorities.add(
new SimpleGrantedAuthority("ROLE_" + role)
);
}
}
}
String preferredUsername = user.getIdToken()
.getClaimAsString("preferred_username");
String nameAttribute = "sub";
if (preferredUsername != null
&& !preferredUsername.trim().isEmpty()) {
nameAttribute = "preferred_username";
}
return new DefaultOidcUser(
authorities,
user.getIdToken(),
user.getUserInfo(),
nameAttribute
);
};
}
provider 配置中仍使用 sub,让 delegate.loadUser() 先完成加载,再选择最终 OidcUser 的 name attribute。如果 ID token 没有 preferred_username,就继续使用 sub。
再显式接入登录流程:
@Bean
public SecurityFilterChain filterChainWithOAuth2Security(
HttpSecurity http,
OAuth2UserService<OidcUserRequest, OidcUser> entraOidcUserService
) throws Exception {
configureStandardHttpSecurity(http)
.oauth2Login(oauth -> oauth
.userInfoEndpoint(userInfo -> userInfo
.oidcUserService(entraOidcUserService)
)
)
.oauth2ResourceServer(oauth -> oauth
.jwt(Customizer.withDefaults())
);
return http.build();
}
configureStandardHttpSecurity(http) 沿用题目中的现有方法。此方案已经处理角色,无需再挂载原来的 userAuthoritiesMapperForEntra()。Spring Security 支持通过 userInfoEndpoint().oidcUserService(...) 接入自定义 OIDC 用户服务。Spring Security 扩展配置
原来的角色读取方式有什么问题?
原代码只从 UserInfo 读取角色:
((OidcUserAuthority) authority).getUserInfo().getClaims()
Entra 的应用角色应检查登录 ID token 中的 roles,不能假定 UserInfo 会返回。原代码还有两个潜在的空值问题:getUserInfo() 可能为 null,而 roles 缺失时调用 roles.stream() 也会失败。
更新后的 oidcUser.getClaims() 使用组合后的 claims,因此能够读到 ID token 中的角色,这是方案能够工作的原因之一。仍建议明确角色来源,并避免直接把未经检查的对象强制转换为 List<String>。
示例将 Entra 的角色值 Admin 映射为 ROLE_Admin,可对应:
.hasRole("Admin")
或:
.hasAuthority("ROLE_Admin")
需要与实际角色值的大小写和应用的权限命名约定保持一致。
email 可以作为替代方案吗?
如果 UserInfo 确实返回 email,改用它可以解决当前的属性缺失错误。但不能据此假定所有用户都有 email,也不宜将其作为长期不变的账户标识。
preferred_username 可能是邮箱、电话号码或其他用户名,并且可以变化;name 也可能变化且不保证唯一。应用内账户关联可使用与 issuer 配套的 sub;需要跨应用关联 Entra 用户时,再根据业务范围考虑 tid 与 oid。显示名称与账户主键应分别处理。Microsoft ID token claims 说明
上面的角色映射只作用于 oauth2Login()。同一应用通过 oauth2ResourceServer().jwt() 接受 Bearer token 时,走的是另一条认证路径,不会调用这个 OIDC 用户服务。如果 API 也要使用 roles,需要另行配置 JWT authorities 转换。由于问题未提供具体依赖版本和异常堆栈,实际排查时还应核对项目版本下的调用路径。
备注:内容仅供参考。
