JwtGenerator 报错 authenticationTime cannot be null,如何解决?
结论
这个异常表示,JwtGenerator 在生成 ID Token 的 auth_time 声明时,无法从用户认证结果中取得认证时间。
如果实际使用的是 Spring Security 7.0.5,且应用同时使用授权服务器和 oauth2Login() 接入外部 OIDC 登录,应先检查登录结果中是否缺少 FactorGrantedAuthority。该版本的 OidcAuthorizationCodeAuthenticationProvider 不会自动添加这个认证因子,上游已记录这一问题。相关问题 #19140
修复需要从认证信息的生成和传递入手。仅凭这段堆栈,还无法确定具体原因,自定义认证、权限映射和旧会话也需要检查。
为什么会在兑换授权码时失败
在 Spring Security 7.0.5 中,以下条件同时满足时,JwtGenerator 会调用 getAuthenticationTime():
- 当前生成的是
id_token。 - 当前授权类型为
authorization_code。 - Token 上下文中存在
SessionInformation。
该方法会遍历用户 Authentication.getAuthorities() 中的 FactorGrantedAuthority,读取其 issuedAt;如果有多个认证因子,就取最新时间。如果一个认证因子也找不到,就会触发 authenticationTime cannot be null。JwtGenerator 源码
所以,用户可能已经成功登录,直到客户端用授权码请求 Token 端点、服务器准备生成 ID Token 时才遇到异常。排查时,需要沿着传给 JwtGenerator 的认证信息,回查此前的用户认证过程。
如何定位认证因子缺失的环节
1. 确认实际加载的依赖版本
先检查实际使用的 Spring Security 依赖,不要只根据 Spring Boot 版本判断。
Maven:
./mvnw dependency:tree "-Dincludes=org.springframework.security:*"
Gradle:
./gradlew dependencyInsight \
--dependency spring-security \
--configuration runtimeClasspath
重点查看 spring-security-core、spring-security-oauth2-client 和 spring-security-oauth2-authorization-server,确认是否手动覆盖过版本,以及各 Spring Security 模块的版本是否一致。
2. 检查用于签发 Token 的用户认证结果
在 JwtGenerator.getAuthenticationTime() 设置断点,查看传入的 Authentication:
authentication.getClass()
authentication.getAuthorities()
确认权限集合中是否包含 FactorGrantedAuthority 类型的对象,并检查其 getIssuedAt() 是否符合预期。
这里要检查传给 JwtGenerator 的用户认证结果。Token 端点当前的 SecurityContext 中可能是客户端认证信息,只查看它可能会查错对象。
使用外部 OIDC 登录时,还应检查 OidcAuthorizationCodeAuthenticationProvider 的返回结果。在 Spring Security 7.0.5 中,这条路径使用映射后的权限创建认证结果,但不会自行补充认证因子。OIDC Provider 源码
如果登录成功时已有认证因子,生成 Token 时却没有,继续检查:
- 自定义代码是否重新创建了
Authentication,只复制了业务角色。 - 权限转换是否把对象统一转换成了
SimpleGrantedAuthority。 - 会话或授权数据的序列化、反序列化是否保留了认证因子的类型和时间。
需要逐步确认认证因子在哪个环节丢失,不能仅凭堆栈认定某一项就是根因。
外部 OIDC 登录的临时修复示例
确认是 OIDC 登录缺少认证因子后,可以通过 GrantedAuthoritiesMapper 在登录过程中补充 FACTOR_AUTHORIZATION_CODE。上游问题中给出了这一临时处理方向,Spring Security 也提供了 userAuthoritiesMapper() 配置入口。相关问题、权限映射配置文档
将下面的方法加入现有安全配置类:
import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.FactorGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
private GrantedAuthoritiesMapper oidcFactorAuthoritiesMapper() {
return authorities -> {
Set<GrantedAuthority> mapped = new LinkedHashSet<>(authorities);
boolean oidcLogin = mapped.stream()
.anyMatch(OidcUserAuthority.class::isInstance);
boolean hasAuthorizationCodeFactor = mapped.stream()
.filter(FactorGrantedAuthority.class::isInstance)
.anyMatch(authority ->
FactorGrantedAuthority.AUTHORIZATION_CODE_AUTHORITY
.equals(authority.getAuthority()));
if (oidcLogin && !hasAuthorizationCodeFactor) {
mapped.add(FactorGrantedAuthority.fromAuthority(
FactorGrantedAuthority.AUTHORIZATION_CODE_AUTHORITY));
}
return mapped;
};
}
在负责用户登录的现有 SecurityFilterChain 中接入:
http.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.userAuthoritiesMapper(oidcFactorAuthoritiesMapper())
)
);
如果已有业务权限映射,应将补充认证因子的逻辑合并进去,并保留原有的角色转换规则。上例通过 OidcUserAuthority 识别 OIDC 登录;如果已有映射会移除该对象,就需要相应调整判断位置。
FactorGrantedAuthority.fromAuthority() 默认将创建时刻作为 issuedAt,因此这段逻辑应在成功登录过程中执行。如果业务需要保留上游身份提供方的原始认证时间,应使用受信任的认证时间建模,不能在兑换 Token 时临时填入当前时间。FactorGrantedAuthority 源码
自定义认证流程如何修复
没有使用外部 OIDC 登录时,通常需要检查自己的 AuthenticationProvider 或认证结果转换代码,上面的 mapper 一般不适用:
- 凭据校验成功后,记录与实际认证方式对应的
FactorGrantedAuthority。 - 重新创建认证对象时,保留已有认证因子及其
issuedAt。 - 从持久化数据恢复认证结果时,保留对象类型,避免只恢复权限字符串。
例如,下面的对象只有权限名称,无法提供认证时间:
new SimpleGrantedAuthority("FACTOR_AUTHORIZATION_CODE")
JwtGenerator 会检查对象类型并读取 issuedAt,因此这个对象无法替代 FactorGrantedAuthority。
修复后需要验证什么
修改后,让受影响用户重新登录,再获取新的授权码并完成兑换。旧会话或已保存的授权数据中,可能仍有缺少认证因子的对象。
验证时检查:
- 新登录产生的用户
Authentication包含认证因子。 - 授权码兑换成功,返回预期的
id_token。 - ID Token 中的
auth_time与登录认证时间一致,而非每次签发 Token 的时间。
仅在 OAuth2TokenCustomizer<JwtEncodingContext> 中补写 auth_time 无法解决这个异常。在上述实现中,读取认证时间和非空断言都发生在 customizer 执行之前,程序会先抛出异常。JwtGenerator 执行顺序
后续升级时,应先核实目标 Spring Security 版本是否已包含对应修复,再通过兼容的 Spring Boot 依赖管理统一升级。关联 PR 或升级建议本身不能证明某个发布版本已经修复,可以跟踪修复 PR #19141,并核对目标版本源码。
备注:内容仅供参考。
