SpringBoot实现JWT动态密钥轮换

作者:风象南日期:2025/10/15

背景:为什么 JWT 密钥也要"轮换"

JWT(JSON Web Token) 是当代认证体系的常用方案, 无论是单体系统、微服务、还是前后端分离登录,几乎都会用到它。

但在大多数系统里,签名密钥往往是一成不变的—— 一旦生成,常年不换,代码里写死或放在配置文件中。

这其实非常危险:

  • 一旦密钥被误传或泄露,攻击者就能伪造任意用户的合法 Token
  • 无论是测试环境误配置,还是日志误打出 key,都可能导致密钥泄露,带来安全隐患

于是我们面临一个工程问题:

"如何能动态更新 JWT 签名密钥,且不让用户重新登录?"

目标:密钥可定期更新,但不影响登录状态

我们的目标是实现:

时间点动作用户状态
10月1日使用 keypair_A 生成 JWT正常
10月10日上线 keypair_B,新签发用它老 Token 仍有效
10月20日老 Token 全部过期删除 keypair_A

✅ 老 Token 正常可验签 ✅ 新 Token 自动使用新密钥 ✅ 用户无感知,不掉线

签名实现:HMAC vs RSA

JWT 支持多种签名算法,常见的有两种:

类型算法示例是否对称特点
HMAC(对称)HS256 / HS512✅ 是签发方与验证方共用同一密钥
RSA / ECDSA(非对称)RS256 / ES256❌ 否签发方用私钥签名,验证方用公钥验签

很多系统为了图省事,默认使用 HMAC(例如 HS256)。 它确实简单,但存在一个致命问题:

一旦 HMAC 密钥泄露,攻击者可以伪造任何合法 Token。

这意味着:

签发方 = 验证方 = 攻击方(如果密钥泄露)

没有信任隔离

无法安全轮换:新旧密钥都得让验证逻辑同时持有

这也是为什么更高安全等级的系统都改用 RSA / ECDSA 非对称签名

安全轮换的关键:KID(Key ID)+ 多版本密钥仓库

JWT Header 允许带一个 "kid" 字段,用来标识当前签名使用的密钥版本。 比如:

1{
2  "alg": "RS256",
3  "typ": "JWT",
4  "kid": "key-20251013-956"
5}
6

这样,验证方只需要:

  1. 读取 header.kid
  2. 去 KeyStore 找对应公钥
  3. 使用它来验签

老 Token 用老公钥,新 Token 用新公钥,完美共存。

核心实现

技术架构

后端技术栈:

  • Spring Boot 3 + Spring Scheduling
  • JJWT 0.12.3(JWT 处理库)
  • RSA 2048 非对称加密
  • 内存 ConcurrentHashMap 存储(方便快速体验DEMO)

前端技术栈:

  • HTML5 + CSS3 + JavaScript ES6
  • Tailwind CSS UI 框架
  • 前后端分离

核心组件设计

1️⃣ DynamicKeyStore - 动态密钥存储管理器
1@Service
2public class DynamicKeyStore {
3    // 线程安全的密钥存储
4    private final Map<String, KeyInfo> keyStore = new ConcurrentHashMap<>();
5    private volatile String currentKeyId;
6
7    // 生成新密钥对
8    public String generateNewKeyPair() {
9        KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
10        generator.initialize(2048, new SecureRandom());
11        KeyPair keyPair = generator.generateKeyPair();
12
13        String keyId = "key-" + LocalDate.now() + "-" + timestamp;
14        KeyInfo keyInfo = new KeyInfo(keyId, keyPair);
15
16        // 轮换逻辑:旧密钥标记为非活跃,新密钥设为当前
17        if (currentKeyId != null) {
18            keyStore.get(currentKeyId).setActive(false);
19        }
20        currentKeyId = keyId;
21        keyStore.put(keyId, keyInfo);
22
23        return keyId;
24    }
25
26    // 根据KID获取密钥(支持多版本共存)
27    public KeyInfo getKey(String keyId) {
28        return keyStore.get(keyId);
29    }
30}
31
2️⃣ JwtTokenService - JWT 服务层

Token 生成(使用当前活跃密钥):

1public String generateToken(String username, Map<String, Object> claims) {
2    // 获取当前活跃密钥
3    var currentKey = keyStore.getCurrentKey();
4    String keyId = currentKey.getKeyId();
5
6    // 构建JWT,设置KID
7    JwtBuilder builder = Jwts.builder()
8            .subject(username)
9            .issuedAt(new Date())
10            .expiration(Date.from(Instant.now().plus(24, ChronoUnit.HOURS)))
11            .header().keyId(keyId).and()
12            .signWith(currentKey.getKeyPair().getPrivate(), Jwts.SIG.RS256);
13
14    // 添加自定义声明
15    if (claims != null && !claims.isEmpty()) {
16        builder.claims().add(claims);
17    }
18
19    return builder.compact();
20}
21

Token 验证(支持多版本密钥):

1public Claims validateToken(String token) throws JwtException {
2    // 1. 解析Header获取KID
3    String[] parts = token.split("\\.");
4    String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]));
5    Map<String, Object> headerMap = mapper.readValue(headerJson, Map.class);
6    String keyId = (String) headerMap.get("kid");
7
8    if (keyId == null) {
9        throw new JwtException("Token缺少密钥ID (kid)");
10    }
11
12    // 2. 根据KID获取对应公钥
13    var keyInfo = keyStore.getKey(keyId);
14    if (keyInfo == null) {
15        throw new JwtException("找不到对应的密钥: " + keyId);
16    }
17    PublicKey publicKey = keyInfo.getKeyPair().getPublic();
18
19    // 3. 使用公钥验证Token
20    Jws<Claims> jws = Jwts.parser()
21            .verifyWith(publicKey)
22            .build()
23            .parseSignedClaims(token);
24
25    return jws.getPayload();
26}
27
3️⃣ KeyRotationScheduler - 定时轮换调度器
1@Component
2public class KeyRotationScheduler {
3
4    @Value("${jwt.rotation-period-days:7}")
5    private int rotationPeriodDays;
6
7    @Value("${jwt.grace-period-days:14}")
8    private int gracePeriodDays;
9
10    // 应用启动时初始化
11    @EventListener(ApplicationReadyEvent.class)
12    public void initialize() {
13        keyStore.initialize();
14    }
15
16    // 定时轮换:每天凌晨2点检查
17    @Scheduled(cron = "0 0 2 * * ?")
18    public void scheduledKeyRotation() {
19        var currentKey = keyStore.getCurrentKey();
20        long daysSinceCreation = ChronoUnit.DAYS.between(
21            currentKey.getCreatedAt(), LocalDateTime.now()
22        );
23
24        if (daysSinceCreation >= rotationPeriodDays) {
25            String newKeyId = keyStore.generateNewKeyPair();
26            logger.info("密钥轮换完成: {} -> {}", currentKeyId, newKeyId);
27        }
28    }
29
30    // 定时清理:每天凌晨3点清理过期密钥
31    @Scheduled(cron = "0 0 3 * * ?")
32    public void scheduledKeyCleanup() {
33        List<String> removedKeys = keyStore.cleanupExpiredKeys(gracePeriodDays);
34        if (!removedKeys.isEmpty()) {
35            logger.info("清理了 {} 个过期密钥", removedKeys.size());
36        }
37    }
38}
39

4️⃣ API接口

认证相关:

  • POST /api/auth/login - 用户登录
  • POST /api/auth/validate - Token验证
  • POST /api/auth/refresh - Token刷新
  • GET /api/auth/me - 获取当前用户信息

管理功能:

  • POST /api/auth/admin/rotate-keys - 手动轮换密钥
  • POST /api/auth/admin/cleanup-keys - 清理过期密钥

演示功能:

  • GET /api/demo/key-stats - 获取密钥统计
  • POST /api/demo/parse-token - 解析Token
  • POST /api/demo/generate-test-token - 生成测试Token
  • GET /api/demo/protected - 受保护资源

5️⃣ 前端交互界面

DEMO提供了完整的前后端分离演示界面

用户登录:登录认证和状态显示

受保护资源:演示Token保护机制

密钥信息:实时密钥存储状态监控

Token解析:JWT结构分析工具

管理功能:手动密钥轮换和清理

平滑过渡策略

密钥轮换不是"替换",而是"共存"。

阶段动作状态
① 新密钥上线新 Token 用新 Key 签发双密钥并行
② 老 Token 仍验证通过旧 Key 在验证端保留用户无感
③ 老 Token 过期删除旧 Key安全收尾

整个过程无须人工干预,也不需要让用户重新登录。

关键验证点

新Token使用新密钥:轮换后新生成的Token包含新的KID

旧Token仍可验证:轮换前的Token继续正常使用

用户无感知:整个轮换过程对用户完全透明

系统监控:实时查看密钥状态和轮换历史

总结

在实际项目中,密钥管理往往是被忽视的角落。直到安全审计时才发现问题。通过合理运用JWT的KID字段和RSA的非对称特性,我们可以让系统自动处理密钥轮换,而不是事后补救。

从代码量来看,增加密钥轮换功能并不需要大幅改动现有架构,但带来的安全收益是长期的。

github.com/yuboon/java…


SpringBoot实现JWT动态密钥轮换》 是转载文章,点击查看原文


相关推荐


从Java集合到云原生现代数据管理的演进之路
kkjt01302025/10/14

从Java集合到云原生现代数据管理的演进之路在软件开发的漫长历程中,数据管理技术的演进一直是推动应用架构变革的核心动力。从最初在单一应用内存中操作的Java集合,到今天跨越全球分布式环境的云原生数据平台,这条路不仅反映了技术的飞跃,更体现了我们对数据处理能力、规模和敏捷性不断增长的需求。这一演进是渐进而深刻的,每一步都为了解决特定时代的核心挑战。Java集合:单机时代的基石Java集合框架(Java Collections Framework, JCF)是早期Java应用数据管理的基石。它以Li


如何为Spring Cloud Gateway配置具体的负载均衡策略?
IT橘子皮2025/10/12

以下是针对 ​Spring Cloud Gateway​ 配置具体负载均衡策略(如加权轮询、最小连接数等)的详细指南,结合最新技术文档和最佳实践: 一、基础配置与依赖 1. ​引入依赖​ 确保项目中已包含 Spring Cloud Gateway 和负载均衡器(Spring Cloud LoadBalancer)的依赖: <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cl


零基础学JavaScript:手把手带你搭建环境,写出第一个程序!
良山有风来2025/10/11

开头:你是不是也遇到过这些问题? 刚学JavaScript的时候,你是不是一脸懵? 打开教程,满屏的“Node.js”、“npm”、“VS Code”,完全不知道从哪下手? 照着网上的教程配置环境,结果各种报错,心态爆炸? 写了半天代码,连个“Hello World”都显示不出来? 别担心!这篇文章就是为你准备的。 我会用最直白的方式,带你一步步搭建JavaScript开发环境,并写出你的第一个程序。 看完这篇文章,你不仅能顺利运行第一个JavaScript程序,还能理解背后的原理,为后续学习打


【机器学习】无监督学习 —— K-Means 聚类、DBSCAN 聚类
一杯水果茶!2025/10/9

K-Means 聚类标准 K-Means 算法K-Means 评估:肘部法则(Elbow Method)与轮廓系数(Silhouette Score)1. 肘部法则(Elbow Method)2. 轮廓系数(Silhouette Score) DBSCAN 聚类(Density-Based Spatial Clustering of Applications with Noise)DBSCAN 的关键参数DBSCAN 算法 K-Means 聚类 K‑Means 聚类 是一种


一文读懂 Vue 组件间通信机制(含 Vue2 / Vue3 区别)
excel2025/10/8

一、组件间通信的概念 在 Vue 中,组件(Component) 是最核心的概念之一。每个 .vue 文件都可以视为一个独立的组件。 而 通信(Communication) 是指一个组件如何将信息传递给另一个组件。 通俗地说: 组件间通信,就是不同组件之间如何共享数据、触发行为、进行信息交互的过程。 例如:当我们使用 UI 框架中的 table 组件时,需要向它传入 data 数据,这个“传值”的过程本质上就是一种组件通信。 二、组件间通信解决了什么问题? 在实际开发中,每个组件都有自己的


【Node】Node.js 多进程与多线程:Cluster 与 Worker Threads 入门
你的人类朋友2025/10/6

前言 在 Node.js 开发中,处理 CPU 密集型任务和提升应用性能是常见需求。 今天我们来深入理解 Node.js 提供的两种并发处理方案:Cluster 模块和 Worker Threads 模块。 ☺️ 这边要求阅读本文的新手小伙伴要有一个印象:【Cluster】 与【进程】相关,【Worker Threads】 与【线程】相关 小贴士 📚: ✨Cluster 的中文意思是集群 ✨Worker Threads 的中文意思是工作线程 官方定义解析 Node.js 官方文档指出:


【Linux系统】快速入门一些常用的基础指令
落羽的落羽2025/10/5

各位大佬好,我是落羽!一个坚持不断学习进步的学生。 如果您觉得我的文章还不错,欢迎多多互三分享交流,一起学习进步! 也欢迎关注我的blog主页: 落羽的落羽 文章目录 pwd、whoami、clearmkdir、touch、treecdlsrmdir、rmmanechocat、tac、more、less、head、tailcp、mvfind、which、whereisaliasgrepzip、unzip、taruname 开始学习使用Linux,我们首先要掌握一些Linux


快速搭建redis环境并使用redis客户端进行连接测试
你的人类朋友2025/10/4

前言 最近工作要用到 redis,所以这边简要记录一下自己搭建 redis 环境的过程,后面忘记了回头看比较方便。 正文 一、环境安装 这边推荐个跨 windows 和 mac 的 redis 客户端,another Redis Desktop Manager windows 一般安装Another-Redis-Desktop-Manager-win-1.7.1-x64.exe mac 一般安装Another-Redis-Desktop-Manager-mac-1.7.1-arm64.dmg 具


Java 设计模式在 Spring 框架中的实践:工厂模式与单例模式
武昌库里写JAVA2025/10/2

# Java 设计模式在 Spring 框架中的实践:工厂模式与单例模式 概述 在软件开发中,设计模式是为了解决特定问题的最佳实践经验的总结。而工厂模式和单例模式是其中两个最为常用和重要的设计模式,在 Java 开发中得到了广泛应用。在 Spring 框架中,工厂模式和单例模式也有着非常重要的应用。本文将从实际的案例出发,介绍工厂模式和单例模式在 Spring 框架中的实践。 工厂模式在 Spring 框架中的实践 工厂模式简介 工厂模式是一种创


如何用 CSS 中写出超级美丽的阴影效果
非优秀程序员2025/10/2

「这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战」。 在我看来,最好的网站和Web应用程序对它们具有切实的"真实"质量。实现这种质量涉及很多因素,但阴影是一个关键因素。 然而,当我环顾网络时,很明显,大多数阴影并不像它们所希望的那样丰富。网络上覆盖着模糊的灰色盒子,看起来并不像影子。 在本教程中,我们将学习如何将典型的箱形阴影转换为美丽、逼真的阴影: 为什么还要使用阴影? 我保证,我们很快就会谈到有趣的CSS技巧。但首先,我想退后一步,谈谈为什么阴影存在于CSS中,以

首页编辑器站点地图

Copyright © 2025 聚合阅读

License: CC BY-SA 4.0