凌晨4点的安全警报:JWT 密钥泄露后如何紧急止损+无损轮换方案
引言
凌晨 4:07,值班手机震动。安全团队告警:GitHub 上一份内部仓库的 application.yml 被同事不小心 push 到了公开仓库,里面赫然躺着 JWT 签名密钥 jwt.secret=MyJwtSecret2024。更要命的是——三小时前刚推上去,GitHub 爬虫和自动化扫描脚本早已抓到了。
值班的几个问题砸过来,一个比一个要命:
- 这个密钥能干什么?伪造任意用户的 Token,包括管理员——拿到密钥的人可以签发一个
role=admin, exp=2099的 Token,以任何身份调任何接口 - 现在换密钥会怎样?所有已登录用户的 Token 立刻失效,几十万在线用户被踢下线,大面积 401 报错
- 不换行吗?密钥已经在公网暴露 3 小时,每一秒都可能有人在伪造 Token 探测系统
这不是假设。这是真实发生过多起的事件——Uber 2022 年的泄露就是因为攻击者从内部仓库拿到了 AWS 凭证和 JWT 密钥。很多团队都以为"密钥泄露"是小概率事件,直到它发生在自己身上。
这篇文章把我们走过的一遍完整流程写下来:应急止损四步(30 分钟内止血)→ 排查泄露范围 → 长期密钥轮换方案(双密钥过渡期 + 无损切换)→ 密钥管理规范。文末附一份可直接打印的应急操作手册。
一、先想清楚:JWT 密钥泄露到底意味着什么
1.1 密钥泄露的攻击面
JWT 的签名密钥(HS256 的 secret 或 RS256 的私钥)是整个认证体系的根。泄露后的攻击面:
| 攻击 | 方式 | 危害 |
|---|---|---|
| 身份伪造 | 用密钥签发任意 userId/role 的 Token | 以任何用户身份调接口,包括管理员 |
| 权限提升 | 签发 role=admin 的 Token | 获得系统最高权限 |
| 永久 Token | 签发 exp=2099-01-01 的 Token | 永不过期的后门,换了密钥才失效 |
| 横向移动 | 伪造的 Token 调内部管理接口 | 探测系统结构,寻找数据出口 |
一句话:密钥泄露 = 认证体系被完全攻破。这不是"改个密码"级别的故障,是 P0 级安全事件。
1.2 为什么"改密钥"不是一句话的事
止血的直接方案是换密钥。但换密钥有个致命副作用:
旧密钥签发的 Token → 新密钥验签失败 → 401
↓
所有在线用户被踢下线
↓
几十万用户同时重新登录 → 登录服务雪崩
直接换密钥 = 把安全事件变成可用性事件。我们的目标是:既要让伪造 Token 立刻失效,又不能让合法用户大规模掉线。这个矛盾就是"无损轮换"要解决的核心问题。
1.3 止血 vs 根治:两条时间线
整个处置分两条线并行:
| 时间线 | 目标 | 手段 |
|---|---|---|
| 止血(0~30 分钟) | 立刻让伪造 Token 失效,保住系统 | 新密钥签发 + 旧 Token 黑名单 + 强制重登 |
| 根治(1~7 天) | 排查泄露范围,建立轮换机制 | 日志审计 + 密钥管理改造 + 定期轮换 |
止血阶段不要追求完美,先止血再治病。但止血方案要和根治方案兼容——止血时用的"双密钥过渡"机制,根治时直接复用。
二、应急止损:30 分钟内四步止血
2.1 四步操作时间线
T+0min 确认泄露 → 启动应急预案 → 拉应急群
│
T+2min 生成新密钥(AKID 命名,版本号 +1)
│
T+5min 上线"双密钥验签":新密钥签发 + 新旧密钥都能验签
│ → 新登录拿新 Token,旧 Token 不掉线
│
T+10min 旧密钥签发的管理员/高权 Token 加入黑名单
│ → 伪造的管理员 Token 立刻失效
│
T+15min 发布安全公告:全量强制重新登录(灰度分批)
│
T+30min 确认止血完成:监控 401 率、异常 Token 告警归零
│
T+30min~ 进入排查阶段(第三章)
2.2 第一步:生成新密钥并上线双密钥验签
核心思路:验签时新旧密钥都认,签发时只用新密钥。这样旧 Token 不掉线,新 Token 用新密钥签,伪造的旧密钥 Token 在黑名单阶段被干掉。
# 配置中心(Nacos/Apollo)推送,不停机
jwt:
# 新密钥(仅签发用)
signing:
key: "NEW_SECRET_a8f3c2e9...(256bit 随机串)"
key-id: "v2"
# 验签密钥列表(新旧都能验,过渡期用)
verification:
keys:
- key-id: "v2"
secret: "NEW_SECRET_a8f3c2e9..."
- key-id: "v1"
secret: "OLD_SECRET_MyJwtSecret2024"
# 过渡期结束后删除此项
代码改造——验签时按 kid(Key ID)路由到对应密钥:
/**
* 多密钥验签器:支持新旧密钥过渡期并行验签
*
* JWT 头部的 kid(Key ID)标识用哪个密钥签的,
* 验签时按 kid 路由到对应密钥——这是无损轮换的核心机制
*/
public class MultiKeyJwtVerifier {
// keyId → 密钥(从配置中心热加载)
private final Map<String, SecretKey> verificationKeys = new ConcurrentHashMap<>();
// 当前签发用的 keyId
private volatile String currentKeyId;
/**
* 验签:按 Token 头部的 kid 找密钥
* 无 kid 的旧 Token 用默认密钥(v1)验
*/
public Claims verify(String token) throws JwtException {
// 解析头部取 kid,不验签
String kid = parseKeyId(token);
SecretKey key = verificationKeys.get(kid);
if (key == null) {
throw new JwtException("未知的密钥 ID: " + kid);
}
return Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(token)
.getPayload();
}
/** 签发:始终用当前密钥(新密钥) */
public String issue(String subject, Map<String, Object> claims, long ttlSeconds) {
long now = System.currentTimeMillis();
return Jwts.builder()
.header().keyId(currentKeyId).and() // 头部写入 kid
.subject(subject)
.claims(claims)
.issuedAt(new Date(now))
.expiration(new Date(now + ttlSeconds * 1000))
.signWith(verificationKeys.get(currentKeyId))
.compact();
}
/** 配置中心推送新密钥时调用:热加载不停机 */
public void reloadKeys(Map<String, String> newKeys, String newCurrentKeyId) {
Map<String, SecretKey> rebuilt = new ConcurrentHashMap<>();
newKeys.forEach((kid, secret) ->
rebuilt.put(kid, new SecretKeySpec(
sha256(secret), "HmacSHA256")));
verificationKeys.clear();
verificationKeys.putAll(rebuilt);
currentKeyId = newCurrentKeyId;
}
private String parseKeyId(String token) {
// 解析 JWT header(不验签),取 kid 字段
String header = token.split("\\.")[0];
String json = new String(Base64.getUrlDecoder().decode(header));
// 用 Jackson 取 kid,省略解析代码
return extractKid(json); // 无 kid 返回 "v1"(旧 Token 兼容)
}
}
kid(Key ID)是 JWT 规范的标准头部字段(RFC 7517),专门用于多密钥场景。旧 Token 没有 kid,默认用 v1 密钥验签——这就是"无损"的关键。
2.3 第二步:高权限 Token 黑名单
双密钥验签让旧 Token 继续工作,但被泄露的密钥签发的伪造 Token 也能通过验签——必须额外拦截。全部旧 Token 拉黑等于全量踢下线(违背无损目标),所以只拉黑高权限 Token:
/**
* Token 黑名单:泄露应急期,按维度精准拉黑
*/
public class TokenBlacklist {
private final StringRedisTemplate redis;
// 策略一:按 userId 拉黑(精准:只踢可疑/高权用户)
public void blacklistUser(String userId, long ttlSeconds) {
redis.opsForValue().set("jwt:blacklist:user:" + userId,
"1", ttlSeconds, TimeUnit.SECONDS);
}
// 策略二:按密钥版本拉黑(粗暴:旧密钥签发的全部失效,配合强制重登分批执行)
public void blacklistKeyVersion(String keyId, long ttlSeconds) {
redis.opsForValue().set("jwt:blacklist:kid:" + keyId,
"1", ttlSeconds, TimeUnit.SECONDS);
}
/** 校验:Token 是否在黑名单中 */
public boolean isBlacklisted(String userId, String keyId) {
return Boolean.TRUE.equals(redis.hasKey("jwt:blacklist:user:" + userId))
|| Boolean.TRUE.equals(redis.hasKey("jwt:blacklist:kid:" + keyId));
}
}
拦截器侧的配合:
public class JwtAuthFilter implements GatewayFilter {
private final MultiKeyJwtVerifier verifier;
private final TokenBlacklist blacklist;
@Override
public FullHttpResponse preFilter(RequestContext ctx) {
String token = extractToken(ctx);
try {
Claims claims = verifier.verify(token);
String kid = verifier.parseKeyId(token);
// 黑名单检查(应急期新增的关卡)
if (blacklist.isBlacklisted(claims.getSubject(), kid)) {
return reject(401, "Token 已被撤销,请重新登录");
}
ctx.setUserId(claims.getSubject());
return null; // 放行
} catch (JwtException e) {
return reject(401, "Token 无效");
}
}
}
止血阶段的黑名单策略:
| 策略 | 范围 | 场景 |
|---|---|---|
blacklistUser(adminUserId) | 管理员账号 | 伪造管理员 Token 立刻失效 |
blacklistKeyVersion("v1") | 所有 v1 密钥 Token | 配合强制重登,分批拉黑旧 Token |
2.4 第三步:强制重新登录(分批灰度)
不能一次性把所有 v1 Token 拉黑——几十万用户同时重新登录会打爆认证服务。分批执行:
/**
* 强制重登:按用户 ID 哈希分批拉黑旧密钥 Token
*
* 每批 5% 用户,间隔 2 分钟,20 批 ~40 分钟完成全量切换
*/
public class ForcedReloginScheduler {
private final TokenBlacklist blacklist;
private final UserRepository userRepo;
// 分批拉黑 v1 密钥 Token(配合旧 Token 的最大 TTL 设黑名单 TTL)
public void scheduleBatchedBlacklist(String oldKeyId, int batchPercent) {
List<String> allUserIds = userRepo.findAllUserIds();
int batchSize = allUserIds.size() * batchPercent / 100;
long tokenMaxTtl = 7200; // 旧 Token 最长 2 小时,黑名单 TTL 同步
for (int i = 0; i < allUserIds.size(); i += batchSize) {
int from = i;
int to = Math.min(i + batchSize, allUserIds.size());
scheduler.schedule(() -> {
List<String> batch = allUserIds.subList(from, to);
// 这批用户的 v1 Token 全部拉黑
batch.forEach(uid -> blacklist.blacklistUser(uid, tokenMaxTtl));
log.info("[强制重登] 第 {} 批完成,用户数 {}", from / batchSize + 1, batch.size());
}, (long) (i / batchSize) * 2, TimeUnit.MINUTES);
}
}
}
分批拉黑后,被拉黑的用户下次请求收到 401,前端引导重新登录,拿到 v2 新密钥签的 Token。40 分钟内全量切换完成,登录服务 QPS 平滑不雪崩。
2.5 第四步:止血完成确认
三个监控指标确认止血成功:
# 1. 旧密钥 Token 使用率持续下降(用户重登后拿新 Token)
# Grafana 面板:jwt_verification_by_kid{kid="v1"} 持续降为 0
# 2. 401 率:分批拉黑期间短暂上升,切换完成后回落
# 告警阈值:401 率 > 5% 持续 5 分钟
# 3. 异常 Token 告警归零
# 监控:伪造/过期 Token 被拦截的次数
止血完成的标志:v1 密钥 Token 调用量归零、401 率恢复正常、无异常 Token 告警。此后进入排查阶段。
三、排查泄露范围:别漏掉后门
3.1 三个维度的排查
止血完成后,必须排查"泄露的 3 小时窗口内攻击者做了什么":
| 维度 | 排查方式 | 关注信号 |
|---|---|---|
| Token 溯源 | 按 kid=v1 过滤日志,统计异常 Token | 同一 userId 的 Token 在短时间从不同 IP 出现 |
| 接口审计 | 排查窗口内高权限接口的调用日志 | 管理员接口、批量数据导出、权限变更类接口 |
| 数据出口 | 排查窗口内的数据导出/批量查询 | 大量分页查询、全量数据拉取、报表导出 |
3.2 识别伪造 Token的特征
合法 Token 和伪造 Token 在日志里有可辨识差异:
-- 排查窗口内的可疑 Token 使用(伪造 Token 特征)
SELECT user_id, ip, user_agent, request_path, count(*) as cnt
FROM api_access_log
WHERE create_time BETWEEN '泄露开始时间' AND '止血时间'
AND (
-- 特征1:同一 userId 短时间不同 IP(攻击者用自己 IP 拿伪造 Token)
user_id IN (
SELECT user_id FROM api_access_log
WHERE create_time BETWEEN '泄露开始时间' AND '止血时间'
GROUP BY user_id HAVING COUNT(DISTINCT ip) > 3
)
-- 特征2:异常长的 exp(攻击者签发永久 Token)
-- 特征3:管理员接口被非常规 IP 调用
OR (request_path LIKE '/admin/%' AND ip NOT IN (SELECT ip FROM known_admin_ips))
)
GROUP BY user_id, ip, user_agent, request_path
ORDER BY cnt DESC;
3.3 排查后的处置
| 发现 | 处置 |
|---|---|
| 伪造 Token 调过管理接口 | 审查该接口的操作是否造成数据变更,必要时回滚 |
| 批量数据被导出 | 评估数据合规影响,按法规通知受影响用户 |
| 发现留了后门(永久 Token) | 旧密钥黑名单 + 该 userId 永久拉黑 |
| 无异常调用 | 也要完成轮换,不能心存侥幸 |
四、长期方案:JWT 密钥定期轮换
4.1 双密钥过渡期轮换机制
止血时的"双密钥验签"机制可以常态化,做定期轮换:
轮换周期(如每 90 天):
Day 0 生成 v3 密钥 → 签发切 v3,验签认 v3 + v2
│ v1 密钥从验签列表删除(已过两个周期,旧 Token 必定过期)
│
Day 90 生成 v4 密钥 → 签发切 v4,验签认 v4 + v3
│ v2 密钥删除
│
Day 180 ... 滚动轮换
规则:签发只用最新密钥,验签认最近两版密钥,超过两个周期的旧密钥删除。这样任何时刻切换都有过渡期,旧 Token 自然过期不强制踢人。
/**
* 密钥轮换管理器
*
* 轮换操作:生成新密钥 → 推配置中心 → reloadKeys() 热加载
* 旧密钥保留一个轮换周期后自动清理
*/
public class KeyRotationManager {
private static final int MAX_VERIFICATION_KEYS = 2; // 最多保留 2 版密钥
private static final long ROTATION_INTERVAL_DAYS = 90;
/**
* 执行轮换(定时任务触发,或手动触发)
*/
public void rotate() {
String newKeyId = "v" + (currentVersion + 1);
String newSecret = generateSecureRandom(32); // 256bit 随机
// 1. 推送到配置中心
configService.push("jwt.verification.keys", buildNewKeyList(newKeyId, newSecret));
configService.push("jwt.signing.key-id", newKeyId);
// 2. 热加载(不停机)
verifier.reloadKeys(loadFromConfig(), newKeyId);
// 3. 旧密钥调度清理(一个周期后删除)
scheduler.schedule(() -> removeOldKey(newKeyId),
ROTATION_INTERVAL_DAYS, TimeUnit.DAYS);
log.info("[密钥轮换] 新密钥 {} 已上线,旧密钥将在 {} 天后清理",
newKeyId, ROTATION_INTERVAL_DAYS);
}
private String generateSecureRandom(int bytes) {
byte[] random = new byte[bytes];
new SecureRandom().nextBytes(random);
return Base64.getEncoder().encodeToString(random);
}
}
4.2 轮换的触发方式
| 方式 | 场景 | 实现 |
|---|---|---|
| 定期自动轮换 | 每 90 天 | 定时任务触发 rotate() |
| 紧急手动轮换 | 密钥泄露 | 人工触发,立即执行 |
| 配置中心监听 | 任何密钥变更 | Nacos/Apollo listener → reloadKeys() |
4.3 RS256 非对称方案的轮换优势
如果用 RS256(非对称签名),轮换更优雅:
| 方案 | 签发 | 验签 | 轮换特点 |
|---|---|---|---|
| HS256(对称) | 同一个 secret | 同一个 secret | 密钥要在多方共享,轮换要同步 |
| RS256(非对称) | 私钥(仅认证服务持有) | 公钥(各服务持有,可公开) | 只换私钥,公钥通过 JWKS 端点自动发现 |
RS256 配合 JWKS(JSON Web Key Set)端点,下游服务通过 /.well-known/jwks.json 自动获取最新公钥,轮换时认证服务换私钥 + 更新 JWKS 端点,下游服务自动刷新——真正零停机无损轮换:
// RS256 + JWKS 的验签器(Spring Security / Nimbus JWT 库支持)
// 下游服务从 JWKS 端点自动拉取公钥,密钥轮换后自动更新
@Bean
public JwtDecoder jwtDecoder() {
NimbusJwtDecoder decoder = NimbusJwtDecoder
.withJwkSetUri("https://auth.example.com/.well-known/jwks.json")
.cache(Duration.ofMinutes(5)) // 公钥缓存 5 分钟,轮换后最多 5 分钟自动刷新
.build();
return decoder;
}
生产建议:开放 API 和多服务场景用 RS256 + JWKS,内部单体应用 HS256 + 双密钥过渡也够用。
五、密钥管理:绝不放代码仓库
5.1 密钥存储方案对比
| 方案 | 安全性 | 适用 | 问题 |
|---|---|---|---|
| 代码仓库硬编码 | ❌ 最差 | 仅 demo | Git 历史、泄露风险 |
| 环境变量 | ⚠️ 一般 | 单机部署 | 进程可见、日志可能泄漏 |
| 配置中心(Nacos/Apollo) | ⚠️ 中等 | 微服务 | 有审计但密钥可读 |
| Vault / KMS | ✅ 最优 | 生产标准 | 加密存储 + 审计 + 动态密钥 |
5.2 Vault 方案示意
/**
* 从 HashiCorp Vault 获取 JWT 密钥
*
* 密钥存 Vault KV 引擎,应用启动时拉取,定期刷新
* 代码仓库零密钥,运维也只有 Vault 权限才能看到明文
*/
public class VaultKeyProvider implements KeyProvider {
private final VaultTemplate vault;
private final String secretPath = "secret/data/jwt/signing-key";
@Override
public String loadCurrentKey() {
// Vault KV v2 读取
Map<String, Object> response = vault.ops()
.read(secretPath)
.getData();
return (String) response.get("current-key");
}
@Override
public void rotateKey(String newKey) {
// 只有有权限的服务账号才能写
Map<String, Object> data = Map.of(
"current-key", newKey,
"previous-key", loadCurrentKey(),
"rotated-at", Instant.now().toString());
vault.ops().write(secretPath, data);
}
}
5.3 密钥管理三条红线
- 代码仓库零密钥。CI/CD 加密钥扫描(git-secrets / TruffleHog),任何
secret=、password=、key=提交直接拦 - 最小权限。读取密钥的权限只给认证服务,其他服务不需要密钥(RS256 下游只用公钥)
- 审计日志。Vault/KMS 的每次密钥读取和轮换都要有审计记录,泄漏后可追溯
六、常见问题
6.1 换密钥后所有用户都要重新登录吗?
如果直接换密钥(旧密钥立刻废弃),是的——所有旧 Token 失效。这就是为什么要用"双密钥过渡期":验签时新旧密钥都认,旧 Token 自然过期(比如 2 小时 TTL),新登录拿新密钥 Token,过渡期 = 旧 Token 的最大 TTL,到期后旧密钥安全删除。
6.2 黑名单方案不是违背了 JWT 无状态吗?
是的。纯 JWT 的设计理念是无状态(不查 DB/Redis 验签),黑名单引入了状态。但安全现实要求"密钥泄露后能撤销已签发的 Token"——无状态和可撤销是一对矛盾,生产系统必须在两者间取舍。我们的做法:日常不用黑名单(保持无状态优势),应急时启用(短期引入状态止血),过渡期结束后关闭黑名单恢复无状态。
6.3 为什么不用 Token 吊销列表(CRL)而是黑名单?
CRL(Certificate Revocation List)是 PKI 体系的完整吊销列表,每次验签都查全量列表。对于 JWT 场景太重了。Redis 黑名单是轻量版:只存需要撤销的 Token 标识(userId 或 kid 维度),TTL 到期自动清理。应急时用完即弃。
6.4 HS256 换 RS256 需要改什么?
三件事:① 认证服务生成 RSA 密钥对,私钥签发、公钥验签;② 暴露 JWKS 端点发布公钥;③ 下游服务改用 JWKS URI 自动发现公钥。对业务接口透明——Token 格式不变,只是签名算法从 HS256 变 RS256,alg 头部自动标识。建议在新项目直接用 RS256,省去后续迁移。
6.5 密钥轮换周期定多久合适?
取决于安全要求和业务影响。90 天是业界通用值(SOC2 合规建议)。高安全场景(金融、支付)30 天。关键不是周期长短,而是轮换是否自动化——手动轮换容易忘、容易拖,自动化定时任务才能保证执行。
6.6 泄露在 GitHub 上了怎么办?
三步:① 立刻轮换密钥(比删仓库更重要,爬虫已经抓到了);② 联系 GitHub 提交 DMCA 删除(GitHub 有密钥泄漏检测,会主动通知);③ 用 git-secrets 清理 Git 历史(git filter-branch 或 BFG Repo-Cleaner)。但历史已被爬虫抓走的无法回收,所以轮换是唯一有效止损手段。
七、总结
应急四步速查卡
┌──────────┬────────────────────────────────────────────┐
│ 第 1 步 │ 生成新密钥 → 上线双密钥验签(kid 路由) │
│ T+5min │ 新 Token 用新密钥签,旧 Token 不掉线 │
├──────────┼────────────────────────────────────────────┤
│ 第 2 步 │ 高权限 Token 黑名单(按 userId / kid 拉黑) │
│ T+10min │ 伪造的管理员 Token 立刻失效 │
├──────────┼────────────────────────────────────────────┤
│ 第 3 步 │ 强制重新登录(分批灰度,每批 5%,~40 分钟) │
│ T+15min │ 旧密钥 Token 逐步淘汰,登录服务不雪崩 │
├──────────┼────────────────────────────────────────────┤
│ 第 4 步 │ 止血确认:v1 Token 归零 + 401 恢复 + 告警清 │
│ T+30min │ 进入排查阶段 │
└──────────┴────────────────────────────────────────────┘
长期方案速查卡
┌──────────────┬─────────────────────────────────────────┐
│ 定期轮换 │ 每 90 天自动轮换,双密钥过渡期 │
│ │ 签发用最新,验签认最近两版,超过两版删除 │
├──────────────┼─────────────────────────────────────────┤
│ RS256 + JWKS │ 非对称签名 + 公钥自动发现 │
│ │ 轮换只换私钥,下游自动刷新公钥,真正无损 │
├──────────────┼─────────────────────────────────────────┤
│ 密钥管理 │ Vault/KMS 加密存储,代码仓库零密钥 │
│ │ CI/CD 密钥扫描 + 最小权限 + 审计日志 │
└──────────────┴─────────────────────────────────────────┘
关键数据
- 止血窗口:30 分钟内完成四步
- 分批重登:每批 5% 用户,间隔 2 分钟,~40 分钟全量切换
- 过渡期长度 = 旧 Token 最大 TTL(通常 2 小时)
- 轮换周期:90 天(通用)/ 30 天(高安全)
- JWKS 公钥缓存刷新:5 分钟
一句话
密钥泄露的止损不是"换密钥"三个字,而是"双密钥过渡 + 黑名单精准拉黑 + 分批重登"的组合拳。长期看,RS256+JWKS 的自动发现机制让轮换真正无损,Vault 管理让密钥永不进代码仓库——这些都是出事之前就该做的事,而不是凌晨 4 点临时抱的佛脚。
给团队的建议
| 项目状态 | 建议 |
|---|---|
| 新项目 | 直接 RS256 + JWKS + Vault,从第一天就为轮换设计 |
| 存量 HS256 | 先加 kid 多密钥机制,下次发版支持双密钥过渡 |
| 密钥在代码仓库 | 今天就迁出去,上 Vault/配置中心 + CI 扫描 |
| 无轮换机制 | 加定时轮换任务,90 天一次自动化执行 |
| 刚经历泄露 | 按本文四步止血,然后排查范围 + 改造密钥管理 |
互动话题:你们团队的 JWT 密钥存在哪?有没有轮换机制?经历过密钥泄露的"凌晨四点"吗?评论区聊聊——如果你正在经历,按本文四步走,30 分钟止血,天亮再收拾。
参考资料
- RFC 7517:JSON Web Key (JWK)
- RFC 7519:JWT 规范
- RFC 8725:JWT 安全最佳实践
- JWKS 端点规范(OIDC Discovery)
- Uber 2022 安全事件分析
- HashiCorp Vault 密钥管理文档
- git-secrets:防止密钥提交到 Git
- Spring Security JWT + JWKS 集成
标题:凌晨4点的安全警报:JWT 密钥泄露后如何紧急止损+无损轮换方案
作者:jiangyi
地址:http://jiangyi.space/articles/2026/09/01/1787989100388.html
公众号:服务端技术精选
- 引言
- 一、先想清楚:JWT 密钥泄露到底意味着什么
- 1.1 密钥泄露的攻击面
- 1.2 为什么"改密钥"不是一句话的事
- 1.3 止血 vs 根治:两条时间线
- 二、应急止损:30 分钟内四步止血
- 2.1 四步操作时间线
- 2.2 第一步:生成新密钥并上线双密钥验签
- 2.3 第二步:高权限 Token 黑名单
- 2.4 第三步:强制重新登录(分批灰度)
- 2.5 第四步:止血完成确认
- 三、排查泄露范围:别漏掉后门
- 3.1 三个维度的排查
- 3.2 识别伪造 Token的特征
- 3.3 排查后的处置
- 四、长期方案:JWT 密钥定期轮换
- 4.1 双密钥过渡期轮换机制
- 4.2 轮换的触发方式
- 4.3 RS256 非对称方案的轮换优势
- 五、密钥管理:绝不放代码仓库
- 5.1 密钥存储方案对比
- 5.2 Vault 方案示意
- 5.3 密钥管理三条红线
- 六、常见问题
- 6.1 换密钥后所有用户都要重新登录吗?
- 6.2 黑名单方案不是违背了 JWT 无状态吗?
- 6.3 为什么不用 Token 吊销列表(CRL)而是黑名单?
- 6.4 HS256 换 RS256 需要改什么?
- 6.5 密钥轮换周期定多久合适?
- 6.6 泄露在 GitHub 上了怎么办?
- 七、总结
- 应急四步速查卡
- 长期方案速查卡
- 关键数据
- 一句话
- 给团队的建议
- 参考资料
评论