Token与JWT机制深度解析:从结构原理到刷新方案的全链路实战
Token 与 JWT 是现代鉴权基石:讲清 JWT 结构(Header/Payload/Signature)、无状态校验与 Refresh Token 刷新方案。 掌握后能说清「为什么用 JWT 替代 Session」及过期刷新链路。
一句话概括
JWT(JSON Web Token)是一个自包含的、经过签名的 JSON 令牌,它让服务端能在不查数据库的情况下验证”你是谁”和”你能做什么”——但它的无状态特性也是一把双刃剑:Token 一旦发出,就跟”泼出去的水”一样难以收回。
核心知识点
1. JWT 的三段式结构
JWT 由三部分组成,用 . 分隔:Header.Payload.Signature
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 用 Node.js 原生 crypto 手写一个 JWT,彻底理解签名过程
import crypto from 'crypto';
const base64url = (str) =>
Buffer.from(str).toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
function sign(payload, secret, expiresIn = 3600) {
const header = { alg: 'HS256', typ: 'JWT' };
const now = Math.floor(Date.now() / 1000);
const body = { ...payload, iat: now, exp: now + expiresIn, jti: crypto.randomUUID() };
const h64 = base64url(JSON.stringify(header));
const p64 = base64url(JSON.stringify(body));
const sig = crypto.createHmac('sha256', secret).update(`${h64}.${p64}`).digest('base64url');
return `${h64}.${p64}.${sig}`;
}
function verify(token, secret) {
const [h64, p64, sig] = token.split('.');
const expected = crypto.createHmac('sha256', secret).update(`${h64}.${p64}`).digest('base64url');
if (sig !== expected) throw new Error('签名不匹配');
const payload = JSON.parse(Buffer.from(p64, 'base64url').toString());
if (payload.exp < Date.now() / 1000) throw new Error('Token 已过期');
return payload;
}
// 试试
const token = sign({ sub: 'user_42', role: 'admin' }, 'my-256-bit-secret!!');
console.log(verify(token, 'my-256-bit-secret!!'));
// { sub: 'user_42', role: 'admin', iat: ..., exp: ..., jti: '...' }
关键点:Payload 只是 Base64 编码,不是加密!任何人都能解码看到内容,但不能伪造——因为不知道密钥算不出签名。
2. 对称签名 vs 非对称签名:选哪个?
1
2
3
4
5
6
7
// HS256:同一个密钥签发和验证,适合单体应用
const token = sign(payload, SHARED_SECRET); // 签发 & 验证都用 SHARED_SECRET
// RS256:私钥签发、公钥验证,适合微服务
import { generateKeyPairSync, privateDecrypt, publicEncrypt } from 'crypto';
const { privateKey, publicKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });
// Auth 服务用 privateKey 签发 → 其他服务用 publicKey 验证,无需共享密钥
| 维度 | HS256 | RS256 |
|---|---|---|
| 密钥管理 | 所有服务共享同一个 secret | 私钥只在一处,公钥到处分发 |
| 适用场景 | 单体应用 | 微服务、第三方集成 |
| 性能 | 快 | 慢(RSA 签名耗时约 HS256 的 5-10 倍) |
| 风险 | 密钥泄露 = 全崩 | 私钥泄露才崩,公钥可以公开 |
3. Token 刷新的正确姿势:Refresh Token 旋转
前端面试高频题,但真正能讲清楚”旋转”机制的不到 20%:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 普通刷新(❌ 有风险)
// 用 refreshToken1 换 accessToken2 → refreshToken1 依然有效
// 如果 refreshToken1 被窃取,攻击者可以一直用
// 旋转刷新(✅ 关键:每次刷新发新的 refresh token,旧的同时失效)
async function rotateToken(oldRefreshToken, userId) {
// 用 Lua 脚本保证原子性:检查旧 token → 存入新 token
const script = `
local stored = redis.call('GET', KEYS[1])
if stored ~= ARGV[1] then return redis.error_reply('STOLEN') end
redis.call('SET', KEYS[1], ARGV[2], 'EX', ARGV[3])
return 'OK'
`;
const newToken = crypto.randomBytes(32).toString('hex');
await redis.eval(script, [`refresh:${userId}`], [oldRefreshToken, newToken, 7 * 86400]);
return newToken;
}
// 如果返回 STOLEN → 说明这个 refresh token 已被消费 → 可能被窃取 → 立即拉黑用户所有 session
旋转的精妙之处:正常用户和攻击者同时持有 refreshToken1 时,谁先用谁拿到新 token,另一个人的立即失效,从而自动暴露窃取行为。
4. Access Token 和 Refresh Token 的生命周期设计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const TOKEN_CONFIG = {
accessToken: '15min', // 短(泄露窗口小)
refreshToken: '7d', // 长(但支持旋转和吊销)
// 原则:access token 过期了用 refresh token 换,refresh token 过期了只能重新登录
};
// 前端 Axios 拦截器:自动无感刷新
api.interceptors.response.use(
res => res,
async err => {
if (err.response?.status === 401 && !err.config._retry) {
err.config._retry = true;
const { accessToken, refreshToken } = await refreshTokens();
// 存新的、重试原请求
err.config.headers.Authorization = `Bearer ${accessToken}`;
return api(err.config);
}
return Promise.reject(err);
}
);
5. JWT 常见安全雷区
1
2
3
4
5
6
7
8
9
10
11
12
13
// ❌ 雷区1:Payload 放敏感信息
{ "credit_card": "6222..." } // Base64 不是加密!
// ❌ 雷区2:密钥太短
const secret = 'abc123'; // HS256 要求至少 256 位(32 字节)
// ❌ 雷区3:不校验算法
jwt.verify(token, secret); // 老版本库可能接受 alg: "none"(无签名!)
// ✅ 明确指定允许的算法
jwt.verify(token, secret, { algorithms: ['HS256'] });
// ❌ 雷区4:Token 永不过期
{ exp: Math.floor(Date.now()/1000) + 365*86400 } // 一年?太危险
其实你每天都在用
- 登录态保持:打开任何网站登录后刷新页面仍然保持登录——背后就是 access token 存在 localStorage/sessionStorage,每次请求带在
Authorization头里 - OAuth 第三方登录:用微信/GitHub 登录某个网站时,微信返回给网站的就是一个 JWT,里面包含了你的 openId 和基本信息
- API 网关鉴权:公司内部微服务之间的调用,网关拿到 JWT 后直接解析角色和权限,无需每次查数据库
- 邮件验证/密码重置链接:那些”点击这里重置密码”的一次性链接,本质就是一个短时效 JWT,包含用户 ID 和操作类型
- 移动端自动登录:App 首次登录后下次打开不用重新输入密码,就是因为 Refresh Token 存在了 Keychain/Keystore 里
常见误解(FAQ)
❌ 误区:JWT 是加密的,所以可以存放敏感信息
JWT 的标准是签名(JWS),不是加密。加密版本叫 JWE(JSON Web Encryption),实际使用极少。Payload 内容是 Base64URL 编码的明文,任何人都可以解码。敏感信息应该存服务端,JWT 里只放用户 ID。
❌ 误区:JWT 完全无状态,比 Session 方案更好
“无状态”意味着 Token 发出去后无法主动吊销。如果公司裁员需要立刻收回权限,在纯 JWT 方案下只能等 Token 自然过期(或者上黑名单,这本质就是”有状态”了)。没有银弹——高安全场景把 JWT 当 Session ID 用(Redis 存映射),普通场景用纯 JWT + 短 TTL。
❌ 误区:用 localStorage 存 Token 就够了
localStorage 可以被同一域名下的任何 JS 读取,包括被注入的恶意脚本。更安全的做法是 Access Token 存内存(变量),Refresh Token 存 httpOnly + Secure + SameSite=Strict 的 Cookie。这样即使 XSS 注入也只能拿到 Access Token(很快过期),拿不到 Refresh Token。
❌ 误区:JWT 越大越方便,把所有用户信息都塞进去
每次 HTTP 请求都带着 JWT,如果 Token 有 5KB,每个请求就多 5KB 开销。一天 100 万次请求就是 5GB 额外流量。Payload 尽量精简——用户 ID + 角色足矣,详细信息走 API 查询。
一句话总结
JWT 不是加密,是签名——它让”你是谁”这个答案不需要问数据库就能被信任,但这份信任的代价是”说出去的话收不回来”。