为了安全考虑,一般后端都要求在请求接口时传递登录的 Token。为了防止 Token 泄漏的风险,服务器的 Token 一般不会设置太长时间,比如我最近联调的,两个小时就会过期,token 过期就需要重新登陆。频繁要求登陆会造成用户体验差,所以后端同时会提供刷新 Token 的接口,本文就是记录一下几种无感知刷新的方法。
方案一
在登录时,后端返回过期时间,前端每次请求就判断 token 的过期时间,如果快到过期时间,就去调用刷新 token 接口,我们可以封装一个 refreshToken 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| const refreshToken = async () => { if (dayjs().diff(LOCAL_REFRESH_TIME > LOCAL_EXPIRE) { if (global.workPromise) { return global.workPromise } global.workPromise = new Promise(async (resolve) => { const {data} = await request({ url: `https://api.com/login`, method: 'POST', }); global.workPromise = null; resolve() }) return global.workPromise } return Access_Token }
|
该方法主要原理是通过将 refresh 接口请求存在全局的 workPromise 中来保证在并发请求接口时只存在一个请求。
方案二
登录时设置定时器刷新 token 接口,请求的时候判断当前是否有 workPromise 存在,如果存在就等刷新完成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| setInterval(() => { global.workPromise = new Promise(async (resolve) => { const {data} = await request({ url: `https://api.com/login`, method: 'POST', }); cookies.set('refresh_time') global.workPromise = null; resolve() }) }, EXPIRE / 2)
const request = () => { if (global.workPromise) { await global.workPromise() } }
|
该方案由于有定时器一直存在,会额外消耗资源,不推荐使用。
方案三
如果使用了 axios,可以在请求响应拦截器中拦截,判断 token 返回过期后,调用刷新 token 接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| import axios from 'axios'
let isRefreshing = false
let requests = [] service.interceptors.response.use( response => { if (response.data.code === 409) { if (!isRefreshing) { isRefreshing = true return refreshToken().then(res => { const { token } = res.data setToken(token) response.headers.Authorization = `${token}` requests.forEach((cb) => cb(token)) requests = [] return service(response.config) }).catch(err => { removeToken() router.push('/login') return Promise.reject(err) }).finally(() => { isRefreshing = false }) } else { return new Promise(resolve => { requests.push(token => { response.headers.Authorization = `${token}` resolve(service(response.config)) }) }) } } return response && response.data }, (error) => { return Promise.reject(error) } )
|