使用axios实现无感刷新token,并总结经验
一、 踩的坑
在token过期后,需要刷新新的token,要是再这时同时发起多次请求,会出现token多次重复刷新问题。
解决这个总结一句话就是等待刷新token请求回来在进行接口请求。具体操作下边代码会有。
因为一开始网上找的资料直接点出这个问题,所以在基本写完收一脚踩进去了。
第二个坑就是js的隐式转换,以前不常用sessionStorage所以没注意。
这里重点说一下,存上在取出来数据类型都是字符串类型存数组对象得转成字符串存储,存数字在取出来,数字会变成字符串
二、 用到的知识点
1.sessionStorage
2.axios拦截器interceptors
三、后端登录后给的信息
token(token令牌)
refresh_token(这个是用于刷新token的令牌)
resetTime(这个是token过期的时间)
还有一个refresh_Time(refresh_token过期的时间)可能有也可能没有
还有就是刷新token的请求接口
四、逻辑初步讲解
1.token的操作逻辑
// 获取token信息
export function getToken() {
let token=sessionStorage.getItem("token")
if(token){//有token信息
return token
}else{//没有登陆信息,返回 '' 前端不判断
return ''
}
}
// 存储 token 信息
export function tokenLogin(LToken) {
//用当前时间戳加上过期的毫秒数,就是到期日期,(到时候判断有没有过期用当前时间和到期时间>/<或比较就行)
let resetTime=Date.now()+(Number(LToken.resetTime))
//将登录的三个信息通过sessionStorage存储起来,
}
// 删除 token信息
export function DelToken() {
//删除tokenLogin存在sessionStorage的信息
}
// 删除 用户信息
export function DelUserInfo() {
//用户信息删除
}
// token 失效刷新 token
export async function refreshToken() {
try{
let refresh_token=sessionStorage.getItem("refresh_token")
// 使用 ref_token 刷新token
//这里的请求是直接新引入的axios这样就脱离了下方写的拦截器,不受下方逻辑拦截,当然要是不脱离写还要再拦截器里再判断一回,就是对刷新token请求进行直接放行
let result=await axios.post(url.baseUrl+'...',{refresh:refresh_token})
if(result.data.access){
// 获取成功,存储token信息 return token
tokenLogin({...})
return result.data.access
}else{
//获取失败 删除token信息和身份信息 userInfo/buserInfo
DelToken()
return ''
}
}catch (error) {
//获取失败 删除token信息和身份信息
DelToken()
return ''
}
}
2.axios中的逻辑,这里主要说一下同时请求多次刷新token的问题,具体问题开头写了
/* 被挂起的请求数组 */
let refreshSubscribers = []
/* push所有请求到数组中 */
function subscribeTokenRefresh(cb) {
refreshSubscribers.push(cb)
}
//这个是标识 现在是否正在刷新
let isRefreshing=false
/* 刷新请求(refreshSubscribers数组中的请求得到新的token之后会自执行,用新的token去请求数据) */
function onRrefreshed(access) {
refreshSubscribers.map(cb => cb(access))
}
//请求拦截
http.interceptors.request.use(async config=>{//这里面的代码不要改变我写的顺序
//将所有请求都存起来,挂起
let retry = new Promise((resolve, reject) => {
/* (token) => {...}这个函数就是回调函数 */
subscribeTokenRefresh((access) => {
config.headers.Authorization = 'Bearer '+ access
/* 将请求挂起 */
resolve(config)
})
})
let resetTime=sessionStorage.getItem("resetTime")//超期时间
let currentTime=Date.now()//当前时间
if(currentTime>Number(resetTime)){//token超期
if(!isRefreshing){//当前没有请求接口没有在发起刷新token请求
isRefreshing = true;//改变标识状态正在刷新
onRrefreshed(await refreshToken());//得到token结果,将存起来的请求进行发送请求
isRefreshing = false;//刷新完成改变状态
}
}else{//token未超期,直接返回token不改变
onRrefreshed(getToken());
}
return retry
})
五、编写源码
1.operateToken.js
import axios from 'axios' //引用
import url from "./api";
import store from '../store/index'
// import Router from '../router' //路由引用,若是在文件内有涉及,就要进行引用
// 获取token信息
export function getToken() {
let token=sessionStorage.getItem("token")
if(token){//有token信息
return token
}else{//没有登陆信息,返回 '' 前端不判断
return ''
}
}
// 存储 token 信息
export function tokenLogin(LToken) {
sessionStorage.setItem('token',LToken.token)
sessionStorage.setItem('refresh_token',LToken.refresh_token)
// 刷新时间存储处理成过期时间戳,到时候比对那当前时间进行对比
let resetTime=Date.now()+(Number(LToken.resetTime))
// let resetTime=Date.now()+(Number(LToken.resetTime)-120000)
sessionStorage.setItem('resetTime',resetTime)
sessionStorage.setItem('access_time',LToken.resetTime)
}
// 删除 token信息
export function DelToken() {
sessionStorage.removeItem("token");
sessionStorage.removeItem("refresh_token");
sessionStorage.removeItem("resetTime");
sessionStorage.removeItem("access_time");
}
// 删除 用户信息
export function DelUserInfo() {
store.dispatch('DelAuserInfo')
store.dispatch('DelABuserInfo')
sessionStorage.removeItem('logi');
sessionStorage.removeItem('bkL');
}
// token 失效刷新 token
export async function refreshToken() {
try{
let refresh_token=sessionStorage.getItem("refresh_token")
// 使用 ref_token 刷新token
let result=await axios.post(url.baseUrl+'/api/token/refresh/',{refresh:refresh_token})
// console.log(result,222);
if(result.data.access){
// 获取成功,存储token信息 return token
tokenLogin({
token:result.data.access,
resetTime:sessionStorage.getItem("access_time"),
refresh_token:sessionStorage.getItem("refresh_token"),
})
return result.data.access
}else{
//获取失败 删除token信息和身份信息 userInfo/buserInfo
DelToken()
return ''
}
}catch (error) {
//获取失败 删除token信息和身份信息
DelToken()
return ''
}
}
2.axiosConfig.js
import axios from 'axios' //引用
import { getToken,DelToken,DelUserInfo,refreshToken } from "./operateToken";
let http=axios.create();
http.defaults.withCredentials = true; // 允许携带cookie
/* 被挂起的请求数组 */
let refreshSubscribers = []
/* push所有请求到数组中 */
function subscribeTokenRefresh(cb) {
refreshSubscribers.push(cb)
}
let isRefreshing=false
/* 刷新请求(refreshSubscribers数组中的请求得到新的token之后会自执行,用新的token去请求数据) */
function onRrefreshed(access) {
refreshSubscribers.map(cb => cb(access))
}
//请求拦截
http.interceptors.request.use(async config=>{
// console.log(config,'----------发起');
let retry = new Promise((resolve, reject) => {
/* (token) => {...}这个函数就是回调函数 */
subscribeTokenRefresh((access) => {
config.headers.Authorization = 'Bearer '+ access
/* 将请求挂起 */
resolve(config)
})
})
let resetTime=sessionStorage.getItem("resetTime")
let currentTime=Date.now()
if(currentTime>Number(resetTime)){//token超期
if(!isRefreshing){
isRefreshing = true;
onRrefreshed(await refreshToken());
isRefreshing = false;
}
}else{//token未超期,直接返回token不改变
onRrefreshed(getToken());
}
return retry
})
//响应拦截
http.interceptors.response.use(response=>{
// console.log(response,'----------返回');
return response
},error=>{
// console.log(error,'----error------返回');
let response = error.response;
const status = response.status;
if (status === 401) {
// 判断状态码是401 跳转到登录
DelToken()
DelUserInfo()
alert(error.message)
location.reload();
}
return Promise.reject(error)
})
//抛出模块
export default http
五、参考网上的案例
/* 是否有请求正在刷新token */
window.isRefreshing = false
/* 被挂起的请求数组 */
let refreshSubscribers = []
/* push所有请求到数组中 */
function subscribeTokenRefresh(cb) {
refreshSubscribers.push(cb)
}
/* 刷新请求(refreshSubscribers数组中的请求得到新的token之后会自执行,用新的token去请求数据) */
function onRrefreshed(token) {
refreshSubscribers.map(cb => cb(token))
}
const fetch = axios.create({
baseURL: '',
timeout: '30000'
})
function computedTime() {
let r = getRefresh();
if (!r || !r.expires_in) return false;
let currentTime = Date.parse(new Date()) / 1000;
let expiresTime = r.expires_in;
// 600秒后即将过期,true则不需要刷新
return expiresTime - currentTime <= 600
}
fetch.interceptors.request.use(async (config) => {
if (config.url !== '/oauth/token') {//获取token的接口不进行拦截
getToken() && (config.headers.Authorization = getToken());
if (computedTime()) {
if (!window.isRefreshing) {
window.isRefreshing = true;
let r = getRefresh();
if (!r) return;
let refreshData = {
grant_type: 'refresh_token',
client_id: r.client_id,
client_secret: r.client_secret,
refresh_token: r.refresh_token
}
getTokens(refreshData).then((data) => {
window.isRefreshing = false;
let rData = {
client_id: r.client_id,
client_secret: r.client_secret,
expires_in: (Date.parse(new Date()) / 1000 + data.expires_in),
grant_type: 'refresh_token',
org_id: r.org_id,
refresh_token: r.refresh_token
}
// 存储token,存进cookie里面
store.commit('setTokenInfo', data.token_type + data.access_token);
// 存储refresh_token
store.commit('setRefreshToken', rData);
onRrefreshed(data.token_type + data.access_token);
/* 执行onRefreshed函数后清空数组中保存的请求 */
refreshSubscribers = [];
}).catch(err => {
console.log(err);
router.replace({
path: '/login'
})
})
}
/* 把请求(token)=>{....}都push到一个数组中 */
let retry = new Promise((resolve, reject) => {
/* (token) => {...}这个函数就是回调函数 */
subscribeTokenRefresh((token) => {
config.headers.Authorization = token
/* 将请求挂起 */
resolve(config)
})
})
return retry
} else {
return config
}
} else {
return config
}
}, (error) => {
return Promise.reject(error);
})