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

Spring Boot资源服务器自定义JWT解码器:JWT转Spring Security Jwt对象时的时间戳类型错误求助

解决Spring Security JwtDecoder中时间戳类型不匹配的问题

你遇到的问题根源很明确:Nimbus JWTParser解析出来的exp、iat这类时间戳claim是Date类型,但Spring Security的Jwt.Builder要求这些时间字段必须是Instant类型。你之前的尝试只是单独设置了expiresAt,但没有移除原claims map里的Date类型exp值,所以Builder在处理整个claims集合时还是会触发类型检查异常。

下面是正确的解决步骤和完整代码:

核心修复思路

  1. 先从Nimbus解析后的claims中提取时间相关字段(exp、iat、nbf等),转换为Instant类型后通过Jwt.Builder的专属方法设置。
  2. 从原claims map中移除这些时间字段,避免和Builder设置的Instant值冲突。
  3. 将剩余的claims放入Jwt.Builder中,确保所有字段类型符合要求。

完整的自定义JWTDecoder实现

import com.nimbusds.jwt.JWT;
import com.nimbusds.jwt.JWTParser;
import com.nimbusds.jwt.JWTClaimsSet;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtException;

import java.text.ParseException;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;

public class CustomJWTDecoder implements JwtDecoder {

    // 替换为你的实际AES解密密钥
    private String aesSecretKey = "your-aes-secret-key-here";

    @Override
    public Jwt decode(String encryptedToken) throws JwtException {
        try {
            // 第一步:解密AES加密的token
            String decryptedToken = decryptAES(encryptedToken);

            // 第二步:用Nimbus解析解密后的JWT字符串
            JWT parsedJwt = JWTParser.parse(decryptedToken);
            JWTClaimsSet claimsSet = parsedJwt.getJWTClaimsSet();

            // 第三步:转换为Spring Security的Jwt对象
            return createSpringJwt(decryptedToken, parsedJwt, claimsSet);

        } catch (ParseException e) {
            throw new JwtException("解析JWT失败: " + e.getMessage(), e);
        } catch (Exception e) {
            throw new JwtException("解密或处理JWT失败: " + e.getMessage(), e);
        }
    }

    private String decryptAES(String encryptedToken) throws Exception {
        // 这里实现你的AES解密逻辑,示例伪代码:
        // Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        // SecretKeySpec keySpec = new SecretKeySpec(aesSecretKey.getBytes(), "AES");
        // cipher.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(yourIvBytes));
        // return new String(cipher.doFinal(Base64.getDecoder().decode(encryptedToken)));
        return encryptedToken; // 临时返回原token,实际开发替换为真实解密逻辑
    }

    private Jwt createSpringJwt(String tokenValue, JWT parsedJwt, JWTClaimsSet claimsSet) {
        // 提取JWT头部信息
        Map<String, Object> headers = new LinkedHashMap<>(parsedJwt.getHeader().toJSONObject());

        // 复制所有claims,后续移除时间相关字段
        Map<String, Object> claims = new LinkedHashMap<>(claimsSet.getClaims());

        // 初始化Jwt构建器
        Jwt.Builder jwtBuilder = Jwt.withTokenValue(tokenValue)
                .headers(h -> h.putAll(headers));

        // 处理过期时间exp
        Date expDate = claimsSet.getExpirationTime();
        if (Objects.nonNull(expDate)) {
            jwtBuilder.expiresAt(expDate.toInstant());
            claims.remove("exp"); // 移除原Date类型的exp字段
        }

        // 处理签发时间iat
        Date iatDate = claimsSet.getIssueTime();
        if (Objects.nonNull(iatDate)) {
            jwtBuilder.issuedAt(iatDate.toInstant());
            claims.remove("iat"); // 移除原Date类型的iat字段
        }

        // 处理生效时间nbf(如果存在)
        Date nbfDate = claimsSet.getNotBeforeTime();
        if (Objects.nonNull(nbfDate)) {
            jwtBuilder.notBefore(nbfDate.toInstant());
            claims.remove("nbf"); // 移除原Date类型的nbf字段
        }

        // 放入剩余的非时间类型claims
        jwtBuilder.claims(c -> c.putAll(claims));

        return jwtBuilder.build();
    }
}

为什么之前的代码会报错?

你之前的代码先调用了claims(c -> c.putAll(claims)),把包含Date类型exp的map放进去,之后再调用expiresAt设置Instant值。但此时claims map里的exp还是Date类型,Spring Security的Jwt.Builder在验证所有claim类型时,会检查到exp的类型不符合要求(必须是Instant),所以依然抛出异常。

通过先移除时间相关的claim,再用Builder的专属方法设置Instant值,就能避免这个类型冲突问题。

安全配置中的正确使用

建议通过@Bean方式注入自定义解码器,方便后续扩展和依赖注入:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public JwtDecoder customJwtDecoder() {
        return new CustomJWTDecoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new KeycloakRoleConverter());

        http
                .authorizeRequests()
                .antMatchers(HttpMethod.GET, "/users/status/check")
                .hasRole("developer")
                .anyRequest().authenticated()
                .and()
                .oauth2ResourceServer()
                .jwt()
                .decoder(customJwtDecoder())
                .jwtAuthenticationConverter(jwtAuthenticationConverter);
    }
}

内容的提问来源于stack exchange,提问作者Shahab.es

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.27 19:58:16