2. JWT签发与验证流程:服务端签发JWT、客户端存储与携带、服务端验签与解析的完整生命周期

JWT(JSON Web Token)的完整生命周期涵盖三个核心阶段:服务端签发、客户端存储与携带、服务端验签与解析。理解这一流程是构建安全权限体系的基础,任何环节的疏漏都可能导致令牌泄露、伪造或权限越界。

2.1 服务端签发JWT

签发阶段发生在用户成功完成身份认证(如密码验证、OAuth授权)之后。服务端负责构造JWT的三部分结构:Header(头部)、Payload(载荷)、Signature(签名)。

组成部分 字段示例 说明
Header {"alg":"HS256","typ":"JWT"} 声明签名算法(HS256/RS256)与令牌类型
Payload {"sub":"user_123","role":"admin","iat":1700000000,"exp":1700003600} 包含用户标识、权限声明、签发时间(iat)、过期时间(exp)等
Signature HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret) 使用服务端密钥对前两部分进行签名,防止篡改
关键原则:Payload中绝对不要存放密码、密钥等敏感信息。JWT的Payload仅做Base64Url编码,并非加密,任何持有令牌的人都可以解码读取。

签发时的算法选择直接影响安全性与性能:

  • HS256(对称算法):单密钥,签发与验签使用同一密钥,适合单体应用或内部微服务。密钥需严格保管,泄露即全量失效。
  • RS256(非对称算法):私钥签发、公钥验签,适合跨服务、跨组织的分布式场景。私钥仅签发服务持有,公钥可安全分发。
// 示例:Node.js 使用 jsonwebtoken 库签发 RS256 JWT
const jwt = require('jsonwebtoken');
const fs = require('fs');

const privateKey = fs.readFileSync('private.pem');
const payload = {
  sub: 'user_123',
  role: 'admin',
  permissions: ['read:orders', 'write:orders'],
  iat: Math.floor(Date.now() / 1000),
  exp: Math.floor(Date.now() / 1000) + 3600 // 1小时过期
};

const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
console.log('签发JWT:', token);

2.2 客户端存储与携带

客户端收到JWT后,需要安全存储并在后续请求中携带。存储方式直接决定令牌的抗攻击能力。

存储方式 安全性 适用场景 风险说明
HttpOnly Cookie 高 Web应用(同源策略) 防止XSS窃取,但易受CSRF攻击,需配合SameSite/CSRF Token
localStorage 中 SPA单页应用 易被XSS脚本读取,不推荐存储敏感令牌
内存变量 高 原生移动端/桌面端 页面刷新后丢失,需配合刷新令牌机制

携带方式通常通过HTTP Header实现:

// 标准携带方式:Authorization Header
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsInJvbGUiOiJhZG1pbiJ9.abc123_signature

// 或通过Cookie(需设置HttpOnly + Secure + SameSite=Lax)
Set-Cookie: jwt_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600
避坑提示:禁止将JWT放在URL查询参数中携带。URL会被服务器日志、浏览器历史记录、Referer Header等多处记录,极易泄露令牌。

2.3 服务端验签与解析

服务端收到请求后,需完成验签、解析、权限校验三步。验签失败或令牌过期应立即拒绝请求。

// 示例:Node.js 使用公钥验签并解析
const jwt = require('jsonwebtoken');
const fs = require('fs');

const publicKey = fs.readFileSync('public.pem');
const token = req.headers.authorization?.split(' ')[1]; // 提取Bearer Token

try {
  const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] });
  // decoded 包含 payload 中的 sub, role, permissions, iat, exp 等
  req.user = decoded;
  next(); // 进入后续路由
} catch (err) {
  if (err.name === 'TokenExpiredError') {
    return res.status(401).json({ error: '令牌已过期,请刷新' });
  }
  if (err.name === 'JsonWebTokenError') {
    return res.status(401).json({ error: '令牌无效或签名不匹配' });
  }
  return res.status(500).json({ error: '验签异常' });
}

验签后的解析结果应包含以下关键校验点:

  • 过期时间(exp):当前时间 > exp 则拒绝,返回401。
  • 签发时间(iat):可配合nbf(Not Before)字段,限制令牌在特定时间后才生效。
  • 颁发者(iss):验证令牌是否由可信服务签发,防止跨域令牌滥用。
  • 受众(aud):验证令牌是否指定给当前服务,防止令牌被重放到其他服务。
架构建议:在微服务架构中,建议将验签逻辑封装为网关层中间件或独立认证服务。业务服务只信任网关传递的已解析用户信息,避免每个服务重复实现验签逻辑,降低密钥分发复杂度。

2.4 完整生命周期流程图

下图展示了JWT从签发到验签的完整数据流,包含客户端、服务端、密钥存储三个角色的交互关系:

JWT签发与验证完整生命周期 JWT签发与验证完整生命周期 客户端(浏览器/App) 服务端(认证/业务服务) 密钥存储(私钥/公钥/密钥库) 1. 用户登录/认证 提交用户名+密码 POST /login 2. 验证身份 校验密码/凭证 读取私钥 3. 返回私钥 HS256/RS256密钥 4. 签发JWT Header+Payload+签名 返回JWT 5. 存储JWT Cookie/localStorage/内存 6. 携带JWT请求 Authorization: Bearer xxx GET /api/resource 7. 验签与解析 校验签名+过期+权限 读取公钥 8. 返回公钥 RS256公钥/HS256密钥 9. 返回业务数据 验签通过后处理请求 200 OK + 数据 10. 展示/处理数据 渲染页面/更新状态 说明:步骤1-5为签发阶段,步骤6-10为验签与使用阶段。密钥存储可内嵌于服务端或独立密钥管理服务(KMS)。

2.5 令牌刷新与黑名单机制

JWT一旦签发,在过期前无法被服务端主动撤销。为解决令牌泄露或用户权限变更问题,需引入刷新令牌与黑名单机制:

  • Access Token(短期):有效期通常为15分钟~1小时,用于业务请求鉴权。
  • Refresh Token(长期):有效期通常为7~30天,仅用于获取新的Access Token,存储在服务端或HttpOnly Cookie中。
  • 黑名单(Blacklist):将需要撤销的JWT的jti(JWT ID)存入Redis等缓存,设置TTL与令牌过期时间一致。每次验签时检查黑名单。
// 黑名单校验伪代码
function verifyToken(token) {
  const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] });
  
  // 检查黑名单
  const isBlacklisted = await redis.get(`blacklist:${decoded.jti}`);
  if (isBlacklisted) {
    throw new Error('令牌已被撤销');
  }
  
  return decoded;
}

// 撤销令牌(用户登出/权限变更时调用)
async function revokeToken(jti, exp) {
  const ttl = exp - Math.floor(Date.now() / 1000);
  if (ttl > 0) {
    await redis.setex(`blacklist:${jti}`, ttl, 'revoked');
  }
}
架构级建议:在分布式系统中,黑名单应使用Redis集群或集中式缓存存储,避免每个服务实例维护独立黑名单导致不一致。同时,黑名单的TTL必须与JWT的exp对齐,过期后自动清理,防止缓存膨胀。

2.6 常见安全风险与防御

风险类型 攻击方式 防御措施
令牌泄露 XSS窃取localStorage中的JWT 使用HttpOnly Cookie存储;开启CSP(内容安全策略)
令牌重放 截获JWT后冒充用户请求 使用HTTPS;设置短过期时间;绑定客户端指纹(如User-Agent+IP)
算法混淆 将RS256改为HS256,用公钥作为密钥伪造签名 验签时强制指定算法白名单,拒绝alg: none
密钥泄露 私钥或HS256密钥被窃取 定期轮换密钥;使用KMS管理;支持多密钥版本并行验证

JWT的生命周期管理是权限体系的第一道防线。签发时严格限定Payload内容与过期时间,存储时优先选择HttpOnly Cookie,验签时强制算法校验与黑名单检查,才能构建一个可信、可撤销、可审计的令牌系统。