3. JWT 工作流程:客户端请求、服务端签发、客户端存储、服务端验证的完整流程

JWT(JSON Web Token)的完整工作流程贯穿了用户认证与授权的四个核心阶段:客户端发起请求、服务端签发 Token、客户端安全存储以及服务端验证 Token。理解这一闭环是掌握 JWT 权限验证的基础。

核心要点: JWT 是一种无状态(Stateless)的认证机制。服务端不保存会话信息,所有用户身份与权限数据都编码在 Token 本身,通过数字签名保证其完整性与真实性。

3.1 完整流程总览

下图展示了 JWT 从用户登录到后续请求验证的完整生命周期,包含 7 个关键步骤。

JWT 完整工作流程(7 步闭环) 客户端 (浏览器/App) 认证服务端 (签发与验证) 受保护资源 (API/数据库) ① 登录请求 (用户名 + 密码) ② 验证身份 签发 JWT ③ 返回 JWT (Access Token) ④ 客户端存储 localStorage / Cookie ⑤ 携带 JWT 请求 (Authorization: Bearer <token>) ⑥ 验证 JWT 签名 + 有效期 + 权限 ⑦ 返回资源 (允许访问 / 拒绝访问)

3.2 阶段一:客户端请求(登录)

流程的起点是用户通过客户端(浏览器、移动 App 等)向服务端发送登录请求。这是整个认证链条的触发动作。

  • 请求方式:通常使用 POST 方法,通过 HTTPS 协议传输,防止敏感信息在传输过程中被窃听。
  • 请求内容:包含用户的身份凭证,最常见的是 username(或 email)和 password。
  • 数据格式:请求体通常采用 JSON 格式,例如:
POST /api/auth/login HTTP/1.1
Host: api.example.com
Content-Type: application/json

{
  "username": "alice@example.com",
  "password": "securePassword123"
}
安全提示:生产环境中务必使用 HTTPS 加密传输。明文传输密码在公网上是严重的安全漏洞,极易被中间人攻击截获。

3.3 阶段二:服务端签发 JWT

服务端收到登录请求后,执行以下三个子步骤来签发 JWT:

  1. 验证身份:查询数据库,比对用户名和密码(密码通常使用 bcrypt 等算法哈希存储)。
  2. 构建 Payload:将用户标识(如 user_id)、角色(如 role)、权限范围(如 scope)以及签发时间(iat)、过期时间(exp)等声明写入 JWT 的 Payload 部分。
  3. 生成签名:使用服务端持有的密钥(Secret)或私钥(Private Key),对 Header 和 Payload 进行签名,生成最终的 JWT 字符串。

签发过程的伪代码如下:

// 服务端签发 JWT 伪代码
function signJWT(user) {
  const header = { "alg": "HS256", "typ": "JWT" };
  const payload = {
    "sub": user.id,           // 用户 ID
    "name": user.username,    // 用户名
    "role": user.role,        // 用户角色
    "iat": Math.floor(Date.now() / 1000),       // 签发时间
    "exp": Math.floor(Date.now() / 1000) + 3600 // 过期时间(1小时后)
  };
  const secret = "your-256-bit-secret"; // 服务端密钥
  const token = jwt.sign(header, payload, secret);
  return token;
}

3.4 阶段三:客户端存储 JWT

客户端收到服务端返回的 JWT 后,需要将其安全地存储起来,以便在后续请求中携带。常见的存储方式有两种,各有优劣:

存储方式 实现方法 优点 缺点
localStorage / sessionStorage JavaScript 直接存储 简单易用,跨标签页共享(localStorage) 易受 XSS 攻击,攻击者可通过脚本窃取 Token
HttpOnly Cookie 服务端设置 Set-Cookie 头 无法被 JavaScript 读取,天然防御 XSS 易受 CSRF 攻击,需额外配置 SameSite 属性
安全警告:避免将 JWT 存储在 URL 参数或浏览器的 window.name 中。这些位置容易被泄露或劫持。对于高安全性应用,推荐使用 HttpOnly + Secure + SameSite=Strict 的 Cookie 方案。

3.5 阶段四:服务端验证 JWT

当客户端访问受保护资源时,会在 HTTP 请求头中携带 JWT。服务端收到请求后,执行严格的验证流程:

  1. 提取 Token:从 Authorization: Bearer <token> 头中提取 JWT 字符串。
  2. 解析与验签:使用服务端持有的密钥(与签发时相同)验证签名。如果签名不匹配,说明 Token 被篡改,直接拒绝请求。
  3. 检查有效期:验证 exp(过期时间)和 nbf(生效时间)等时间声明。如果 Token 已过期,返回 401 Unauthorized。
  4. 权限校验:从 Payload 中提取用户角色或权限列表,判断当前用户是否有权访问所请求的资源。
  5. 放行或拒绝:验证通过后,将用户信息注入请求上下文(如 req.user),并放行请求到业务逻辑层;验证失败则返回 401 或 403 状态码。

验证过程的伪代码如下:

// 服务端验证 JWT 伪代码(中间件)
function authMiddleware(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, 'your-256-bit-secret');
    // 检查权限(示例:仅管理员可访问)
    if (decoded.role !== 'admin') {
      return res.status(403).json({ error: '权限不足' });
    }
    req.user = decoded; // 将用户信息注入请求
    next();             // 放行
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token 已过期' });
    }
    return res.status(401).json({ error: '无效的 Token' });
  }
}

3.6 流程关键对比:有状态 vs 无状态

JWT 的核心优势在于其无状态特性。下表对比了传统 Session 认证与 JWT 认证在流程上的关键差异:

对比维度 传统 Session 认证 JWT 认证
服务端存储 需要存储 Session 数据(内存/Redis) 无需存储,Token 自包含用户信息
扩展性 水平扩展需共享 Session(如 Redis 集群) 天然支持水平扩展,任何服务节点均可验证
跨域支持 需配置 CORS 或代理 通过 Authorization 头,天然支持跨域
吊销机制 直接删除 Session 即可 需配合黑名单或短有效期 + Refresh Token

3.7 流程中的异常处理

在实际运行中,JWT 流程可能遇到多种异常情况,需要服务端和客户端协同处理:

  • Token 过期:客户端应捕获 401 状态码,并使用 Refresh Token(如果存在)自动刷新 Access Token,提升用户体验。
  • Token 被篡改:签名验证失败时,服务端应记录安全日志,并返回通用错误信息,避免泄露内部细节。
  • 权限不足:服务端返回 403 Forbidden,客户端应引导用户联系管理员或切换账号。
  • 网络中断:客户端应实现重试机制,但需避免无限重试导致资源耗尽。
最佳实践:将 JWT 的过期时间设置为较短的值(如 15-30 分钟),配合 Refresh Token(有效期较长,如 7 天)使用。这样既保证了安全性(减少 Token 泄露后的风险窗口),又提供了良好的用户体验(自动刷新,无需频繁登录)。