JWT认证机制完全指南#
JSON Web Token(JWT)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息。它是现代Web应用中最流行的身份认证方案之一。
什么是JWT#
JWT是一个紧凑的、自包含的令牌格式,用于在各方之间以JSON对象安全地传输信息。由于信息是经过数字签名的,因此可以被验证和信任。
JWT的特点#
- 无状态:服务器不需要存储会话信息
- 跨域支持:适合分布式系统和微服务架构
- 自包含:令牌本身包含了所有必要的用户信息
- 性能优秀:减少数据库查询
JWT的结构#
JWT由三部分组成,用点(.)分隔:
Header.Payload.Signature
包含令牌的类型和使用的签名算法:
1
2
3
4
| {
"alg": "HS256",
"typ": "JWT"
}
|
常见算法:
- HS256:HMAC SHA256(对称加密)
- RS256:RSA SHA256(非对称加密)
- ES256:ECDSA SHA256
2. Payload(载荷)#
包含声明(claims),即关于实体和其他数据的声明:
1
2
3
4
5
6
| {
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622
}
|
标准声明:
iss(issuer):签发者sub(subject):主题aud(audience):接收方exp(expiration):过期时间nbf(not before):在此之前不可用iat(issued at):签发时间jti(JWT ID):唯一标识符
3. Signature(签名)#
用于验证消息在传输过程中未被篡改:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
JWT工作流程#
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 用户 │ │ 服务器 │ │ 资源 │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
│ 1. POST /login │ │
├───────────────────────────>│ │
│ │ │
│ 2. 返回JWT │ │
│<───────────────────────────┤ │
│ │ │
│ 3. GET /api/data │ │
│ Header: Authorization │ │
│ Bearer <JWT> │ │
├───────────────────────────────────────────────────────>│
│ │ │
│ 4. 返回数据 │ │
│<───────────────────────────────────────────────────────┤
│ │ │
JWT实战实现#
生成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
| const crypto = require('crypto');
// Base64URL编码
function base64UrlEncode(str) {
return Buffer.from(str)
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
// 生成JWT
function generateJWT(payload, secret, algorithm = 'HS256') {
const header = {
alg: algorithm,
typ: 'JWT'
};
const encodedHeader = base64UrlEncode(JSON.stringify(header));
const encodedPayload = base64UrlEncode(JSON.stringify(payload));
const data = `${encodedHeader}.${encodedPayload}`;
const signature = crypto
.createHmac('sha256', secret)
.update(data)
.digest('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
return `${data}.${signature}`;
}
// 使用示例
const payload = {
sub: '1234567890',
name: 'John Doe',
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + (60 * 60) // 1小时后过期
};
const secret = 'your-256-bit-secret';
const token = generateJWT(payload, secret);
console.log(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
| // 验证JWT
function verifyJWT(token, secret) {
try {
const parts = token.split('.');
if (parts.length !== 3) {
throw new Error('Invalid token format');
}
const [encodedHeader, encodedPayload, signature] = parts;
// 重新生成签名
const data = `${encodedHeader}.${encodedPayload}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(data)
.digest('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
// 验证签名
if (signature !== expectedSignature) {
throw new Error('Invalid signature');
}
// 解码payload
const payload = JSON.parse(
Buffer.from(encodedPayload, 'base64').toString()
);
// 检查过期时间
if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
throw new Error('Token expired');
}
return { valid: true, payload };
} catch (error) {
return { valid: false, error: error.message };
}
}
// 使用示例
const result = verifyJWT(token, secret);
console.log(result);
|
解码JWT(不验证签名)#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // 仅解码JWT(用于调试)
function decodeJWT(token) {
const parts = token.split('.');
if (parts.length !== 3) {
throw new Error('Invalid token format');
}
const [encodedHeader, encodedPayload] = parts;
const header = JSON.parse(
Buffer.from(encodedHeader, 'base64url').toString()
);
const payload = JSON.parse(
Buffer.from(encodedPayload, 'base64url').toString()
);
return { header, payload };
}
|
Express.js集成JWT#
安装依赖#
1
| npm install jsonwebtoken express
|
登录接口#
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
| const jwt = require('jsonwebtoken');
const express = require('express');
const app = express();
const SECRET_KEY = 'your-secret-key';
app.post('/api/login', (req, res) => {
const { username, password } = req.body;
// 验证用户凭据
if (username === 'admin' && password === 'password') {
// 创建JWT
const token = jwt.sign(
{
userId: 1,
username: 'admin',
role: 'admin'
},
SECRET_KEY,
{ expiresIn: '1h' }
);
res.json({
success: true,
token: token
});
} else {
res.status(401).json({
success: false,
message: 'Invalid credentials'
});
}
});
|
中间件验证#
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
| // 验证JWT中间件
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({
success: false,
message: 'Access token required'
});
}
jwt.verify(token, SECRET_KEY, (err, user) => {
if (err) {
return res.status(403).json({
success: false,
message: 'Invalid or expired token'
});
}
req.user = user;
next();
});
}
// 保护的路由
app.get('/api/protected', authenticateToken, (req, res) => {
res.json({
success: true,
data: 'This is protected data',
user: req.user
});
});
|
刷新令牌#
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
| const refreshTokens = [];
app.post('/api/refresh', (req, res) => {
const { token } = req.body;
if (!token) {
return res.status(401).json({
success: false,
message: 'Refresh token required'
});
}
if (!refreshTokens.includes(token)) {
return res.status(403).json({
success: false,
message: 'Invalid refresh token'
});
}
jwt.verify(token, REFRESH_SECRET, (err, user) => {
if (err) {
return res.status(403).json({
success: false,
message: 'Invalid refresh token'
});
}
const accessToken = jwt.sign(
{ userId: user.userId, username: user.username },
SECRET_KEY,
{ expiresIn: '15m' }
);
res.json({
success: true,
accessToken: accessToken
});
});
});
|
前端使用JWT#
存储JWT#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // 登录后存储token
async function login(username, password) {
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (data.success) {
// 存储到localStorage
localStorage.setItem('token', data.token);
// 或存储到cookie
document.cookie = `token=${data.token}; path=/; secure; httponly`;
}
return data;
}
|
发送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
| // 在请求头中携带token
async function fetchData() {
const token = localStorage.getItem('token');
const response = await fetch('/api/protected', {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.status === 401) {
// Token过期,需要重新登录
logout();
return null;
}
return await response.json();
}
// 使用axios拦截器
axios.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
axios.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
// 处理token过期
logout();
}
return Promise.reject(error);
}
);
|
安全最佳实践#
1. 使用HTTPS#
JWT必须在HTTPS连接中传输,防止中间人攻击。
2. 设置合理的过期时间#
1
2
3
4
5
6
7
8
9
| // 短期访问令牌
const accessToken = jwt.sign(payload, SECRET_KEY, {
expiresIn: '15m' // 15分钟
});
// 长期刷新令牌
const refreshToken = jwt.sign(payload, REFRESH_SECRET, {
expiresIn: '7d' // 7天
});
|
3. 敏感信息不放入JWT#
1
2
3
4
5
6
7
8
9
10
11
12
| // ❌ 错误:不要将敏感信息放入JWT
const payload = {
userId: 1,
password: 'secret123', // 不要这样做
creditCard: '1234-5678-9012-3456'
};
// ✅ 正确:只存储必要信息
const payload = {
userId: 1,
role: 'user'
};
|
4. 使用强密钥#
1
2
3
| // 生成强密钥
const crypto = require('crypto');
const SECRET_KEY = crypto.randomBytes(64).toString('hex');
|
5. 验证所有声明#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| function verifyJWT(token) {
const decoded = jwt.verify(token, SECRET_KEY);
// 验证发行者
if (decoded.iss !== 'https://yourdomain.com') {
throw new Error('Invalid issuer');
}
// 验证接收者
if (decoded.aud !== 'your-api') {
throw new Error('Invalid audience');
}
return decoded;
}
|
6. 实现令牌吊销#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // 使用Redis存储已吊销的令牌
const revokedTokens = new Set();
function revokeToken(token) {
revokedTokens.add(token);
}
function isTokenRevoked(token) {
return revokedTokens.has(token);
}
app.use((req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (token && isTokenRevoked(token)) {
return res.status(401).json({
success: false,
message: 'Token has been revoked'
});
}
next();
});
|
常见问题#
Q1: JWT vs Session#
JWT优势:
Session优势:
Q2: 如何处理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
| // 自动刷新token
let refreshTokenTimeout;
function startRefreshTokenTimer() {
const token = JSON.parse(localStorage.getItem('token'));
const expires = new Date(token.expireAt);
const timeout = expires.getTime() - Date.now() - (60 * 1000); // 提前1分钟
refreshTokenTimeout = setTimeout(() => {
refreshToken();
}, timeout);
}
async function refreshToken() {
const token = localStorage.getItem('token');
const response = await fetch('/api/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token })
});
const data = await response.json();
localStorage.setItem('token', JSON.stringify(data.token));
startRefreshTokenTimer();
}
|
Q3: JWT的存储位置#
localStorage:
- ✅ 易于使用
- ✅ 跨标签页共享
- ❌ 容易受到XSS攻击
Cookie:
- ✅ httponly防止XSS
- ✅ 自动发送
- ❌ 容易受到CSRF攻击
最佳实践:存储在httpOnly、secure、sameSite的cookie中。
JWT是一种强大且灵活的认证机制,特别适合现代分布式应用和微服务架构。正确使用JWT可以简化认证流程,提升应用性能和可扩展性。
记住安全第一:始终使用HTTPS、设置合理的过期时间、不在JWT中存储敏感信息,并根据具体需求选择合适的认证方案。
🧰 本章配套工具箱#
边读边练:以下工具均在浏览器本地运行,数据不上传,可直接打开使用。