4. 令牌过期处理:服务端验证 exp、过期后返回 401 状态码、客户端刷新令牌流程

JWT 令牌的 exp(过期时间)声明是后端安全合规的第一道防线。本节将深入讲解服务端如何严格验证 exp、过期后如何返回标准 401 状态码,以及客户端如何通过刷新令牌(Refresh Token)机制实现无感续期。

4.1 服务端 exp 验证机制

服务端在每次收到 JWT 访问令牌时,必须执行以下验证步骤:

  1. 解码令牌:解析 JWT 的 Header 和 Payload。
  2. 验证签名:使用服务端密钥或公钥验证令牌未被篡改。
  3. 检查 exp 声明:将当前时间戳与 exp 字段对比。
  4. 判断结果:若当前时间 >= exp,则判定令牌过期。
关键合规点: 服务端必须使用 UTC 时间进行比较,避免时区差异导致验证漏洞。推荐使用 System.currentTimeMillis() / 1000(Java)或 datetime.utcnow().timestamp()(Python)获取标准时间戳。

4.2 过期后返回 401 状态码

当服务端检测到令牌过期时,必须返回 HTTP 401 Unauthorized 状态码,并在响应体中携带标准错误信息,便于客户端识别并触发刷新流程。

字段 值 说明
HTTP 状态码 401 标准未授权状态码
WWW-Authenticate Bearer error="invalid_token", error_description="The access token expired" RFC 6750 标准错误头
响应体 code TOKEN_EXPIRED 业务错误码,用于客户端逻辑判断
响应体 message 访问令牌已过期,请刷新 用户友好的提示信息
// 服务端验证 exp 并返回 401 示例(Node.js Express 中间件)
const jwt = require('jsonwebtoken');

function authMiddleware(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) {
    return res.status(401).json({
      code: 'TOKEN_MISSING',
      message: '缺少访问令牌'
    });
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({
        code: 'TOKEN_EXPIRED',
        message: '访问令牌已过期,请刷新'
      });
    }
    return res.status(401).json({
      code: 'TOKEN_INVALID',
      message: '无效的访问令牌'
    });
  }
}

4.3 客户端刷新令牌流程

客户端收到 401 且错误码为 TOKEN_EXPIRED 时,应自动触发刷新令牌流程,避免用户频繁重新登录。

最佳实践: 刷新令牌(Refresh Token)应具有更长的有效期(如 7 天),且必须存储在安全位置(HttpOnly Cookie 或 Secure Storage),避免 XSS 攻击窃取。

4.4 令牌过期与刷新流程 SVG 图

JWT 令牌过期处理与刷新流程 客户端 服务端 认证服务 ① 请求API 验证 exp 过期 ② 返回 401 + TOKEN_EXPIRED 发起刷新请求 ③ 发送 Refresh Token 验证 Refresh Token 颁发新 Access Token ④ 返回新 Access Token 重试原始请求 ⑤ 携带新令牌请求 验证通过,返回数据 流程说明: ① 客户端携带 Access Token 请求受保护 API。 ② 服务端验证 exp 过期,返回 401 + TOKEN_EXPIRED 错误码。 ③ 客户端自动使用 Refresh Token 向认证服务申请新令牌。 ④ 认证服务验证 Refresh Token 有效后,颁发新的 Access Token。

4.5 刷新令牌安全对比

对比维度 Access Token Refresh Token
有效期 短(15 分钟 ~ 1 小时) 长(7 天 ~ 30 天)
存储位置 内存 / 短期缓存 HttpOnly Cookie / Secure Storage
传输频率 每次 API 请求 仅在过期刷新时
泄露风险 高(频繁传输) 低(极少传输)
吊销机制 依赖短有效期自动失效 服务端黑名单 / 版本号控制
安全警告: 切勿将 Refresh Token 存储在 localStorage 或 sessionStorage 中,这些存储方式易受 XSS 攻击。推荐使用 HttpOnly + Secure + SameSite=Strict 的 Cookie 存储 Refresh Token。

4.6 客户端刷新令牌代码示例

// 客户端 Axios 拦截器实现自动刷新
import axios from 'axios';

const apiClient = axios.create({
  baseURL: 'https://api.example.com',
  withCredentials: true // 携带 HttpOnly Cookie 中的 Refresh Token
});

let isRefreshing = false;
let failedQueue = [];

const processQueue = (error, token = null) => {
  failedQueue.forEach(prom => {
    if (error) {
      prom.reject(error);
    } else {
      prom.resolve(token);
    }
  });
  failedQueue = [];
};

apiClient.interceptors.response.use(
  response => response,
  async error => {
    const originalRequest = error.config;

    if (error.response?.status === 401 &&
        error.response?.data?.code === 'TOKEN_EXPIRED' &&
        !originalRequest._retry) {

      if (isRefreshing) {
        return new Promise((resolve, reject) => {
          failedQueue.push({ resolve, reject });
        }).then(token => {
          originalRequest.headers.Authorization = `Bearer ${token}`;
          return apiClient(originalRequest);
        });
      }

      originalRequest._retry = true;
      isRefreshing = true;

      try {
        const response = await axios.post(
          'https://auth.example.com/refresh',
          {},
          { withCredentials: true }
        );
        const newToken = response.data.accessToken;
        processQueue(null, newToken);
        originalRequest.headers.Authorization = `Bearer ${newToken}`;
        return apiClient(originalRequest);
      } catch (refreshError) {
        processQueue(refreshError, null);
        // 刷新失败,跳转登录页
        window.location.href = '/login';
        return Promise.reject(refreshError);
      } finally {
        isRefreshing = false;
      }
    }

    return Promise.reject(error);
  }
);

4.7 令牌过期处理与刷新流程(简化版)

令牌过期处理简化流程 发起 API 请求 服务端验证 exp 令牌已过期? 是 返回 401 状态码 客户端刷新令牌 重试原始请求 否(正常处理) 返回正常响应

4.8 常见问题与避坑指南

  • 时钟偏差问题: 服务端和客户端时间不同步可能导致令牌提前过期或延迟过期。建议服务端设置 exp 时预留 30 秒的时钟偏差容忍窗口。
  • 并发刷新竞态: 多个请求同时触发刷新时,应使用队列机制(如上述代码中的 failedQueue)避免重复刷新。
  • Refresh Token 轮换: 每次使用 Refresh Token 后,应颁发新的 Refresh Token 并废弃旧的,防止重放攻击。
  • 401 状态码误判: 并非所有 401 都是令牌过期,需通过错误码 TOKEN_EXPIRED 区分,避免对无效令牌也执行刷新操作。