跳到主要内容

鉴权:双 Token 机制

背景:为什么要“双 token”

  • Access token:短生命周期(例如 5~15 分钟),用于访问业务 API。
  • Refresh token:长生命周期(例如 7~30 天),用于在 access token 过期后换取新的 access token。

目标通常是:

  • 访问令牌泄露后可快速失效(降低风险面)。
  • 用户体验上尽量无感续期(不频繁登录)。

浏览器在发请求时,会根据请求 URL 自动决定带哪些 Cookie:

  • 只有当请求路径满足 requestPath cookiePath 为前缀 时,该 Cookie 才会被附带。

因此可以把 refresh token Cookie 设为更“深”的路径,例如:

  • refresh token Cookie:Path=/auth/refresh
  • access token Cookie(如果也用 Cookie):Path=/ 或者根本不放 Cookie、改用 Authorization: Bearer ...

这样做的效果是:

  • 绝大多数业务 API 请求不会携带 refresh token(因为路径不匹配)。
  • refresh token 只会在调用 /auth/refresh(或同前缀路径)时被浏览器自动带上。

HttpOnly / Secure / SameSite 的配合

仅靠 Path 不是安全边界(只是“减少暴露面/减少携带”),通常会组合:

  • HttpOnly:防止 JS 直接读取 refresh token(降低 XSS 直接窃取的概率)。
  • Secure:仅 HTTPS 传输。
  • SameSite=Lax/Strict(或必要时 None; Secure):降低 CSRF 风险(取决于是否需要跨站)。
  • 服务器端绑定/轮换 refresh token:例如 refresh token 一次性使用、每次刷新都轮换(rotation),并保存旧 token 的撤销状态(防重放)。

典型流程(简化)

  1. 登录成功:
  2. 正常调用业务 API:
  3. access token 过期:

注意点 / 常见坑

  • 同域名下的其它路径不会带 refresh token,但如果你的刷新接口路径设计得太宽(例如 Path=/),那 refresh token 会被所有请求携带,暴露面变大。
  • Domain、子域名策略要谨慎:避免 refresh token 被不必要的子域接触到。
  • 如果前端和 API 在不同子域(如 app.example.comapi.example.com),Cookie 的 DomainSameSite 策略会更复杂,可能需要改成同站点架构或使用不同的 token 存储/传递方式。

双 token 机制:通过设置 refresh token 的 Cookie Path,降低其可见性,避免每次请求都自动携带。


前端实现代码

Axios 拦截器自动刷新

import axios from "axios";

const api = axios.create({ baseURL: "/api" });
let isRefreshing = false;
let failedQueue = [];

function processQueue(error, token = null) {
failedQueue.forEach(({ resolve, reject }) => {
error ? reject(error) : resolve(token);
});
failedQueue = [];
}

api.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((token) => {
originalRequest.headers.Authorization = "Bearer " + token;
return api(originalRequest);
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
const { data } = await axios.post("/auth/refresh");
const newToken = data.accessToken;
processQueue(null, newToken);
originalRequest.headers.Authorization = "Bearer " + newToken;
return api(originalRequest);
} catch (refreshError) {
processQueue(refreshError);
window.location.href = "/login";
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
}
);

💡 关键:用队列 failedQueue 收集并发请求,避免同时发起多个 refresh 请求。刷新完成后统一用新 token 重试。

安全加固清单

  • Refresh Token 存 HttpOnly Cookie,JS 无法读取
  • Cookie Path 限制为 /auth/refresh,减少暴露面
  • Token Rotation:每次刷新后旧 token 立即失效
  • 服务端维护 token 黑名单(Redis)
  • 检测复用攻击:旧 refresh token 被使用时,撤销整个 token family
  • Access Token 有效期尽量短(5-15 分钟)