Token与JWT机制深度解析:从结构组成到刷新方案的全栈实践
一句话概括
JWT(JSON Web Token)是一种基于JSON的自包含令牌格式,能够在分布式系统中无状态地传递用户身份信息,但它的”无状态”优势也带来了Token吊销和刷新管理的根本性挑战。
背景与意义
2025年12月,一个拥有2000万用户的社交平台发生严重安全事故。攻击者通过窃取JWT签名密钥,伪造了管理员Token,在数小时内获取了超过50万用户的个人信息。事后调查发现,该平台使用了一个硬编码的弱密钥”secret123“来签名JWT,而且Token的有效期设置为30天——更令人震惊的是,由于系统没有实现Token吊销机制,即使发现攻击后也无法立即让已发放的Token失效。
这个案例揭示了JWT在实践中的两个核心矛盾:
- 安全性 vs 便利性:短TTL需要频繁刷新,影响用户体验;长TTL增加了泄露风险
- 无状态 vs 可控性:无状态Token无法被服务端主动吊销,这是无状态认证最大的代价
JWT最初在2015年随OAuth 2.0的广泛采用而快速普及。它解决了传统session在分布式系统中的扩展性问题——不需要共享session存储、不需要查询数据库来验证用户身份。但今天,随着微服务架构和移动端开发的成熟,JWT已经从”银弹”的神坛上走下来,开发者们更理性地看待它:JWT是一个工具,不是一个解决方案。
概念与定义
JWT是什么
JWT(RFC 7519)是一种紧凑的、URL安全的令牌格式,由三部分组成,用.分隔:
1
2
3
4
5
header.payload.signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Token vs Session
| 对比维度 | Session(服务端会话) | Token(客户端令牌) | JWT |
|---|---|---|---|
| 存储位置 | 服务端内存/Redis | 客户端 | 客户端 |
| 验证方式 | 查询session存储 | 查询数据库/自验证 | 签名验证 |
| 扩展性 | 差(需共享session) | 中(需查询数据库) | 好(自包含) |
| 可吊销 | ✅ 可以 | ✅ 可以 | ❌ 需要黑名单 |
| 负载大小 | 小(只有session ID) | 大(包含用户信息) | 大(Base64编码后更大) |
认证 vs 鉴权 vs 授权
在理解JWT之前,必须分清这三个概念:
- 认证(Authentication):验证”你是谁”——登录验证用户名密码
- 鉴权(Authorization):判断”你能做什么”——验证Token是否有效
- 授权(Permission):决定”你被允许做什么”——RBAC权限检查
1
2
认证 → 鉴权 → 授权
登录 Token验证 权限检查
最小示例:完整的JWT签发与验证流程
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
// jwt-basics.js - 完整的JWT端到端示例(使用原生Node.js crypto)
const crypto = require('crypto');
const fs = require('fs');
class SimpleJWT {
constructor(secret) {
this.secret = secret;
}
// Base64 URL-safe 编码
base64UrlEncode(data) {
return Buffer.from(data)
.toString('base64')
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
}
// Base64 URL-safe 解码
base64UrlDecode(str) {
str = str.replace(/-/g, '+').replace(/_/g, '/');
while (str.length % 4) str += '=';
return Buffer.from(str, 'base64').toString('utf-8');
}
// HMAC-SHA256 签名
sign(header, payload) {
const data = `${header}.${payload}`;
const hmac = crypto.createHmac('sha256', this.secret);
hmac.update(data, 'utf-8');
return this.base64UrlEncode(hmac.digest());
}
// 签发Token
signToken(payload, options = {}) {
const header = {
alg: 'HS256',
typ: 'JWT'
};
const now = Math.floor(Date.now() / 1000);
const tokenPayload = {
...payload,
iat: now, // 签发时间
exp: now + (options.expiresIn || 3600), // 过期时间(默认1小时)
jti: crypto.randomUUID() // Token唯一ID(防重放)
};
const encodedHeader = this.base64UrlEncode(JSON.stringify(header));
const encodedPayload = this.base64UrlEncode(JSON.stringify(tokenPayload));
const signature = this.sign(encodedHeader, encodedPayload);
return `${encodedHeader}.${encodedPayload}.${signature}`;
}
// 验证Token
verifyToken(token) {
try {
const parts = token.split('.');
if (parts.length !== 3) {
return { valid: false, error: 'INVALID_FORMAT' };
}
const [encodedHeader, encodedPayload, signature] = parts;
// 验证签名
const expectedSignature = this.sign(encodedHeader, encodedPayload);
if (signature !== expectedSignature) {
return { valid: false, error: 'SIGNATURE_MISMATCH' };
}
// 解码payload
const payload = JSON.parse(this.base64UrlDecode(encodedPayload));
// 检查过期
const now = Math.floor(Date.now() / 1000);
if (payload.exp && now >= payload.exp) {
return { valid: false, error: 'TOKEN_EXPIRED' };
}
return { valid: true, payload };
} catch (err) {
return { valid: false, error: 'VERIFICATION_FAILED', details: err.message };
}
}
}
// === 使用示例 ===
const jwt = new SimpleJWT('my-secret-key-2026-at-least-32-chars!!');
// 签发Token
const token = jwt.signToken(
{
sub: 'user_12345',
name: '张三',
role: 'admin',
permissions: ['read:users', 'write:users', 'delete:users']
},
{ expiresIn: 7200 } // 2小时
);
console.log('签发的Token:');
console.log(token);
console.log();
console.log('各部分解析:');
const parts = token.split('.');
console.log('Header:', JSON.parse(jwt.base64UrlDecode(parts[0])));
console.log('Payload:', JSON.parse(jwt.base64UrlDecode(parts[1])));
console.log('Signature:', parts[2]);
// 验证Token
console.log('\n=== 验证Token ===');
const result = jwt.verifyToken(token);
console.log('验证结果:', result.valid ? '✅ 通过' : '❌ 失败');
console.log('用户信息:', result.payload);
// 验证篡改
console.log('\n=== 模拟篡改Token ===');
const tamperedToken = token.replace('user_12345', 'user_99999');
const tamperResult = jwt.verifyToken(tamperedToken);
console.log('篡改验证结果:', tamperResult.valid ? '通过' : '失败');
console.log('错误:', tamperResult.error);
// 验证过期Token
console.log('\n=== 模拟过期Token ===');
const expiredToken = jwt.signToken(
{ sub: 'user_12345' },
{ expiresIn: -1 } // 立即过期
);
const expiredResult = jwt.verifyToken(expiredToken);
console.log('过期验证结果:', expiredResult.valid ? '通过' : '失败');
console.log('错误:', expiredResult.error);
核心知识点拆解
1. JWT三部分结构详解
Header(头部)
1
2
3
4
5
{
"alg": "HS256",
"typ": "JWT",
"kid": "key-id-abc123"
}
alg(必选):签名算法- 对称签名:HS256, HS384, HS512
- 非对称签名:RS256, RS384, RS512, ES256, ES384, ES512, EdDSA
typ(可选):令牌类型,通常为”JWT”kid(可选):密钥ID,用于多密钥轮换场景
Payload(负载)
1
2
3
4
5
6
7
8
9
10
11
{
"sub": "user_12345",
"name": "张三",
"role": "admin",
"iat": 1720080000,
"exp": 1720083600,
"jti": "550e8400-e29b-41d4-a716-446655440000",
"aud": "https://api.myapp.com",
"iss": "https://auth.myapp.com",
"nbf": 1720080000
}
标准声明(Registered Claims):
| 声明 | 全称 | 含义 | 是否推荐 |
|---|---|---|---|
iss | Issuer | 签发者 | 是 |
sub | Subject | 主题(用户ID) | 是 |
aud | Audience | 接收方 | 是 |
exp | Expiration | 过期时间(时间戳) | 是 |
nbf | Not Before | 生效时间 | 否 |
iat | Issued At | 签发时间 | 是的 |
jti | JWT ID | 唯一标识 | 是 |
自定义声明(Custom Claims)最佳实践:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ✅ 好的做法
{
"sub": "user_12345",
"roles": ["admin", "editor"],
"permissions": ["read:users", "write:users"],
"org_id": "org_67890",
"session_id": "sess_abc123"
}
// ❌ 坏的做法——负载过大
{
"sub": "user_12345",
"name": "张三丰",
"email": "zhangsan@example.com",
"phone": "13800138000",
"avatar": "https://cdn.example.com/avatar/large/user_12345.webp",
"address": "北京市朝阳区建国路88号",
"roles": ["admin", "editor", "viewer", "moderator"],
"permissions": ["read:*", "write:*", "delete:*", "admin:*", "audit:*"],
"preferences": { "theme": "dark", "fontSize": 14 }
// 这些信息不应该放在JWT中!
}
为什么负载不宜过大?
- JWT会随每个请求发送(在Authorization Header中)
- 每次请求增加不必要的传输开销
- Base64编码后体积增加约33%
- 最佳实践:JWT负载保持在1KB以内
Signature(签名)
签名是JWT安全的基石。以下是签名生成的精确过程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 签名算法伪代码
function createSignature(header, payload, secret, algorithm) {
// 1. 创建签名输入:header.payload(Base64URL编码)
const signingInput = base64UrlEncode(JSON.stringify(header))
+ '.'
+ base64UrlEncode(JSON.stringify(payload));
// 2. 使用指定的算法和密钥生成签名
switch(algorithm) {
case 'HS256':
return HMAC_SHA256(signingInput, secret); // 对称
case 'RS256':
return RSA_SHA256(signingInput, privateKey); // 非对称
case 'ES256':
return ECDSA_SHA256(signingInput, privateKey); // 非对称
}
// 3. 对签名进行Base64URL编码
return base64UrlEncode(signature);
}
2. 对称签名 vs 非对称签名
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// jwt-key-types.js - 对称与非对称签名对比
const crypto = require('crypto');
class JWTWithRSA {
constructor() {
// 生成RSA密钥对(生产环境应持久化存储私钥)
this.keyPair = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});
}
signWithRS256(payload) {
const header = { alg: 'RS256', typ: 'JWT' };
return this.sign(header, payload, this.keyPair.privateKey, 'rsa');
}
verifyWithRS256(token) {
return this.verify(token, this.keyPair.publicKey, 'rsa');
}
sign(header, payload, key, type) {
const encodedHeader = this.base64UrlEncode(JSON.stringify(header));
const encodedPayload = this.base64UrlEncode(JSON.stringify(payload));
const signingInput = `${encodedHeader}.${encodedPayload}`;
const sign = crypto.createSign('RSA-SHA256');
sign.update(signingInput);
const signature = sign.sign(key, 'base64');
return `${encodedHeader}.${encodedPayload}.${this.base64UrlEncode(signature)}`;
}
verify(token, key) {
const [header, payload, signature] = token.split('.');
const signingInput = `${header}.${payload}`;
const verify = crypto.createVerify('RSA-SHA256');
verify.update(signingInput);
return verify.verify(key, this.base64UrlDecode(signature), 'base64');
}
base64UrlEncode(data) {
return Buffer.from(data).toString('base64')
.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
}
base64UrlDecode(str) {
str = str.replace(/-/g, '+').replace(/_/g, '/');
while (str.length % 4) str += '=';
return Buffer.from(str, 'base64');
}
}
选择指南:
| 维度 | HS256(对称) | RS256/ES256(非对称) |
|---|---|---|
| 签名密钥 | 单密钥(签发和验证相同) | 私钥签发,公钥验证 |
| 密钥管理 | 简单 | 复杂(需管理私钥安全) |
| 分发范围 | 限信任方 | 公钥可公开分发 |
| 微服务适用性 | ❌ 所有服务需共享密钥 | ✅ 验证服务只需公钥 |
| 性能 | 快 | 慢(特别是RSA) |
| 推荐场景 | 单体应用 | 微服务/第三方集成 |
3. Token刷新方案设计
Token刷新是JWT实践中最有争议的话题。以下是三种主流的刷新方案:
方案A:传统Refresh Token(最常用)
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
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ 客户端 │ │ Auth服务 │ │ API服务 │
└────┬─────┘ └──────┬───────┘ └────┬─────┘
│ 1. 登录 (username/password) │
│────────────────────>│ │
│ 2. 返回 AccessToken(15min) + RefreshToken(7d) │
│<────────────────────│ │
│ │ │
│ 3. 请求API (AccessToken) │
│──────────────────────────────────────────────────>
│ 4. Token有效 → 返回资源 │
│<──────────────────────────────────────────────────
│ │ │
│ 5. AccessToken过期 │
│──────────────────────────────────────────────────>
│ 6. 401 Unauthorized (TokenExpired) │
│<──────────────────────────────────────────────────
│ │ │
│ 7. 请求刷新 (RefreshToken) │
│────────────────────>│ │
│ 8. 验证RefreshToken → 新AccessToken │
│<────────────────────│ │
│ │ │
│ 9. 用新Token重试请求 │
│──────────────────────────────────────────────────>
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
// jwt-refresh-system.js - 完整Token刷新系统
const crypto = require('crypto');
const express = require('express');
// ===== 配置 =====
const CONFIG = {
accessTokenExpiresIn: 15 * 60, // 15分钟
refreshTokenExpiresIn: 7 * 24 * 3600, // 7天
secret: process.env.JWT_SECRET || 'your-256-bit-secret-minimum',
refreshTokenSecret: process.env.REFRESH_SECRET || 'different-secret-for-refresh'
};
// ===== JWT 实现 =====
class JWTService {
constructor(secret, refreshSecret) {
this.secret = secret;
this.refreshSecret = refreshSecret;
// 存储已吊销的Refresh Token(生产环境用Redis)
this.revokedRefreshTokens = new Set();
}
base64url(str) {
return Buffer.from(str).toString('base64')
.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
}
sign(payload, secret, expiresIn) {
const header = { alg: 'HS256', typ: 'JWT' };
const now = Math.floor(Date.now() / 1000);
const fullPayload = {
...payload,
iat: now,
exp: now + expiresIn,
jti: crypto.randomUUID()
};
const b64Header = this.base64url(JSON.stringify(header));
const b64Payload = this.base64url(JSON.stringify(fullPayload));
const signingInput = `${b64Header}.${b64Payload}`;
const sig = crypto.createHmac('sha256', secret)
.update(signingInput).digest('base64');
const b64Sig = this.base64url(sig);
return `${b64Header}.${b64Payload}.${b64Sig}`;
}
verify(token, secret) {
try {
const parts = token.split('.');
if (parts.length !== 3) throw new Error('Invalid format');
const signingInput = `${parts[0]}.${parts[1]}`;
const expectedSig = this.base64url(
crypto.createHmac('sha256', secret)
.update(signingInput).digest('base64')
);
if (parts[2] !== expectedSig) throw new Error('Signature mismatch');
const payload = JSON.parse(
Buffer.from(parts[1], 'base64url').toString()
);
if (payload.exp < Math.floor(Date.now() / 1000)) {
throw new Error('Token expired');
}
return { valid: true, payload };
} catch (e) {
return { valid: false, error: e.message };
}
}
// 签发Access Token
issueAccessToken(user) {
return this.sign(
{ sub: user.id, role: user.role },
this.secret,
CONFIG.accessTokenExpiresIn
);
}
// 签发Refresh Token
issueRefreshToken(user) {
const refreshPayload = {
sub: user.id,
type: 'refresh',
family: crypto.randomUUID() // Token家族标识
};
return {
token: this.sign(
refreshPayload,
this.refreshSecret,
CONFIG.refreshTokenExpiresIn
),
family: refreshPayload.family
};
}
// 验证并刷新
refreshAccessToken(refreshToken) {
// 检查是否已被吊销
if (this.revokedRefreshTokens.has(refreshToken)) {
return { success: false, error: 'TOKEN_REVOKED' };
}
const result = this.verify(refreshToken, this.refreshSecret);
if (!result.valid) {
return { success: false, error: result.error };
}
const { sub } = result.payload;
const user = this.getUserById(sub); // 从数据库获取
if (!user) {
return { success: false, error: 'USER_NOT_FOUND' };
}
return {
success: true,
accessToken: this.issueAccessToken(user),
refreshToken: this.issueRefreshToken(user).token
};
}
// 吊销Refresh Token
revokeRefreshToken(refreshToken) {
this.revokedRefreshTokens.add(refreshToken);
}
getUserById(id) {
// 模拟数据库查询
return { id, role: 'user', name: 'Test User' };
}
}
// ===== Express API =====
const app = express();
app.use(express.json());
const jwtService = new JWTService(CONFIG.secret, CONFIG.refreshTokenSecret);
// 登录
app.post('/api/auth/login', (req, res) => {
const { username, password } = req.body;
// 验证用户名密码...
const user = { id: 'user_12345', role: 'user' };
const accessToken = jwtService.issueAccessToken(user);
const { token: refreshToken } = jwtService.issueRefreshToken(user);
res.json({
accessToken,
refreshToken,
expiresIn: CONFIG.accessTokenExpiresIn
});
});
// 刷新Token
app.post('/api/auth/refresh', (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(400).json({ error: 'Refresh token required' });
}
const result = jwtService.refreshAccessToken(refreshToken);
if (!result.success) {
return res.status(401).json({ error: result.error });
}
res.json({
accessToken: result.accessToken,
refreshToken: result.refreshToken
});
});
// 注销(吊销Refresh Token)
app.post('/api/auth/logout', (req, res) => {
const { refreshToken } = req.body;
jwtService.revokeRefreshToken(refreshToken);
res.json({ success: true });
});
// 受保护的API
app.get('/api/user/profile', (req, res) => {
const auth = req.headers.authorization;
if (!auth?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = auth.slice(7);
const result = jwtService.verify(token, jwtService.secret);
if (!result.valid) {
return res.status(result.error === 'Token expired' ? 401 : 403).json({
error: result.error
});
}
res.json({ user: result.payload, data: 'Your profile data' });
});
// ===== 前端配合 =====
const AXIOS_INTERCEPTOR_EXAMPLE = `
// 前端:Axios拦截器实现Token自动刷新
import axios from 'axios';
const api = axios.create({ baseURL: 'http://localhost:3000/api' });
let isRefreshing = false;
let failedQueue = [];
const processQueue = (error, token = null) => {
failedQueue.forEach(prom => {
if (error) prom.reject(error);
else prom.resolve(token);
});
failedQueue = [];
};
// 请求拦截器:自动添加Token
api.interceptors.request.use(config => {
const token = localStorage.getItem('accessToken');
if (token) {
config.headers.Authorization = 'Bearer ' + token;
}
return config;
});
// 响应拦截器:自动刷新
api.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then(token => {
originalRequest.headers.Authorization = 'Bearer ' + token;
return api(originalRequest);
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
const refreshToken = localStorage.getItem('refreshToken');
const { data } = await axios.post('/api/auth/refresh', { refreshToken });
localStorage.setItem('accessToken', data.accessToken);
localStorage.setItem('refreshToken', data.refreshToken);
processQueue(null, data.accessToken);
originalRequest.headers.Authorization = 'Bearer ' + data.accessToken;
return api(originalRequest);
} catch (refreshError) {
processQueue(refreshError, null);
// Refresh Token也过期了 → 重定向到登录页
localStorage.clear();
window.location.href = '/login';
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
}
);
`;
console.log('JWT刷新系统已启动');
app.listen(3000, () => console.log('Server on :3000'));
方案B:双Token + Token指纹(最安全)
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
33
34
35
36
37
38
39
40
41
42
43
// jwt-fingerprint.js - 带设备指纹的Token方案
class FingerprintJWT {
constructor(secret) {
this.secret = secret;
}
// 创建Token指纹(基于设备特征)
createFingerprint(req) {
const components = [
req.ip || req.connection.remoteAddress,
req.headers['user-agent'],
req.headers['accept-language'],
req.headers['sec-ch-ua'] // Client Hints
];
const hash = crypto.createHash('sha256');
hash.update(components.join('|'));
return hash.digest('hex').slice(0, 16);
}
issueToken(user, fingerprint) {
// 将指纹嵌入Token
const payload = {
sub: user.id,
fp: fingerprint, // 设备指纹
role: user.role
};
return this.sign(payload, this.secret, CONFIG.accessTokenExpiresIn);
}
verifyWithFingerprint(token, fingerprint) {
const result = this.verify(token, this.secret);
if (!result.valid) return result;
// 对比Token中的指纹和当前请求的指纹
if (result.payload.fp !== fingerprint) {
return { valid: false, error: 'FINGERPRINT_MISMATCH' };
}
return result;
}
}
指纹方案的好处:
- 即使Access Token被窃取,攻击者的IP/UserAgent不同,验证会失败
- 不需要额外的存储,指纹是从请求特征计算得到
方案C:Session模式混合(牺牲无状态换取可控性)
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
33
34
35
36
37
38
39
40
41
42
43
44
// jwt-hybrid.js - JWT + Session混合方案
// JWT仍用于传递用户信息,但使用Redis存储Token黑名单
class HybridTokenManager {
constructor(redis) {
this.redis = redis;
this.jwtService = new JWTService(CONFIG.secret, CONFIG.refreshSecret);
}
async verifyWithRevocationCheck(token) {
const result = this.jwtService.verify(token, this.jwtService.secret);
if (!result.valid) return result;
// 检查Token是否已被吊销(查询Redis)
const jti = result.payload.jti;
const isRevoked = await this.redis.get(`revoked:${jti}`);
if (isRevoked) {
return { valid: false, error: 'TOKEN_REVOKED' };
}
return result;
}
async revokeToken(token) {
const result = this.jwtService.verify(token, this.jwtService.secret, { verifyExp: false });
if (!result.payload) return;
const jti = result.payload.jti;
const exp = result.payload.exp;
const ttl = exp - Math.floor(Date.now() / 1000);
// 在Redis中记录吊销,TTL等于Token剩余有效期
if (ttl > 0) {
await this.redis.set(`revoked:${jti}`, '1', 'EX', ttl);
}
}
async revokeAllUserTokens(userId) {
// 这是一种更彻底的吊销:更改用户的secret版本
const newVersion = crypto.randomBytes(4).readUInt32BE();
await this.redis.set(`user:${userId}:token_version`, newVersion);
}
}
4. JWT的常见安全误区
误区1:Base64编码就是加密
1
2
3
4
5
6
7
8
// ❌ 错误认知
const jwt = "eyJzdWIiOiJ1c2VyXzEyMzQ1In0"; // Base64编码的内容
// const decoded = atob(jwt); // → '{ "sub": "user_12345" }'
// ✅ 正确认知
// JWT的Payload是Base64URL编码,不是加密!
// 任何人都可以解码查看内容,只是无法伪造签名。
// 不要在JWT中存储敏感信息(密码、信用卡号等)
误区2:使用太短的密钥
1
2
3
4
5
6
7
8
// ❌ 错误
const secret = 'abc123'; // 太短,容易被暴力破解
// ✅ 正确的密钥长度
const secret = crypto.randomBytes(32).toString('hex'); // 64字符的十六进制字符串
// HS256至少需要256位(32字节)的密钥
// HS384需要384位
// HS512需要512位
误区3:使用”none”算法
一些JWT库的旧版本默认接受alg: 'none'的Token(即无签名)。攻击者可以修改payload,将alg设为none,绕过签名验证。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 攻击者修改JWT的Header
// 原始: {"alg":"HS256","typ":"JWT"}
// 修改: {"alg":"none","typ":"JWT"}
// 然后删除签名的部分
// ✅ 防御
function verifyJWT(token, secret) {
const parts = token.split('.');
const header = JSON.parse(atob(parts[0]));
// 明确禁止none算法
if (header.alg === 'none') {
throw new Error('Algorithm "none" is not allowed');
}
// 明确指定允许的算法
const allowedAlgorithms = ['HS256', 'RS256'];
if (!allowedAlgorithms.includes(header.alg)) {
throw new Error(`Algorithm "${header.alg}" is not allowed`);
}
// 继续验证...
}
实战案例:构建多租户SaaS平台的JWT权限体系
场景描述
假设我们正在开发一个多租户SaaS平台(如项目管理工具),需要支持:
- 每个用户属于一个或多个组织(Organization)
- 每个组织有不同的角色(Owner/Admin/Member/Viewer)
- 资源级别的权限控制(项目、任务、文档)
- Token需要跨服务验证(Auth Service + API Gateway + 多个微服务)
权限模型设计
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
// saas-jwt-system.js - 多租户SaaS JWT权限系统
const express = require('express');
const crypto = require('crypto');
const jwt = require('jsonwebtoken'); // 使用标准库方便演示
// ===== 密钥管理 =====
const KEY_MANAGER = {
currentKeyId: 'key_v2',
getKey(keyId) {
const keys = {
key_v1: { private: process.env.PRIV_KEY_V1 || 'fallback-v1', public: '' },
key_v2: { private: process.env.PRIV_KEY_V2 || 'fallback-v2', public: '' }
};
return keys[keyId] || keys[this.currentKeyId];
},
// 公钥分发端点
getJwks() {
return {
keys: [{
kid: this.currentKeyId,
kty: 'oct',
alg: 'HS256',
k: Buffer.from(this.getKey(this.currentKeyId).private).toString('base64url')
// 生产环境使用RSA公钥
}]
};
},
rotateKeys() {
// 密钥轮换:生成新密钥,保留旧密钥一段时间
const newKeyId = `key_v${Date.now()}`;
const newSecret = crypto.randomBytes(32).toString('hex');
// 存储密钥到安全存储...
this.currentKeyId = newKeyId;
}
};
// ===== Token签发 =====
class TokenIssuer {
constructor() {}
createAccessToken(user, organization, context) {
// 只传递必要的最小权限信息
const payload = {
sub: user.id,
org: organization.id,
role: organization.role,
// 租户上下文
tenant: organization.tenant_id,
// 资源权限(按需加载,避免Token过大)
scope: this.computeScope(user, organization),
// 安全上下文
amr: ['pwd'], // Authentication Methods Reference
auth_time: Math.floor(Date.now() / 1000), // 认证时间
// 审计信息
ip: context.ip
};
return jwt.sign(payload, process.env.JWT_SECRET, {
algorithm: 'HS256',
expiresIn: '30m',
issuer: 'https://auth.saas.example.com',
audience: 'https://api.saas.example.com',
jwtid: crypto.randomUUID(),
keyid: KEY_MANAGER.currentKeyId
});
}
computeScope(user, org) {
// 基于角色计算权限范围
const roleScopes = {
'owner': ['*:*'],
'admin': ['project:create', 'project:read', 'project:update', 'project:delete',
'task:create', 'task:read', 'task:update', 'task:delete',
'member:invite', 'member:remove', 'billing:read'],
'member': ['project:read', 'task:create', 'task:read', 'task:update'],
'viewer': ['project:read', 'task:read']
};
return roleScopes[org.role] || [];
}
}
// ===== 鉴权中间件 =====
class AuthorizationMiddleware {
constructor(options = {}) {
this.publicEndpoints = options.publicEndpoints || [];
}
// 全局鉴权中间件
authenticate(req, res, next) {
// 公开端点跳过
if (this.publicEndpoints.some(p => req.path.startsWith(p))) {
return next();
}
const auth = req.headers.authorization;
if (!auth?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'MISSING_TOKEN' });
}
try {
const token = auth.split(' ')[1];
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256', 'RS256'],
issuer: 'https://auth.saas.example.com',
audience: 'https://api.saas.example.com'
});
// 注入用户信息
req.user = decoded;
req.tenant = decoded.tenant;
next();
} catch (err) {
if (err instanceof jwt.TokenExpiredError) {
return res.status(401).json({
error: 'TOKEN_EXPIRED',
message: 'Access token已过期,请使用refresh token刷新'
});
}
return res.status(403).json({ error: 'INVALID_TOKEN', message: err.message });
}
}
// 租户隔离检查
checkTenant(req, res, next) {
const pathOrgId = req.params.orgId;
if (pathOrgId && pathOrgId !== req.user.org) {
return res.status(403).json({
error: 'TENANT_MISMATCH',
message: '无权访问其他组织的资源'
});
}
next();
}
// 细粒度权限检查
requireScope(...requiredScopes) {
return (req, res, next) => {
const userScopes = req.user.scope || [];
// 检查是否有通配权限
if (userScopes.includes('*:*')) return next();
// 检查是否有任一所需权限
const hasPermission = requiredScopes.some(s =>
userScopes.includes(s) ||
userScopes.includes(s.split(':')[0] + ':*')
);
if (!hasPermission) {
return res.status(403).json({
error: 'INSUFFICIENT_PERMISSIONS',
required: requiredScopes,
message: `需要以下权限之一: ${requiredScopes.join(', ')}`
});
}
next();
};
}
}
// ===== API Server =====
const app = express();
app.use(express.json());
const auth = new AuthorizationMiddleware({
publicEndpoints: ['/api/auth', '/api/public']
});
const tokenIssuer = new TokenIssuer();
// 登录(签发Token)
app.post('/api/auth/login', (req, res) => {
const { email, password } = req.body;
// 验证凭据...
const user = { id: 'usr_123', name: '张三' };
const organization = {
id: 'org_456',
tenant_id: 'tenant_saas_prod',
role: 'admin'
};
const accessToken = tokenIssuer.createAccessToken(user, organization, {
ip: req.ip
});
res.json({
accessToken,
tokenType: 'Bearer',
expiresIn: 1800, // 30分钟
user: { id: user.id, name: user.name }
});
});
// 受保护的API
app.get('/api/org/:orgId/projects',
auth.authenticate,
auth.checkTenant,
auth.requireScope('project:read', 'project:*'),
(req, res) => {
// 获取项目的业务逻辑
res.json({ projects: [] });
}
);
app.post('/api/org/:orgId/projects',
auth.authenticate,
auth.checkTenant,
auth.requireScope('project:create', 'project:*'),
(req, res) => {
res.json({ success: true });
}
);
// JWKS端点(供其他微服务和第三方获取公钥)
app.get('/api/.well-known/jwks.json', (req, res) => {
res.json(KEY_MANAGER.getJwks());
});
// 跨服务Token验证(微服务之间)
app.post('/api/auth/introspect', (req, res) => {
const { token } = req.body;
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
res.json({
active: true,
sub: decoded.sub,
org: decoded.org,
scope: decoded.scope,
exp: decoded.exp
});
} catch {
res.json({ active: false });
}
});
app.listen(3001, () => console.log('SaaS API server on :3001'));
底层原理:JWT签名与验证的密码学基础
1. HMAC-SHA256 的数学原理
1
2
3
4
5
6
7
8
9
10
HMAC(K, M) = H((K' ⊕ opad) || H((K' ⊕ ipad) || M))
其中:
H = SHA-256哈希函数
K = 密钥
M = 消息(header.payload)
K' = 如果K比块大小(64字节)长,则K'=H(K);否则K'=K
ipad = 0x36重复64次
opad = 0x5C重复64次
⊕ = 异或操作
2. RS256(RSA-SHA256)的签名过程
1
2
3
4
5
6
1. 对消息进行SHA-256哈希
2. 将哈希值填充到RSA加密的消息长度
- 使用PKCS#1 v1.5的签名方案
- 填充格式: 0x00 || 0x01 || FF...FF || 0x00 || DigestInfo
3. 使用RSA私钥对填充后的数据进行加密(即签名)
4. 验证: 使用公钥解密签名,与计算的哈希值比对
3. Token刷新中的时钟漂移问题
分布式系统中各服务器时钟不同步,会导致Token验证失败:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function verifyWithClockSkew(token, secret, maxSkewSeconds = 30) {
const payload = decode(token);
const now = Math.floor(Date.now() / 1000);
// 允许30秒的时钟偏移
if (payload.exp && now > payload.exp + maxSkewSeconds) {
throw new Error('Token expired');
}
if (payload.nbf && now < payload.nbf - maxSkewSeconds) {
throw new Error('Token not yet valid');
}
// 签名验证...
}
高频面试题解析
面试题1:JWT和Session认证的根本区别是什么?在微服务架构中为什么JWT更受欢迎?
答案:
根本区别在于”状态”的存在位置:
Session认证是有状态的:
- 用户信息存储在服务端(内存、Redis、数据库)
- 客户端只保存一个session ID(Cookie)
- 每个请求服务端都需要查询会话存储
JWT是无状态的:
- 用户信息编码在Token中
- 服务端通过签名验证即可信任Token内容
- 不需要共享会话存储
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Session认证
// 请求流程:请求 → 验证Cookie → 查询Redis获取session数据 → 处理请求
app.use(async (req, res, next) => {
const sessionId = req.cookies.sessionId;
const session = await redis.get(`session:${sessionId}`);
if (!session) return res.status(401).end();
req.user = JSON.parse(session);
next();
});
// JWT认证
// 请求流程:请求 → 验证签名 → 解析Token → 处理请求
app.use((req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
try {
req.user = jwt.verify(token, secret);
next();
} catch {
res.status(401).end();
}
});
微服务架构中JWT的优势:
- 零共享状态:每个服务独立验证Token,不需要访问中心化的session存储
- 减少网络开销:省去了每次请求查询Redis/数据库的往返时间
- 弹性更好:session存储宕机不会影响已有的有效Token
- 跨语言友好:JWT是标准格式,任何语言都有解析库
但JWT也有微服务中的陷阱:
1
2
3
4
5
6
问题:如果Auth Service更新了用户权限,其他服务什么时候能感知到?
答案是:需要等到用户的Access Token过期。
└─ 如果Access Token有效期是15分钟,
那么权限变更最多有15分钟的延迟。
└─ 解决方案:短TTL + 刷新,或实时查询权限服务
面试题2:解释Refresh Token旋转(Rotation)机制,以及它如何防御Token窃取?
答案:
Refresh Token旋转是指每次使用Refresh Token获取新的Access Token时,同时颁发一个新的Refresh Token,并使旧的Refresh Token失效。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
普通刷新流程(有风险):
1. 用户获得 accessToken1 + refreshToken1
2. 使用 refreshToken1 获取 accessToken2
3. 服务端返回 accessToken2
4. refreshToken1 仍然有效!
→ 如果 refreshToken1 被窃取,攻击者可以一直使用它获取新Token
旋转刷新流程(安全):
1. 用户获得 accessToken1 + refreshToken1
2. 使用 refreshToken1 获取 accessToken2
3. 服务端返回 accessToken2 + refreshToken2
4. refreshToken1 立即失效!
→ 如果 refreshToken1 被窃取,只能使用一次
→ 窃取者使用时,所有合法请求都会失效(竞争条件)
旋转带来的窃取检测:
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class RotatingRefreshTokenService {
constructor(redis) {
this.redis = redis;
}
async rotateRefreshToken(oldToken, userId) {
// 使用Lua脚本保证原子性
const script = `
local stored = redis.call('GET', KEYS[1])
if not stored or stored ~= ARGV[1] then
-- Token已被使用或被篡改 → 可能是窃取!
return redis.error_reply('TOKEN_STOLEN')
end
local newToken = ARGV[2]
redis.call('SET', KEYS[1], newToken, 'EX', ARGV[3])
return 'OK'
`;
const newToken = crypto.randomBytes(32).toString('hex');
try {
await this.redis.eval(script,
[`refresh:${userId}`],
[oldToken, newToken, 7 * 24 * 3600]
);
return { newToken, rotated: true };
} catch (err) {
if (err.message === 'TOKEN_STOLEN') {
// 检测到Token窃取!
// 1. 吊销该用户的所有Token
await this.revokeAllUserTokens(userId);
// 2. 记录安全事件
await this.logSecurityEvent('TOKEN_THEFT_DETECTED', userId);
// 3. 要求用户重新登录
return { stolen: true };
}
throw err;
}
}
async revokeAllUserTokens(userId) {
// 增加用户的token版本号
await this.redis.incr(`token_version:${userId}`);
}
}
旋转的机制:
1
2
3
4
5
6
7
8
9
10
11
12
13
正常用户:
refreshToken1 → 刷新 → refreshToken2 → 刷新 → refreshToken3
攻击者窃取了refreshToken1:
(攻击者还没有使用refreshToken1)
正常用户刷新:refreshToken1 → refreshToken2 ✅ 正常
攻击者尝试:refreshToken1 ❌ 已被消费 → TOKEN_STOLEN
或者:
攻击者使用refreshToken1 → 获得refreshToken2'
正常用户下次刷新:refreshToken1 ❌ 已被消费 → TOKEN_STOLEN
无论哪种情况,都会触发窃取告警!
面试题3:在设计JWT鉴权系统时,如何处理不同权限层级(如User、Admin、SuperAdmin)的Token有效期和安全性?
答案:
一种称为”分层Token策略”的解决方案:
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
class TieredTokenStrategy {
constructor() {
this.tiers = {
read_only: {
accessTokenTTL: '1h',
refreshTokenTTL: '30d',
allowedIPs: false,
requireMFA: false
},
standard: {
accessTokenTTL: '15m',
refreshTokenTTL: '7d',
allowedIPs: true,
requireMFA: false
},
privileged: {
accessTokenTTL: '5m',
refreshTokenTTL: '24h',
allowedIPs: true,
requireMFA: true,
requireTPM: true // Trusted Platform Module
},
admin: {
accessTokenTTL: '2m',
refreshTokenTTL: '4h',
allowedIPs: true,
requireMFA: true,
requireTPM: true,
ipWhitelist: true, // 限制IP范围
approvalRequired: true // 双人审批
}
};
}
issueToken(user) {
const tier = this.determineTier(user);
const config = this.tiers[tier];
const token = jwt.sign({
sub: user.id,
tier,
// 高风险操作需要额外验证
require_mfa: config.requireMFA,
require_step_up: user.lastMFATime < Date.now() - 3600000
}, getSecret(tier), {
expiresIn: config.accessTokenTTL
});
return { tier, token, ttl: config.accessTokenTTL };
}
determineTier(user) {
if (user.roles.includes('super_admin')) return 'admin';
if (user.roles.includes('admin')) return 'privileged';
if (user.isPrivileged) return 'standard';
return 'read_only';
}
// 分级API保护
middlewareForTier(minimumTier) {
const tierOrder = ['read_only', 'standard', 'privileged', 'admin'];
return (req, res, next) => {
const userTier = req.user.tier;
const userTierIndex = tierOrder.indexOf(userTier);
const minTierIndex = tierOrder.indexOf(minimumTier);
if (userTierIndex < minTierIndex) {
return res.status(403).json({
error: 'INSUFFICIENT_TIER',
currentTier: userTier,
requiredTier: minimumTier,
message: '需要更高权限的Token才能访问此资源'
});
}
// 检查是否需要提升权限
if (req.user.require_step_up) {
return res.status(401).json({
error: 'STEP_UP_REQUIRED',
message: '请重新进行MFA验证以提升权限等级'
});
}
next();
};
}
}
// 使用示例
// GET /api/public → 公开
// GET /api/profile → standard
// POST /api/transfer → privileged(需要MFA)
// DELETE /api/users → admin(需要MFA + IP白名单)
总结与扩展
JWT是一个优雅的认证方案,但它的优雅来自于对问题的重新简化。在实践中,没有银弹——JWT的”无状态”既是最大的优点,也是最大的局限。
核心结论:
- JWT不是加密——它只是签名,敏感信息不要放在Payload中
- 短TTL + 刷新是所有生产系统的标配——除非你能接受手动吊销Token的复杂性
- 对称算法(HS256)适合单体应用,非对称(RS256/ES256)适合微服务——根据架构选择
- Refresh Token旋转是防御窃取的关键机制——不是可选项,是必选项
- 不要自己实现JWT库——使用经过验证的成熟库(如
jsonwebtoken、jose)
未来方向:
- DPoP(Demonstration of Proof-of-Possession):OAuth 2.0的扩展,将Token绑定到特定的客户端密钥,使被盗Token无法在其他设备上使用
- PASETO(Platform-Agnostic Security Tokens):被称为”更安全的JWT替代品”,移除了”none”算法等危险特性
- WebAuthn + JWT:无密码认证与JWT的结合,从根本上解决凭证泄露问题
推荐资源: