黑马程序员技术交流社区

标题: 【成都校区】同时多个axios请求怎么实现无痛刷新token [打印本页]

作者: 小蜀哥哥    时间: 2019-11-28 17:32
标题: 【成都校区】同时多个axios请求怎么实现无痛刷新token
需求最近遇到个需求:前端登录后,后端返回token和refresh_token,当token过期时用旧refresh_token去获取新的token,前端需要做到无痛刷新token,即请求刷新token时要做到用户无感知。
需求解析当用户发起一个请求时,判断token是否已过期,若已过期则先调refreshToken接口,拿到新的token后再继续执行之前的请求。
这个问题的难点在于:当同时发起多个请求,而刷新token的接口还没返回,此时其他请求该如何处理?接下来会循序渐进地分享一下整个过程。
实现这里会使用axios来实现,以上方法是请求后拦截,所以会使用axios.interceptors.response.use()方法。
首先说明一下,项目中的token是存在localStorage中的。
如何防止多次刷新token如果refreshToken接口还没返回,此时再有一个过期的请求进来,上面的代码就会再一次执行refreshToken,这就会导致多次执行刷新token的接口,因此需要防止这个问题。我们可以在request.js中用一个flag来标记当前是否正在刷新token的状态,如果正在刷新则不再调用刷新token的接口。
这样子就可以避免在刷新token时再进入方法了。但是这种做法是相当于把其他失败的接口给舍弃了,假如同时发起两个请求,且几乎同时返回,第一个请求肯定是进入了refreshToken后再重试,而第二个请求则被丢弃了,仍是返回失败,所以接下来还得解决其他接口的重试问题。
同时发起两个或以上的请求时,其他接口如何重试两个接口几乎同时发起和返回,第一个接口会进入刷新token后重试的流程,而第二个接口需要先存起来,然后等刷新token后再重试。同样,如果同时发起三个请求,此时需要缓存后两个接口,等刷新token后再重试。由于接口都是异步的,处理起来会有点麻烦。
当第二个过期的请求进来,token正在刷新,我们先将这个请求存到一个数组队列中,想办法让这个请求处于等待中,一直等到刷新token后再逐个重试清空请求队列。那么如何做到让这个请求处于等待中呢?为了解决这个问题,我们得借助Promise。将请求存进队列中后,同时返回一个Promise,让这个Promise一直处于Pending状态(即不调用resolve),此时这个请求就会一直等啊等,只要我们不执行resolve,这个请求就会一直在等待。当刷新请求的接口返回来后,我们再调用resolve,逐个重试。最终代码:

[JavaScript] 纯文本查看 复制代码
import axios from 'axios'
import { Loading, Message, MessageBox } from 'element-ui'
import api from './api'
import { getToken, setToken, removeToken, getRefreshToken } from '../utils/cookies'

let UserModule = {
        RefreshToken: (data) => {
                setToken('Bearer ' + data.access_token, data.refresh_token)
        }
}

// 是否正在刷新的标记
let isRefreshing = false

// 重试队列,每一项将是一个待执行的函数形式
let retryRequests = []

const request = axios.create({
        baseURL: api.baseUrl,
        timeout: 50000,
        withCredentials: true // cookie跨域必备
})
// http request 拦截器 Request
request.interceptors.request.use(
        (config) => {
                if (getToken()) {
                        config.headers['Authorization'] = getToken()
                }
                return config
        },
        (error) => {
                Promise.reject(error)
        }
)

// http response 拦截器 Response
request.interceptors.response.use(
        (response) => {
                // code == 0: 成功
                const res = response.data
                if (res.code !== 0) {
                        if (res.message) {
                                Message({
                                        message: res.message,
                                        type: 'error',
                                        duration: 5 * 1000
                                })
                        }
                        return Promise.reject(res)
                } else {
                        return response.data
                }
        },
        (error) => {
                if (!error.response) return Promise.reject(error)
                // 根据refreshtoken重新获取token
                // 5000系统繁忙
                // 5001参数错误
                // 1003该用户权限不足以访问该资源接口
                // 1004访问此资源需要完全的身份验证
                // 1001access_token无效
                // 1002refresh_token无效
                if (error.response.data.code === 1004 || error.response.data.code === 1001) {
                        const config = error.config
                        if (!isRefreshing) {
                                isRefreshing = true
                                return getRefreshTokenFunc()
                                        .then((res) => {
                                                // 重新设置token
                                                UserModule.RefreshToken(res.data.data)
                                                config.headers['Authorization'] = getToken()
                                                // 已经刷新了token,将所有队列中的请求进行重试
                                                // @ts-ignore
                                                retryRequests.forEach((cb) => cb(getToken()))
                                                // 重试完清空这个队列
                                                retryRequests = []
                                                // 这边不需要baseURL是因为会重新请求url,url中已经包含baseURL的部分了
                                                config.baseURL = ''
                                                return request(config)
                                        })
                                        .catch(() => {
                                                resetLogin()
                                        })
                                        .finally(() => {
                                                isRefreshing = false
                                        })
                        } else {
                                // 正在刷新token,返回一个未执行resolve的promise
                                return new Promise((resolve) => {
                                        // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
                                        // @ts-ignore
                                        retryRequests.push((token: any) => {
                                                config.baseURL = ''
                                                config.headers['Authorization'] = token
                                                resolve(request(config))
                                        })
                                })
                        }
                } else if (error.response.data.code === 1002) {
                        resetLogin()
                } else {
                        Message({
                                message: error.response.data.message,
                                type: 'error',
                                duration: 5 * 1000
                        })
                        return Promise.reject(error)
                }
        }
)
// 刷新token的请求方法
function getRefreshTokenFunc() {
        let params = {
                refresh_token: getRefreshToken() || ''
        }
        return axios.post(api.baseUrl + 'auth-center/auth/refresh_token', params)
}
function resetLogin(title = '身份验证失败,请重新登录!') {
        if (window.location.href.indexOf('/login') === -1) {
                MessageBox.confirm(title, '退出', {
                        confirmButtonText: '重新登录',
                        cancelButtonText: '取消',
                        type: 'warning'
                }).then(() => {
                        removeToken()
                        location.reload() // To prevent bugs from vue-router
                })
        }
}
/**
* []请求
* @param params  参数
* @param operation     接口
*/
function customRequest(url: string, method: any, data: any) {
        // service.defaults.headers['Content-Type']=contentType
        let datatype = method.toLocaleLowerCase() === 'get' ? 'params' : 'data'
        return request({
                url: url,
                method: method,
                [datatype]: data
        })
}

export { request, customRequest }








欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2