1.背景
由于业务需求,需要对用户信息进行安全性配置,就出现了短token--accessToken,那么短token通常几十分钟就会失效,降低了用户体验,此时后端又返回了一个长token--refreshToken,用来刷新accessToken,提高用户体感,减少用户登录的次数,达到无感刷新的境界。
2.编写逻辑
登录过后,后端会返回来一个accessToken,refreshToken。我们将这两个token已合适的方式存储起来,accessToken通常设置为请求头header,作为接口鉴权所用,当用户发起请求的时候,后端发现此时的accessToken已经过期了就返回401状态码(状态码看后端程序设定,针对不同做处理),我们就拿refreshToken作为请求参数,调用刷新token令牌的接口,刷新完token后,会返回新的accessToken,这时候要替换掉header中旧的那个accessToken,然后再发起之前的原始请求。此时就达到了双token之间的替换以及无感刷新。
3.具体步骤
3.1 定义一个处理AccessToken失效的函数
async function handleAccessTokenError(originalRequest) {
if(!isRefreshing){
isRefreshing = true;
try {
const refreshToken = getRefreshToken();
const resp = await axios.post('/coolfly/app/user/refresh-token', { refreshToken });
if (resp.code === 0) {
setAccessToken(resp.data?.accessToken);
onRefreshed(resp.data?.accessToken);
if (originalRequest) {
originalRequest.headers['Hc-Authorization'] = resp.data?.accessToken;
return axios(originalRequest);
}
} else if (resp.code === 5) { // RefreshToken 未授权
removeToken();
navigateToLogin();
}
} catch (refreshError) {
console.error('Refresh token failed:', refreshError);
removeToken();
navigateToLogin();
} finally {
isRefreshing = false;
}
}
//重新拿到accessToken后,重新发起请求
const retryOriginalRequest = new Promise((resolve) => {
addRefreshSubscriber((token) => {
originalRequest.headers['Hc-Authorization'] = token;
resolve(service(originalRequest));
});
})
return retryOriginalRequest;
}
- isRefreshing:用来避免在刷新令牌的过程中发起多个请求。
- RefreshToken流程:
- 检查
RefreshToken
是否存在。 - 如果存在,使用它来请求新的
AccessToken
。 - 成功获取新令牌后,更新存储的
AccessToken
并重新发送原始请求。 - 如果
RefreshToken
也失效,移除令牌并重定向到登录页面。
- 检查
3.2 辅助函数编写,提高代码可读性
function onRefreshed(token) {
refreshSubscribers.map((callback) => callback(token));
refreshSubscribers = [];
}
function addRefreshSubscriber(callback) {
refreshSubscribers.push(callback);
}
function hintMsg(msg) {
const translatedMessage = i18n.t(msg);
message.info(translatedMessage);
}
function navigateToLogin() {
window.location.replace('/#');
}
详细解释:
- onRefreshed:用于通知所有等待中的请求刷新后的新令牌。
- addRefreshSubscriber:将请求加入等待队列,以便在令牌刷新后继续处理。
- hintMsg:用于显示国际化提示消息。
- navigateToLogin:用于跳转到登录页面。
3.3 request源文件代码如下(去掉一些无关紧要的业务代码):
import axios from "axios";
import { message } from "antd";
import Config from "@/settings.js";
import { getUserInfo, getAccessToken, setAccessToken, getRefreshToken, removeToken } from "@/utils/auth.js";
import store from "@/store";
import { SetUserInfo } from "@/store/modules/auth/action.js";
import { encrypt } from '@/utils/cryptojs.js'
import i18n from 'i18next';
let isRefreshing = false;
let refreshSubscribers = [];
function onRefreshed(token) {
refreshSubscribers.map((callback) => callback(token));
refreshSubscribers = [];
}
function addRefreshSubscriber(callback) {
refreshSubscribers.push(callback);
}
function hintMsg(msg) {
const translatedMessage = i18n.t(msg);
message.info(translatedMessage);
}
// 创建axios实例
const service = axios.create({
baseURL: '/api', // api 的 base_url
// baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: Config.timeout, // 请求超时时间
});
// request拦截器
service.interceptors.request.use(
(config) => {
// 设置通用请求头
config.headers['Content-Type'] = 'application/json';
// 根据token设置Authorization头
// const { accessToken } = getUserInfo()
if (getAccessToken()) {
const userInfo = getUserInfo();
config.headers['Authorization'] = getAccessToken() || userInfo.accessToken;
config.headers['Uid'] = userInfo.uid;
}
//加密请求 需要加密的接口路径
const encryptedApis = [
'/api/app/user/login',
'/api/app/user/refresh-token',
'/api/app/user/reset-pwd',
'/api/app/user/register',
'/api/app/auth/aws-s3/upload', //文件上传
'/api/app/auth/aws-s3/get/ps', //文件预签名
];
if (encryptedApis.includes(config.url)) {
config.headers['Content-Type'] = 'text/plain;charset=UTF-8.';
if (config.data) {
config.data = encrypt(config.data);
}
}
return config;
}
);
// response 拦截器
service.interceptors.response.use(
(response) => {
const {data, status } = response;
const { code, message} = data;
if (code === 0 && status === 200) {
return data;
}else {
hintMsg(message);
handleErrorCode(code, message, response.config)
return Promise.reject(data);
}
},
async (error) => {
const responseCode = error.response?.status || error.response?.data;
if (responseCode !== undefined) {
await handleErrorCode(responseCode, error.response?.message, error.config);
} else {
hintMsg("serverError");
}
return Promise.reject(error);
}
);
async function handleErrorCode(code, message, originalRequest = null) {
switch (code) {
case 1: // AccessToken 未授权
// hintMsg("UserError");
return await handleAccessTokenError(originalRequest);
case 5: // RefreshToken 未授权
message.error(message)
removeToken();
navigateToLogin();
break;
case 98: // 服务器繁忙
hintMsg("serverError");
break;
case 99: // 服务器内部错误
store.dispatch(SetUserInfo(null));
removeToken();
message.error(message)
hintMsg("serverError");
break;
default:
hintMsg(message || "serverError");
break;
}
}
async function handleAccessTokenError(originalRequest) {
if(!isRefreshing){
isRefreshing = true;
try {
const refreshToken = getRefreshToken();
const resp = await axios.post('/api/app/user/refresh-token', { refreshToken });
if (resp.code === 0) {
setAccessToken(resp.data?.accessToken);
onRefreshed(resp.data?.accessToken);
if (originalRequest) {
originalRequest.headers['Authorization'] = resp.data?.accessToken;
return axios(originalRequest);
}
} else if (resp.code === 5) { // RefreshToken 未授权
removeToken();
navigateToLogin();
}
} catch (refreshError) {
console.error('Refresh token failed:', refreshError);
removeToken();
navigateToLogin();
} finally {
isRefreshing = false;
}
}
//重新拿到accessToken后,重新发起请求
const retryOriginalRequest = new Promise((resolve) => {
addRefreshSubscriber((token) => {
originalRequest.headers['Authorization'] = token;
resolve(service(originalRequest));
});
})
return retryOriginalRequest;
}
function navigateToLogin() {
window.location.replace('/#')
}
export default service;
总结
通过以上步骤,我们实现了一个双Token验证的流程。该流程包括AccessToken
和RefreshToken
的管理、请求拦截和响应处理,以及错误处理和提示。这样的配置能够有效提升应用的安全性和用户体验。