JWT 认证原理与实战:从原理到代码实现
JWT 是现代 Web 应用最流行的认证方式。本文深入讲解 JWT 的原理、结构、安全最佳实践,并提供 Node.js 和前端完整实现代码。
为什么需要 JWT
早期的 Web 认证用 Session + Cookie,逻辑简单,但有个硬伤:服务器要存每份 Session,多台服务器就得做共享存储,扩展性差。
JWT 的思路不一样:服务器签发出去之后就不存了,只负责验证签名是否合法。无状态,好扩展——这也是它流行起来的核心原因。
但 JWT 的坑也比你想象的多。
JWT 结构
JWT 由三部分组成,用 . 分隔:
header.payload.signature
Header(头部)
声明 token 类型和签名算法:
{
"alg": "HS256",
"typ": "JWT"
}
常见算法:HS256(对称加密,密钥只有一个)、RS256(非对称,私钥签名公钥验证,生产推荐)。
Payload(载荷)
存实际数据,**不要存敏感信息**——Base64 是编码不是加密,谁都能解码看。
{
"sub": "1234567890",
"name": "张三",
"iat": 1516239022,
"exp": 1516242622
}
标准字段:iat(签发时间)、exp(过期时间)、sub(主题,一般是用户 ID)。
Signature(签名)
用 Header 里指定的算法,对 header.payload 进行签名。篡改 payload 后签名对不上,token 就失效了。
Node.js 实战:签发与验证
安装依赖
npm install jsonwebtoken
签发 Token
const jwt = require('jsonwebtoken');
const token = jwt.sign(
{
userId: user.id,
role: user.role
},
process.env.JWT_SECRET, // 密钥,存在环境变量里
{
expiresIn: '7d' // 7 天过期
}
);
// token 长这样:
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsInJvbGUiOiJ1c2VyIiwiZXhwIjoxNzA...
验证 Token
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// decoded = { userId: 1, role: 'user', iat: ..., exp: ... }
req.user = decoded;
} catch (err) {
// TokenExpiredError: 过期了
// JsonWebTokenError: 签名不对(被篡改或密钥错误)
return res.status(401).json({ error: 'Invalid token' });
}
Express 中间件完整示例
// middleware/auth.js
const jwt = require('jsonwebtoken');
module.exports = function authMiddleware(req, res, next) {
// 从 Authorization 头里取 token
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // "Bearer <token>"
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
return res.status(403).json({ error: 'Invalid or expired token' });
}
};
// 使用
// app.get('/api/profile', authMiddleware, (req, res) => { ... });
前端怎么存 Token
这是 JWT 用得最乱的地方。常见三种方式:
| 存储方式 | 安全性 | 说明 |
|---------|--------|------|
| localStorage | 差 | 容易被 XSS 读取,不推荐 |
| sessionStorage | 一般 | 关闭标签页就清空,XSS 仍有风险 |
| HttpOnly Cookie | 好 | JS 读不到,防 XSS,但要防 CSRF |
**推荐做法**:用 HttpOnly + SameSite=Strict 的 Cookie 存 token,后端设置:
// Express 设置 Cookie
res.cookie('token', token, {
httpOnly: true, // JS 读不到
secure: true, // 只在 HTTPS 下传输
sameSite: 'strict', // 防 CSRF
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 天
});
刷新 Token(Refresh Token)
Access Token 设短一点(比如 15 分钟),另外发一个有效期长的 Refresh Token,过期时用 Refresh Token 换新的 Access Token。
流程:
2. Access Token 过期 → 前端调 /refresh 接口,用 Refresh Token 换新的
3. Refresh Token 也过期 → 用户重新登录
这样即使 Access Token 被截获,有效期短,损失可控。
常见坑
坑1:把敏感信息放 Payload
Payload 是 Base64 编码,不是加密。有人把用户密码、身份证号放进去——用 atob() 就能解码,跟明文没区别。
坑2:密钥硬编码
把 JWT 密钥写死在代码里并推到 GitHub,这种事发生得比你想象的多。一定要用环境变量。
# .env
JWT_SECRET=super_random_string_at_least_32_chars
坑3:Token 无法主动失效
JWT 是无状态的,签发之后在过期前一直有效。如果用户注销或被盗号,没法让 token 立刻失效。
**解决方案**:维护一个 Redis 黑名单,验证 token 时先查是否在黑名单里。或者把 token 存在 Redis,注销时删掉,验证时查 Redis 是否存在。
坑4:用了 `none` 算法
早期 JWT 库支持 alg: "none",攻击者可以把 Header 改成 none,绕过签名验证。
**现在的主流库已经修复了这个问题**,但如果你用的库比较老,要手动禁用 none 算法。
该用 JWT 还是 Session?
| 场景 | 推荐 |
|------|------|
| 单体应用、传统 Web 应用 | Session + Cookie |
| 微服务、API 服务 | JWT |
| 需要服务端主动失效(如注销) | Session 或 JWT+Redis |
| 移动端 App 后端 | JWT |
总结
JWT 不是银弹。无状态带来的扩展好处,是以无法主动失效为代价的。用之前想清楚你的场景到底需不需要它。
> **Pro Tip**: JWT 的 Secret 长度至少 32 字符,用随机生成而不是单词,定期更换。