前言
最近用户一直抱怨,用户操作我们平台,好好的突然就回到登录页面了,需要用户重新登录,这个就是因为token 失效了,导致后端验证不通过,返回401权限问题。 后端的实现是JWT Token机制,如果不清楚这个机制,可以看一下我前面发的一篇文章 JWT的Token认证机制:juejin.cn/post/703785…
解决方案
前端如何实现无感知刷新token:思路是 每一个接口请求的时候去拦截,然后判断当前用户的token时效,如果小于一半的时间,就自动调用刷新token 的接口。如果后端设计的是如果token过期还能去刷新token那其实只要判断过期之后再去请求,我们的后端定的是token过期是不能换取token,最终一起商量就是小于有效时间一半就刷新token
我这个项目是多页面,每个页面都存在 请求代理axios 和fetch,甚至原生的请求都有,所以我把请求拦截的方法单独一个文件,然后所以每个页面模块的main.js 都引入了proxy.js 文件,两种方式我都用代码列出来
proxy.js
import moment from 'moment';
import Cookies from 'js-cookie';
let $requestRepeatRenewalToken = false; // 是否已经调用了刷新token的接口
const TokenKey = 'h3_token';
// 设置cookie
setCookie2 (name: string, value: string, hours = 8) {
const d = new Date();
d.setTime(d.getTime() + hours * 60 * 60 * 1000);
Cookies.set(name, value, {
expires: d,
});
}
// 续期token
export const renewalToken = async (token) => {
const content = parseToken(token);
const expired = content.exp * 1000; // s -> ms
if (expired) {
const expiredTime = moment(expired);
const currentTime = moment();
const hours = expiredTime.diff(currentTime, 'hours');
if (hours < 4) { // 剩余时间小于4小时的时候续期
const res: any = await RenewalToken(token);
console.log('RenewalToken resp: ', res);
setTimeout(() => { // 防止刷新页面的时候 调用多次,所以设置定时器
$requestRepeatRenewalToken = false;
}, 5000);
if (res && res.errcode == 0) {
// 续期成功,刷新cookie
const token = res.result.token;
setCookie2(TokenKey, token);
} else {
console.error(res.errmsg);
}
}
}
};
// 续期token 判断逻辑
export const fliterRenewalTokenHandle = (innerArgs) => {
// 每次接口请求需要去判断token过期时间,如果小于4小时,需要续期token,注意需要过滤续期的接口,否则会一直调用续期的接口
try {
const renewalTokenRequestUrl = 'xxx';// 刷新token 接口
if (innerArgs && innerArgs[0] && innerArgs[0].currentTarget && innerArgs[0].currentTarget.responseURL){
if (innerArgs[0].currentTarget.responseURL.indexOf(renewalTokenRequestUrl) === -1){
const token = getToken();
renewalToken(token);
}
}
} catch (e){
console.log('处理续期接口异常');
}
};
const proxyAjax = () => {
if (!XMLHttpRequest) {
return;
}
const nativeAjaxSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function (...args) {
const oldCb = this.onreadystatechange;
const token = getToken();
if (token) {
this.setRequestHeader('authorization', `Bearer ${token}`);
}
this.onreadystatechange = (...innerArgs) => {
if (!$requestRepeatRenewalToken) { // 这里如果采用防抖处理,多个并行发送请求 接口还是会导致多次调用,所以用变量
$requestRepeatRenewalToken = true;
fliterRenewalTokenHandle(innerArgs);
}
if (this.readyState === 4) {
switch (this.status) {
case 401:
// token 失效, 清除cookie中token, 跳转到登录页
// console.log('401 found');
clearAuthorization();
//TODO
break;
default:
const authorization = this.getResponseHeader('Authorization');
// 判断是否有更新的token
if (authorization) {
setCookie2(TokenKey, authorization.replace('Bearer ', ''));
}
// const h3Token = getCookie(TokenKey);
// h3Token && renewalToken(h3Token);
break;
}
}
oldCb && oldCb.apply(this, innerArgs);
};
return nativeAjaxSend.apply(this, args);
};
};
export default proxyAjax();
或者axios 请求拦截
axios.interceptors.request.use((config) => {
config.headers.authorization=`Bearer ${token}`;
// to do 上面相关代码
if (!$requestRepeatRenewalToken) { // 这里如果采用防抖处理,多个并行发送请求 接口还是会导致多次调用,所以用变量
$requestRepeatRenewalToken = true;
fliterRenewalTokenHandle(innerArgs);
}
……
}, error => {
Promise.reject(error);
});
结语
其实成熟的方案是双token机制,即access token 和 refresh token。
access token 是临时的,有一定有效期。这是因为,access token 在使用的过程中可能会泄露。给 access token 限定一个较短的有效期可以降低因 access token 泄露而带来的风险。
refresh token 一定要保存在使用方的服务器上,而绝不能存放在移动 app、PC端软件、浏览器上,也不能在网络上随便传递。
过期时间: refresh token > access token 一般是2倍以上
简单来说前端调用接口是用 access token,后端去判断 access token如果过期了,即给前端access token 重置一个值,同时后端refresh token 也要重置,如果access token和refresh token都过期了就需要用户重新登录或鉴权…… 这种处理token的机制完全是后端处理。