4. API 鉴权方案实现: 管理、拦截器自动携带 Authorization、刷新 逻辑

4.1 原理概述

API 鉴权的核心是让服务端能够可靠地识别请求发起方身份。 机制(通常指 JWT)将用户身份信息加密后返回给客户端,客户端在后续请求中通过 Authorization: Bearer <> 头携带该凭证。服务端验证签名后即可恢复用户身份,无需保持会话状态。

鉴权链路包含三个关键环节:获取 (登录时由服务端颁发)、自动携带 (通过客户端拦截器)、 刷新(在 过期前无感获取新凭证)。

4.2 核心实现流程

  1. 登录获取 :用户提交凭证(用户名/密码),服务端返回 access_(短期有效,如 15 分钟)和 refresh_(长期有效,如 7 天)。
  2. 本地安全存储:将 access_ 和 refresh_ 存入持久化存储(如 localStorage、SecureStorage 或 Cookie 的 HttpOnly 形式)。
  3. 请求拦截器自动携带:每次发请求前从存储中读取 access_,注入 Authorization 头。
  4. 响应拦截器检测 401:当后端返回 401 Unauthorized 时,触发刷新流程。
  5. 无感刷新 :使用 refresh_ 换取新的 access_,重放原请求失败的队列,并更新本地 。
  6. 并发请求锁:同一时刻多个请求因 过期失败时,只发起一次刷新请求,其他请求排队等待新 。

4.3 管理要点

类型 生命周期 存储位置 使用场景
access_ 短(15 min ~ 2 h) 内存 / localStorage 每次 API 请求的 Authorization 头
refresh_ 长(7 ~ 30 天) httpOnly Cookie 或 SecureStorage 仅用于刷新 access_,不参与业务请求
安全提示: 切勿将 refresh_ 暴露给 JavaScript 环境;生产环境应使用 httpOnly + Secure + SameSite=Strict 的 Cookie 传输 refresh_,或使用专门的安全存储 SDK(如 React Native 的 Keychain)。

4.4 内联流程图: 自动携带与刷新机制

API 鉴权流程: 获取 → 自动携带 → 过期刷新 客户端发起请求 has access_? 携带 Authorization 头 发送请求到服务器 状态码 == 401? 正常返回数据 触发 刷新 用 refresh_ 换新 access_ 重放被阻塞的请求 并发锁:刷新期间其他 401 请求排队等待,避免多次 刷新请求。

4.5 拦截器自动携带 Authorization

以 Axios 为例,在 请求拦截器 中注入 ;在 响应拦截器 中处理 401 并触发刷新。

// 请求拦截器 - 自动携带 access_
axios.interceptors.request.use(
  (config) => {
    const  = localStorage.getItem('access_');
    if () {
      config.headers.Authorization = `Bearer ${}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

4.6 刷新 逻辑(含并发锁)

let isRefreshing = false;
let failedQueue = [];

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

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

    if (error.response?.status === 401 && !originalRequest._retry) {
      if (isRefreshing) {
        // 正在刷新中,将请求加入等待队列
        return new Promise((resolve, reject) => {
          failedQueue.push({ resolve, reject });
        }).then( => {
          originalRequest.headers.Authorization = `Bearer ${}`;
          return axios(originalRequest);
        });
      }

      originalRequest._retry = true;
      isRefreshing = true;

      try {
        const refreshToken = localStorage.getItem('refresh_');
        const { data } = await axios.post('/api/auth/refresh', {
          refresh_: refreshToken
        });
        const newAccessToken = data.access_;
        localStorage.setItem('access_', newAccessToken);

        // 重放队列中其他请求
        processQueue(null, newAccessToken);

        // 重放当前请求
        originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
        return axios(originalRequest);
      } catch (refreshError) {
        // 刷新失败,清空  并跳转登录
        localStorage.removeItem('access_');
        localStorage.removeItem('refresh_');
        processQueue(refreshError, null);
        window.location.href = '/login';
        return Promise.reject(refreshError);
      } finally {
        isRefreshing = false;
      }
    }

    return Promise.reject(error);
  }
);
并发请求优化: 上述代码使用 isRefreshing 标志和 failedQueue 保证了多个 401 并发时只发起一次刷新,避免 被多次刷新导致竞争。实际生产环境建议将队列设计成 Promise 的 resolve/reject 形式,确保所有请求都能按顺序获得新 。

4.7 刷新 的 API 约定

// 请求示例
POST /api/auth/refresh
Content-Type: application/json

{
  "refresh_": "eyJhbGciOiJIUzI1NiIs..."
}

// 成功响应 (200 OK)
{
  "access_": "eyJhbGciOiJSUzI1NiIs...",
  "expires_in": 900,
  "_type": "Bearer"
}

// 失败响应 (401 / 403)
{
  "error": "invalid_grant",
  "error_description": "refresh_ expired or revoked"
}

4.8 常见错误及处理策略

错误场景 现象 解决方案
Access 未携带 接口返回 401,响应头无 Authorization 检查请求拦截器逻辑;确认 是否从存储中成功读取
Refresh 失效 刷新端点返回 401/403,循环刷新失败 清除本地 ,强制跳转登录页;使用独立错误码区分 refresh 失败和普通 401
并发刷新竞争 多个请求触发多次刷新, 被覆盖 实现队列锁(如上代码);刷新期间禁止其他 401 进入刷新流程
存储在 localStorage 被 XSS 攻击 恶意脚本窃取 使用 httpOnly Cookie 传输 refresh_;access_ 存储于内存变量而非持久化存储

4.9 性能与稳定性优化

  • 预刷新机制: 在 过期前(如剩余 1 分钟)提前发起刷新,减少 401 重试概率。可通过解析 JWT 的 exp 字段实现。
  • 请求重试请求去重: 对于非 401 错误(如网络超时),同样可用拦截器与队列实现指数退避重试。
  • 刷新 超时保护: 为刷新请求设置独立的超时时间(如 5 秒),避免因服务端挂起导致队列无限等待。
  • 内存缓存 access_: 首次读取后缓存在模块级变量中,减少 localStorage 读取开销; 更新时同步更新内存。
注意: 预刷新机制会额外增加一次网络请求,不适合高频调用场景;应根据业务 有效期和并发量决定是否启用。在移动端或弱网环境下,建议使用刷新队列配合指数退避重试。