鉴权:双 Token 机制
背景:为什么要“双 token”
- Access token:短生命周期(例如 5~15 分钟),用于访问业务 API。
- Refresh token:长生命周期(例如 7~30 天),用于在 access token 过期后换取新的 access token。
目标通常是:
- 访问令牌泄露后可快速失效(降低风险面)。
- 用户体验上尽量无感续期(不频繁登录)。
Cookie Path 能解决的是什么问题
浏览器在发请求时,会根据请求 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 的撤销状态(防重放)。
典型流程(简化)
- 登录成功:
- 正常调用业务 API:
- access token 过期:
注意点 / 常见坑
- 同域名下的其它路径不会带 refresh token,但如果你的刷新接口路径设计得太宽(例如
Path=/),那 refresh token 会被所有请求携带,暴露面变大。 Domain、子域名策略要谨慎:避免 refresh token 被不必要的子域接触到。- 如果前端和 API 在不同子域(如
app.example.com与api.example.com),Cookie 的Domain与SameSite策略会更复杂,可能需要改成同站点架构或使用不同的 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 分钟)