没有记笔记的习惯,摸鱼无聊,随便写写吧。o( ̄︶ ̄)o
前提
很多项目都有前端页面长时间展示不自动退出的需求。比如大屏可视化项目
问题
目前很多项目都使用token鉴权,为了不自动退出系统,需要保持token可持续性
解决方案
我想到的解决方案有以下几种:
- 后端服务设置token永无过期,由于从安全考虑,token长期存在是有很大隐患,不可取的
- 后端服务设置,当每次请求接口时,更新token的过期时间
- 前端无感刷新token,即前端在请求过程中碰到后端返回token过期的标识,则请求刷新token接口,更新token后重新请求失败的接口。
第2种方案有后端实现,需要把token与过期时间分开,与前端无关
下面来讲讲前端怎么实现第3种方案
无感刷新token
当登录或刷新token时,后端都会返回两个值:token 和 refreshToken
实现流程
- 在请求开始时把请求条件(URL,入参,请求方式等)临时存储
- 请求返回:正确,则正常返回,释放请求条件临时存储
- 请求返回:错误且错误码是token过期,则把这个请求执行函数放入订阅中,然后请求refreshToken API 刷新token的请求
- 请求request时,当前如果是正在刷新token,则请求执行函数放入订阅中
- 刷新token的请求返回后,替换新的token,触发发布所有订阅中的请求函数,达到无感刷新token
token的发布订阅:tokenDep.js
let isTokenRefreshing = false // token 刷新中
let subscriberList = [] // 需要回调的函数列表
let options = {} // 每次请求时都记录下参数,在请求返回时释放,如果token过期,则使用它进行在请求
function setOptions(key, value) {
options[key] = value
}
function getOptions(key) {
return options[key]
}
function removeOptions(key) {
delete options[key]
}
function setTokenRefreshing(flag) {
isTokenRefreshing = flag
}
function getTokenRefreshing() {
return isTokenRefreshing
}
// 添加订阅
function addSubcriber(cb) {
subscriberList.push(cb)
}
// 发布订阅
function tiggerSubcriber() {
while(subscriberList.length) {
const cb = subscriberList.shift()
cb()
}
clearSubcriber()
}
// 清空
function clearSubcriber() {
isTokenRefreshing = false
subscriberList = []
options = {}
}
export default {
setTokenRefreshing,
getTokenRefreshing,
addSubcriber,
tiggerSubcriber,
clearSubcriber,
setOptions,
getOptions,
removeOptions
}
http请求: request.js
import axios from 'axios'
import { Message } from 'element-ui'
import store from '@/store'
import { getToken, getRefreshToken } from './auth'
import settings from '@/settings'
import TokenDep from './tokenDep'
import { refreshTokenApi } from '@/api/auth'
// 以下接口不进行刷新token
const noRefreshTokenAPI = ['/api/refreshToken', '/api/logout', '/api/login']
// create an axios instance
const service = axios.create({
baseURL: '/api', // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests
timeout: 50000 // request timeout
})
// request interceptor
service.interceptors.request.use(
config => {
// 当在刷新token时,进行阻塞
if (TokenDep.getTokenRefreshing() && !noRefreshTokenAPI.includes(config.url)) {
let retry = new Promise((resolve) => {
TokenDep.addSubcriber(() => {
config.headers['token'] = getToken()
resolve(config)
})
})
return retry
}
config.headers['token'] = getToken()
return config
},
error => {
// do something with request error
console.log('Error on request', error) // for debug
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
const toRefreshToken = (request_unique) => {
// 当token过期且不在刷新token时,进行刷新token
if (!TokenDep.getTokenRefreshing()) {
TokenDep.setTokenRefreshing(true)
console.log('刷新token-----begin')
refreshTokenApi({ refreshToken: getRefreshToken() }).then(res => {
store.dispatch('user/refreshToken', res.data).then(() => {
TokenDep.setTokenRefreshing(false)
console.log('刷新token后,触发回调-----end')
TokenDep.tiggerSubcriber()
})
}).catch(() => {
TokenDep.setTokenRefreshing(false)
})
}
let retry = new Promise((resolve) => {
TokenDep.addSubcriber(() => {
const options = TokenDep.getOptions(request_unique)
TokenDep.removeOptions(request_unique)
// options 是从开始传进来的
resolve(request(options))
})
})
return retry
}
const toReLogin = code => {
/*
40001: 会话超时,请重新登录(进行了刷新token)
40002: "不合法的token,请认真比对 token 的签名
40003, "缺少token参数"
40005, "解析用户身份错误,请重新登录!
40008, "您已在另一个设备登录
40009, "登录超时,请重新登录
*/
if ([40002, 40003, 40005, 40008, 40009].includes(code)) {
TokenDep.clearSubcriber()
// 返回登录页面
store.dispatch('user/resetToken').then(() => {
setTimeout(() => {
if (location.href.indexOf('/login') === -1) {
localStorage.removeItem('haveTips');
location.reload()
}
}, 1500)
})
}
}
// response interceptor
service.interceptors.response.use(
response => {
const config = response.config
const request_unique = config.headers['request_unique']
const res = response.data
if (res.code !== 0) {
// code === 40001 为会话超时,进行刷新token
if (!noRefreshTokenAPI.includes(config.url) && (TokenDep.getTokenRefreshing() || res.code === 40001)) {
return toRefreshToken(request_unique)
}
Message({
message: res.msg || '服务器出错,请稍后重试',
type: 'error',
duration: 5 * 1000
})
// 返回登录页面
toReLogin(res.code)
return Promise.reject(new Error(res.msg || '服务器出错,请稍后重试'))
} else {
TokenDep.removeOptions(request_unique)
return res
}
},
error => {
console.log('Error on response', error.response) // for debug
if (error.response) {
const { data: res, config } = error.response
const request_unique = config.headers['request_unique']
// code === 40001 为会话超时,进行刷新token
if (!noRefreshTokenAPI.includes(config.url) && (TokenDep.getTokenRefreshing() || res.code === 40001)) {
return toRefreshToken(request_unique)
}
// 返回登录页面
toReLogin(res.code)
Message({
message: res.msg || '服务器出错,请稍后重试',
type: 'error',
duration: 5 * 1000
})
} else {
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
}
return Promise.reject(error)
}
)
const request = (options) => {
// 唯一值为key存储请求前传入的参数
const request_unique = Symbol()
TokenDep.setOptions(request_unique, options)
if (!options.headers) {
options.headers = {}
}
options.headers['request_unique'] = request_unique
return service(options)
}
export default request
总结
无感刷新token其实就是当请求后端返回token过期,前端不报错,会把当前请求存储起来,先去调用刷新token接口,返回token后更新本地token,再从存储队列里取出请求重新发起请求