以下是关于 Token 认证系统 的详细解析,包含核心概念、完整代码示例及对比总结:
1. Token 令牌详解
定义:Token 是一种字符串,用于标识用户身份和权限。常用格式为 JWT(JSON Web Token),结构为 Header.Payload.Signature
。
JWT 结构示例
// Header(加密算法+类型)
{
"alg": "HS256",
"typ": "JWT"
}
// Payload(用户信息+声明)
{
"sub": "1234567890", // 用户ID
"name": "John Doe", // 用户名
"iat": 1516239022, // 签发时间(Unix时间戳)
"exp": 1516325422 // 过期时间(iat + 有效期)
}
// Signature(签名:Header + Payload + 密钥)
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
2. 认证流程详解
步骤:
- 用户登录:提交用户名/密码。
- 生成 Token:服务器验证成功后生成 JWT。
- 存储 Token:客户端保存 Token(如 localStorage)。
- 携带 Token:后续请求在
Authorization
头中携带Bearer <token>
。 - 服务器验证:解析 Token,检查签名、过期时间等。
3. 验证流程详解
关键验证步骤:
- 签名验证:确保 Token 未被篡改。
- 过期时间(exp):检查当前时间是否超过
exp
。 - 签发者(iss):验证 Token 是否由可信源签发。
- 权限(scope):检查用户是否有访问资源的权限。
4. Token 存储方式
常见存储方式对比:
方式 | 存储位置 | 适用场景 | 安全性 |
---|---|---|---|
localStorage | 浏览器本地存储 | 单页应用(SPA) | 易受 XSS 攻击 |
sessionStorage | 会话级存储 | 临时存储(关闭页面失效) | 同上 |
Cookie | HTTP 头自动携带 | 需要 HTTP 只读(Secure) | 可设置 HttpOnly 防 XSS |
内存 | 前端变量 | 临时存储(页面刷新丢失) | 无存储风险 |
5. Token 过期处理
策略:
- 短有效期 + 刷新 Token:
- Access Token:短有效期(如 15 分钟)。
- Refresh Token:长有效期,用于获取新 Access Token。
- 自动重定向:Token 过期后跳转登录页。
完整代码示例
环境:Node.js + Express + JWT(jsonwebtoken
库)
1. 生成 Token(后端)
const jwt = require('jsonwebtoken');
const secret = 'your-256-bit-secret'; // 密钥需保密
// 用户登录成功后生成 Token
app.post('/login', (req, res) => {
const user = { id: 1, name: 'John' };
// 生成 Access Token(有效期 15 分钟)
const accessToken = jwt.sign(
{ sub: user.id, name: user.name }, // Payload
secret,
{ expiresIn: '15m' } // 过期时间
);
// 生成 Refresh Token(有效期 7 天)
const refreshToken = jwt.sign(
{ sub: user.id, type: 'refresh' },
secret,
{ expiresIn: '7d' }
);
res.json({ accessToken, refreshToken });
});
2. 验证 Token 中间件(后端)
// 验证 Access Token 的中间件
function authMiddleware(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).send('未授权');
}
const token = authHeader.split(' ')[1];
jwt.verify(token, secret, (err, decoded) => {
if (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).send('Token 已过期');
}
return res.status(403).send('无效 Token');
}
req.user = decoded; // 将用户信息挂载到 req 对象
next();
});
}
3. 刷新 Token 接口(后端)
app.post('/refresh-token', (req, res) => {
const refreshToken = req.body.refreshToken;
if (!refreshToken) {
return res.status(400).send('缺少 Refresh Token');
}
try {
const decoded = jwt.verify(refreshToken, secret);
if (decoded.type !== 'refresh') {
throw new Error('无效的 Refresh Token');
}
// 生成新 Access Token
const newAccessToken = jwt.sign(
{ sub: decoded.sub, name: 'John' }, // 假设用户信息不变
secret,
{ expiresIn: '15m' }
);
res.json({ accessToken: newAccessToken });
} catch (err) {
res.status(401).send('Refresh Token 无效或过期');
}
});
4. 前端存储与携带 Token
// 前端登录成功后存储 Token
localStorage.setItem('accessToken', response.accessToken);
localStorage.setItem('refreshToken', response.refreshToken);
// 发送请求时携带 Token
fetch('/api/data', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
}
})
.then(response => {
if (response.status === 401) {
// 自动刷新 Token
return fetch('/refresh-token', {
method: 'POST',
body: JSON.stringify({ refreshToken: localStorage.getItem('refreshToken') })
})
.then(refreshResponse => {
const newToken = refreshResponse.accessToken;
localStorage.setItem('accessToken', newToken);
// 重新发送原始请求
return fetch('/api/data', { headers: { Authorization: `Bearer ${newToken}` } });
});
}
});
对比表格总结
内容 | 描述 | 代码示例片段 |
---|---|---|
Token 生成 | 使用 jwt.sign() 生成带签名的 Token | jwt.sign(payload, secret, { expiresIn: '15m' }) |
Token 验证 | 通过 jwt.verify() 检查签名和过期时间 | jwt.verify(token, secret, (err, decoded) => {...}) |
存储方式 | localStorage 适合 SPA,Cookie 需设置 HttpOnly | localStorage.setItem('token', token); |
过期处理 | 短期 Access Token + 长期 Refresh Token | 刷新接口 /refresh-token 返回新 Access Token |
中间件 | 验证 Token 并挂载用户信息到请求对象 | req.user = decoded; next(); |
关键差异与最佳实践
-
安全性:
- Refresh Token:存储在 HTTP Only Cookie 中,防止 XSS。
- Access Token:存储在 localStorage,需设置较短有效期。
-
过期策略:
- Access Token:短有效期(如 15 分钟)。
- Refresh Token:长有效期(如 7 天),但需绑定设备指纹或 IP。
-
错误处理:
- 401 未授权:Token 无效或过期。
- 403 禁止访问:签名错误或权限不足。
如果需要更复杂的场景(如 RBAC 权限控制),可扩展 Payload
中的 role
字段并在中间件中验证权限。