一、 踩的坑
在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);
})