Node系列 · Express:JWT
JWT(JSON Web Token)是前后端分离 / 移动端 / 微服务场景的事实标准会话方案——无状态、天然支持跨域、无需服务端存储。本章讲清楚 JWT 的结构、生成、验证与安全实践。
一、JWT 是什么
JWT 是一段签名过的 JSON,结构上分三部分:
text
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsIm5hbWUiOiJBbGljZSJ9.dBjftJeN4V6Bw5d7C3xH_kYc5x5x5x5x5x5x5x5x5
└──────────┬──────────┘ └────────────┬────────────┘ └─────────────┬─────────────┘
header payload signature| 部分 | 内容 | 示例 |
|---|---|---|
| header | 算法 + 类型 | {"alg":"HS256","typ":"JWT"} |
| payload | 业务数据 | {"userId":1,"name":"Alice","exp":1723698600} |
| signature | 签名 | HMACSHA256(base64(header) + "." + base64(payload), secret) |
每部分用 Base64URL 编码,用 . 分隔。
二、为什么用 JWT
| 维度 | JWT | Session |
|---|---|---|
| 数据存储 | 客户端(payload) | 服务端 |
| 多实例 | 天然支持(无状态) | 需要共享存储 |
| 跨域 | Cookie 自动带,Authorization header 手动带 | Cookie 受跨域限制 |
| 主动失效 | ❌ 难(要等过期) | ✅ 服务端删除即可 |
| 移动端 | ✅ 友好 | Cookie 在 WebView 里麻烦 |
| 性能 | 无需查库/Redis | 每次要查存储 |
JWT 适合前后端分离 / 移动端 / 微服务;Session 适合传统 Web 应用。
三、安装与基础使用
bash
npm install jsonwebtoken3.1 签发 Token
javascript
const jwt = require('jsonwebtoken');
const SECRET = process.env.JWT_SECRET || 'dev-secret-change-in-production';
const token = jwt.sign(
{ userId: 1, name: 'Alice' }, // payload
SECRET,
{
expiresIn: '7d', // 过期时间
issuer: 'my-app', // 签发者(可选)
audience: 'web', // 受众(可选)
}
);
console.log(token);
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsIm5hbWUiOiJBbGljZSIsImlhdCI6MTcyMzY5ODYwMCwiZXhwIjoxNzI0MzAyNDAwLCJpc3MiOiJteS1hcHAiLCJpYXQiOjE3MjM2OTg2MDAsImV4cCI6MTcyNDMwMjQwMCwiaXNzIjoibXktYXBwIiwiaWF1ZCI6IndlYiJ9.xxxxxx3.2 验证 Token
javascript
try {
const payload = jwt.verify(token, SECRET);
console.log(payload);
// { userId: 1, name: 'Alice', iat: 1723698600, exp: 1724302400, ... }
} catch (err) {
if (err.name === 'TokenExpiredError') {
// 过期
} else if (err.name === 'JsonWebTokenError') {
// 签名错或格式错
}
}jwt.verify 验证三件事:
- 签名是否合法(防止伪造)
- 是否过期(
expclaim) - 格式是否正确
3.3 解码但不验证(不安全)
javascript
const decoded = jwt.decode(token); // 不验签
console.log(decoded);DANGER
jwt.decode 不验证签名——不能用于鉴权。只在调试或需要看 payload 内容时用。
四、常用 claims(载荷字段)
JWT 标准字段:
| claim | 含义 |
|---|---|
iss (issuer) | 签发方 |
sub (subject) | 主题(通常是用户 ID) |
aud (audience) | 受众 |
exp (expiration) | 过期时间(秒级时间戳) |
nbf (not before) | 生效时间 |
iat (issued at) | 签发时间 |
jti (JWT ID) | 唯一标识,用于防重放 |
自定义字段(业务用):
javascript
jwt.sign({
userId: 1,
name: 'Alice',
role: 'admin', // 角色
scope: ['read', 'write'] // 权限范围
}, SECRET, { expiresIn: '7d' });五、签名算法
| 算法 | 类型 | 用法 |
|---|---|---|
| HS256 | HMAC + 共享密钥 | 单服务 / 内部系统 |
| RS256 | RSA + 公私钥 | 跨服务 / 第三方签名验证 |
| ES256 | ECDSA + 公私钥 | 移动端 / 高安全场景 |
HS256 用同一个 secret 签和验——简单但不适合多服务。RS256 / ES256 私钥签名,公钥验签——A 服务签,B 服务验,互不知道对方私钥。
六、过期与刷新
JWT 默认 7 天过期。生产推荐双 Token 机制:
| Token | 用途 | 有效期 |
|---|---|---|
| Access Token | 接口鉴权 | 15 分钟 - 1 小时 |
| Refresh Token | 刷新 Access Token | 7 - 30 天 |
javascript
// 登录:发两个 token
const accessToken = jwt.sign({ userId: 1 }, SECRET, { expiresIn: '15m' });
const refreshToken = jwt.sign({ userId: 1, type: 'refresh' }, REFRESH_SECRET, { expiresIn: '7d' });
// 用 refresh token 换新的 access token
app.post('/api/refresh', (req, res) => {
try {
const payload = jwt.verify(req.body.refreshToken, REFRESH_SECRET);
if (payload.type !== 'refresh') {
return res.status(401).json({ error: 'Invalid token type' });
}
const newAccessToken = jwt.sign({ userId: payload.userId }, SECRET, { expiresIn: '15m' });
res.json({ accessToken: newAccessToken });
} catch (err) {
res.status(401).json({ error: 'Token expired or invalid' });
}
});七、安全最佳实践
| 实践 | 说明 |
|---|---|
| HTTPS 传输 | Token 走 HTTP 会被截获 |
| HttpOnly Cookie 存 token | 防 XSS 偷 token(比 LocalStorage 安全) |
| 短过期 + Refresh Token | Access token 短期,refresh token 长期 |
| 不存敏感数据在 payload | payload 只 Base64 编码,任何人可读 |
| 密钥轮换 | 定期换 secret(让旧 token 失效) |
| 撤销机制 | 重要操作要能主动失效 token(如改密后失效旧 token) |
DANGER
不要把敏感数据放进 JWT payload:
javascript
// ❌ 错误:密码放 payload
jwt.sign({ userId: 1, password: user.passwordHash }, SECRET);
// ✅ 正确:只放 ID,业务数据按 ID 查询
jwt.sign({ userId: 1 }, SECRET);payload 是 Base64 编码,任何拿到 token 的人都能解码看到内容。签名只能防篡改,不能防读。
八、JWT 的撤销难题
JWT 一旦签发,到期前无法主动失效——被盗的 token 在过期前都能用。解决方案:
| 方案 | 实现 |
|---|---|
| 短过期 | Access Token 15 分钟过期 |
| 黑名单 | 服务端维护 revoked_tokens 表,每次请求校验 |
| 版本号 | payload 放 tokenVersion,改密后递增,旧 token 自然失效 |
javascript
// 用户表加 tokenVersion 字段
async function generateToken(user) {
return jwt.sign(
{ userId: user.id, tokenVersion: user.tokenVersion },
SECRET,
{ expiresIn: '15m' }
);
}
// 每次请求校验版本
async function authMiddleware(req, res, next) {
const { userId, tokenVersion } = jwt.verify(token, SECRET);
const user = await User.findByPk(userId);
if (user.tokenVersion !== tokenVersion) {
return res.status(401).json({ error: 'Token invalidated' });
}
req.user = user;
next();
}
// 用户改密时
user.tokenVersion += 1;
await user.save();
// 所有旧 token 立即失效九、最佳实践
| 场景 | 推荐 |
|---|---|
| secret | 长随机字符串,存环境变量 |
| 过期 | Access 15-60 分钟;Refresh 7-30 天 |
| 传输 | HTTPS + HttpOnly Cookie |
| 撤销 | tokenVersion 字段 + 短过期双保险 |
| payload | 只放 ID + 业务字段,不放敏感数据 |
| 多服务 | 用 RS256 / ES256,公私钥分离 |
| 算法 | HS256 起步;多服务切 RS256 |
十、小结
- JWT 三部分:header(算法)+ payload(数据)+ signature(签名)
jwt.sign(payload, secret, options)签发;jwt.verify(token, secret)验证- JWT 无状态、跨域友好,适合前后端分离 / 移动端 / 微服务
- 双 Token 机制:短期 Access Token + 长期 Refresh Token
- payload 不存敏感数据(只 Base64 编码不加密)
- 用
tokenVersion实现主动撤销 - 永远用 HTTPS 传输
