2、Token认证机制:JWT结构解析、Token签发与验证、Access Token与Refresh Token
Token 认证,说白了就是服务器给你发一张「数字通行证」。你带着它来,我就不用每次都查你密码了。JWT(JSON Web Token)是目前最主流的实现方式,我几乎在每个项目里都用它。
2.1 JWT 结构:三段式,一眼看懂
JWT 长什么样?我给你看个例子:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
看到那两个点了吗?JWT 就是由三部分组成的:Header、Payload、Signature。每部分都是 Base64URL 编码,然后用点连起来。
| 部分 | 内容 | 作用 |
|---|---|---|
| Header | 算法类型(如 HS256)和 Token 类型 | 告诉服务器怎么验签 |
| Payload | 用户 ID、角色、过期时间等声明 | 携带用户身份信息 |
| Signature | 对前两部分的签名 | 防止数据被篡改 |
嗯,这里要注意:Payload 是 Base64 编码,不是加密。谁都能解码看到里面的内容。所以千万别把密码、信用卡号这类敏感信息放进去。我在项目中见过有人把用户手机号明文放进去,结果前端一解码全暴露了——这其实是个低级错误。
2.2 Token 签发:服务端说了算
签发 Token 的过程,其实就是服务端生成一个带签名的 JSON 对象。我习惯用 Node.js 的 jsonwebtoken 库,代码很简洁:
const jwt = require('jsonwebtoken');
const payload = {
sub: user.id,
role: user.role,
iat: Math.floor(Date.now() / 1000)
};
const secret = process.env.JWT_SECRET;
const token = jwt.sign(payload, secret, { expiresIn: '15m' });
这里有几个关键点:
- secret:签名密钥,必须放在服务端环境变量里,绝对不能硬编码
- expiresIn:过期时间,Access Token 一般设 15-30 分钟
- iat:签发时间,用于计算 Token 是否过期
⚠️ 我曾经踩过的坑:有一次我把 JWT_SECRET 写死在代码里,还提交到了 Git 仓库。结果代码泄露后,攻击者用这个密钥伪造了管理员 Token。从那以后,我强制团队用环境变量 + 密钥轮换策略,每个月换一次密钥。
2.3 Token 验证:每次请求都要过一遍
验证 Token 是服务端中间件的核心工作。流程其实很简单:
- 从请求头中提取
Authorization: Bearer <token> - 用同样的密钥和算法验证签名
- 检查过期时间
- 解析 Payload 获取用户信息
代码示例:
const verifyToken = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: '未提供 Token' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token 已过期' });
}
return res.status(403).json({ error: 'Token 无效' });
}
};
你想想看,如果每次请求都去查数据库验证用户身份,那性能得多差?JWT 验证是纯计算操作,毫秒级完成,这才是它最大的优势。
2.4 Access Token vs Refresh Token:双 Token 策略
为什么需要两个 Token?原因很简单:安全性和用户体验的平衡。
Access Token 有效期短(15 分钟),即使泄露了,攻击者也只能用一小会儿。但用户不可能每 15 分钟就重新登录一次,太烦人了。所以就有了 Refresh Token。
| 特性 | Access Token | Refresh Token |
|---|---|---|
| 有效期 | 短(15-30 分钟) | 长(7-30 天) |
| 存储位置 | 内存 / 短期缓存 | HttpOnly Cookie / 安全存储 |
| 传输方式 | 请求头 Bearer | 仅刷新接口使用 |
| 泄露风险 | 低(过期快) | 高(需额外保护) |
工作流程是这样的:
- 登录时,服务端同时签发 Access Token 和 Refresh Token
- 客户端用 Access Token 访问资源
- Access Token 过期后,客户端用 Refresh Token 换取新的 Access Token
- Refresh Token 过期后,用户需要重新登录
💡 我个人习惯:Refresh Token 一定要绑定设备信息(如 User-Agent、IP 段)。如果检测到 Refresh Token 在陌生设备上使用,立即作废所有 Token,强制用户重新登录。这能有效防止 Token 被盗用后的横向扩散。
2.5 核心流程:一张图说清楚
下面这张图展示了双 Token 认证的完整流程。我建议你仔细看看,尤其是「刷新」那条路径——很多新手在这里翻车。
2.6 避坑指南:我踩过的那些雷
做了这么多年认证系统,有些坑我记忆犹新:
- Token 不设过期时间:我曾经接手过一个项目,Access Token 有效期设了 30 天。结果一个员工离职后,他的 Token 还能用——这等于把公司大门钥匙给了外人。
- Refresh Token 不做轮换:每次刷新时,应该同时签发新的 Refresh Token,旧的立即作废。否则一个 Refresh Token 泄露,攻击者能一直用。
- 签名算法用错:HS256 是对称算法,RS256 是非对称。如果多个服务需要验证 Token,用 RS256 更安全——私钥只有一个,公钥可以公开。
- Payload 塞太多数据:JWT 每次请求都要传输,Payload 越大,网络开销越大。我见过有人把整个用户对象塞进去,一个 Token 好几 KB——完全没必要。
核心原则:Access Token 只存「谁」和「有什么权限」,不存「详细信息」。用户详情需要时再查数据库,或者用缓存。