Token的无感刷新是一种在用户不感知的情况下自动刷新访问令牌(Access Token)的技术,以避免因Token过期而导致的用户重新登录或页面刷新。这种机制通常依赖于双Token机制,即使用一个短期的Access Token和一个长期的Refresh Token。
- 双Token机制:
- Access Token:用于访问受保护的资源,通常具有较短的过期时间(如30分钟),每次请求时需要携带此Token。
- Refresh Token:用于在Access Token过期后获取新的Access Token,通常具有较长的过期时间(如几天或几周),不需要频繁更换。
- 无感刷新实现原理:
- 当客户端检测到即将过期的Access Token时,会自动向服务器请求新的Access Token。
- 客户端携带Refresh Token向服务器请求新的Access Token。
- 如果服务器验证刷新成功,它会返回新的Access Token和可能更新的Refresh Token。
- 客户端更新本地存储的Token,并继续使用新的Access Token进行后续请求,从而实现无感知的Token刷新。
- 实现步骤:
- 前端实现:在前端应用中,通过监听Token即将过期的时间点,自动触发刷新操作。例如,在Vue中可以使用axios拦截器来捕获Token过期的情况,并自动刷新Token。
- 优化与注意事项:
- 避免频繁刷新:可以通过设置全局变量来控制Token刷新的频率,确保只有在Token即将过期时才进行刷新,避免不必要的请求。
- 安全性:确保Refresh Token的安全性,防止其被泄露或滥用。例如,可以限制其使用次数或设置较短的过期时间。
实现Token无感刷新的详细步骤与代码示例:
1. 定义常量
首先,定义一些常量用于存储Token和HTTP请求头字段。
// config/constant.js
export const ACCESS_TOKEN = "s_tk"; // 短token
export const REFRESH_TOKEN = "l_tk"; // 长token
export const AUTH = "Authorization"; // 存放短token
export const PASS = "PASS"; // 存放长token
2. 定义返回码
定义一些返回码用于处理不同的业务逻辑。
// config/returnCodeMap.js
export const CODE_LOGGED_OTHER = 106; // 在其它客户端被登录
export const CODE_RELOGIN = 108; // 重新登陆
export const CODE_TOKEN_EXPIRED = 104; // token过期
export const CODE_SUCCESS = 0; // 接口请求成功
3. 封装Axios服务
封装Axios服务,添加请求拦截器和响应拦截器。
// service/index.js
import axios from "axios";
import { refreshAccessToken, addSubscriber } from "./refresh";
import { clearAuthAndRedirect } from "./clear";
import { CODE_LOGGED_OTHER, CODE_RELOGIN, CODE_TOKEN_EXPIRED, CODE_SUCCESS } from "../config/returnCodeMap";
import { ACCESS_TOKEN, AUTH } from "../config/constant";
const service = axios.create({
baseURL: "//127.0.0.1:4000",
timeout: 30000,
});
service.interceptors.request.use((config) => {
let { headers } = config;
const s_tk = localStorage.getItem(ACCESS_TOKEN);
s_tk && Object.assign(headers, { [AUTH]: s_tk });
return config;
}, (error) => {
return Promise.reject(error);
});
service.interceptors.response.use((response) => {
let { config, data } = response;
// retry: 第一次请求过期,接口调用refreshAccessToken,第二次重新请求,还是过期则reject出去
let { retry } = config;
return new Promise((resolve, reject) => {
if (data["returncode"] !== CODE_SUCCESS) {
if ([CODE_LOGGED_OTHER, CODE_RELOGIN].includes(data.returncode)) {
clearAuthAndRedirect();
} else if (data["returncode"] === CODE_TOKEN_EXPIRED && !retry) {
config.retry = true;
addSubscriber(() => resolve(service(config)));
refreshAccessToken();
} else {
return reject(data);
}
} else {
resolve(data);
}
});
}, (error) => {
return Promise.reject(error);
});
export default service;
4. 刷新Token逻辑
实现刷新Token的逻辑,并处理多个请求同时触发Token刷新的情况。
// service/refresh.js
let pending = false;
let subscribers = [];
const onAccessTokenFetched = (accessToken) => {
subscribers.forEach(callback => callback(accessToken));
subscribers = [];
};
const addSubscriber = (callback) => {
subscribers.push(callback);
};
export const refreshAccessToken = async () => {
if (!pending) {
try {
pending = true;
const l_tk = localStorage.getItem(REFRESH_TOKEN);
if (l_tk) {
// 重新获取短token
const { accessToken } = await service.get("/refresh", Object.assign({}, { headers: { [PASS]: l_tk } }));
localStorage.setItem(ACCESS_TOKEN, accessToken);
onAccessTokenFetched(accessToken);
}
} catch (e) {
clearAuthAndRedirect();
} finally {
pending = false;
}
}
};
export { addSubscriber };
5. 清除Token并重定向到登录页
当Token过期或被其他客户端登录时,清除Token并重定向到登录页。
// service/clear.js
import { ACCESS_TOKEN } from '../config/constant';
export const clearAuthAndRedirect = () => {
localStorage.removeItem(ACCESS_TOKEN);
window.location.href = '/login';
};
Token的无感刷新技术通过合理利用双Token机制,实现了在用户不感知的情况下自动刷新访问令牌,从而提升了用户体验并减少了因Token过期导致的重复登录问题。