Skip to content

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

维度JWTSession
数据存储客户端(payload)服务端
多实例天然支持(无状态)需要共享存储
跨域Cookie 自动带,Authorization header 手动带Cookie 受跨域限制
主动失效❌ 难(要等过期)✅ 服务端删除即可
移动端✅ 友好Cookie 在 WebView 里麻烦
性能无需查库/Redis每次要查存储

JWT 适合前后端分离 / 移动端 / 微服务;Session 适合传统 Web 应用

三、安装与基础使用

bash
npm install jsonwebtoken

3.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.xxxxxx

3.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 验证三件事:

  1. 签名是否合法(防止伪造)
  2. 是否过期(exp claim)
  3. 格式是否正确

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' });

五、签名算法

算法类型用法
HS256HMAC + 共享密钥单服务 / 内部系统
RS256RSA + 公私钥跨服务 / 第三方签名验证
ES256ECDSA + 公私钥移动端 / 高安全场景

HS256 用同一个 secret 签和验——简单但不适合多服务。RS256 / ES256 私钥签名,公钥验签——A 服务签,B 服务验,互不知道对方私钥。

六、过期与刷新

JWT 默认 7 天过期。生产推荐双 Token 机制

Token用途有效期
Access Token接口鉴权15 分钟 - 1 小时
Refresh Token刷新 Access Token7 - 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 TokenAccess token 短期,refresh token 长期
不存敏感数据在 payloadpayload 只 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 传输