主要步骤:
Promise
;正常200响应的时在onResponse
回到中执行resolve(data)
返回数据;401响应时直接执行refreshToken
方法请求刷新token,成功则重新执行因为401报错的请求;然后执行resolve(data)
把当前Promise状态变为fulfilled;config
和url
放进一个全局请求队列中,把当前的请求方法的Promise
中的resolve
方法也一并放入队里中并且返回(请求的Promise
状态还是处于pending
);refreshToken
接口成功响应之后,循环全局队列一一重新发请求fn(apiFetch(url, config))
并且都执行fn变更Promise
状态后清空全局队列;失败时则执行重新登录逻辑。前端项目请求工具方法封装;
具体项目请参考 request.js
import { baseUrl } from '~~/config'
import { messageDanger } from '~~/utils/toast'
import { TokenKey, RefreshTokenKey } from '@/utils/cookie'
interface PendingTask {
config: any
url: string
fn: Function
}
// 无感刷新token
let refreshing = false // 是否正在刷新token
let queue: PendingTask[] = []
// async/await函数错误统一处理 const [err, data] = await checkFile({ hash, })
export const awaitWrap = <T, U = any>(promise: Promise<T>): Promise<[U | null, T | null]> => {
return promise.then<[null, T]>((data: T) => [null, data]).catch<[U, null]>(err => [err, null])
}
// 创建一个实例
const apiFetch = $fetch.create({ baseURL: baseUrl, })
const $http = async (url: string, options: any): Promise<ApiResponse> => {
const { method = 'GET', params = {}, body = {}, headers, } = options
const config: any = {
headers: {
...headers,
},
credentials: 'include', // session需要携带cookie
method,
/* fetch中 params和body不能同时存在 */
params: ['GET', 'DELETE'].includes(method.toUpperCase()) ? params : undefined,
body: ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase()) ? body : undefined,
onRequest (ctx: any) {
ctx.options.headers.Authorization = getToken()
},
}
return await new Promise((resolve, reject) => {
apiFetch<ApiResponse>(url, {
...config,
onResponse (ctx) {
const status: number = ctx.response.status
if (status === 200 || status === 201) {
resolve(ctx.response._data)
}
},
async onResponseError (ctx: any) {
console.log('onResponseError', ctx)
// console.log('status', ctx.response)
const status: number = ctx.response.status
const { url, } = ctx.response
if (refreshing) {
queue.push({
config,
url,
// 作用是把当前状态为pending的promise放进全局数组中
// 刷新完token之后再把对应的promise状态改为fulfilled,
// 这样之前报401响应的请求没有变更状态,刷新token再变为fulfilled响应后执行等待的相关操作
fn: resolve,
})
// return为关键,不执行下面代码,不然下面resolve变更promise状态,
// 造成每个promise都会执行foreach请求多个
return
}
try {
if (status === 401 && !url.includes('/user/refresh')) {
refreshing = true
const res = await refreshToken()
refreshing = false
if (res) {
queue.forEach(({ config, url, fn, }) => {
fn(apiFetch(url, config))
})
console.log('queue', queue)
queue = []
resolve(apiFetch(url, config))
} else {
// 清除token
const token = useToken()
token.value = ''
localStorage.setItem(TokenKey, '')
console.error(ctx.response._data.message)
}
} else {
// 其他状态码直接变为reject
messageDanger(ctx.response._data.message || '')
reject(ctx.response._data)
}
} catch (error) {
reject(error)
}
},
})
})
}
// 获取 token
const getToken = () => {
const tk = useToken()
let token = ''
if (tk.value) {
token = 'Bearer ' + tk.value
}
// console.log({ token });
return token
}
const get = async (url: string, params = {}): Promise<any> => {
return await $http(url, { method: 'GET', params, }).then(res => res.data)
}
const del = async (url: string, params = {}): Promise<any> => {
return await $http(url, { method: 'DELETE', params, }).then(res => res.data)
}
const post = async (url: string, params = {}): Promise<any> => {
return await $http(url, { method: 'POST', body: params, }).then(res => res.data)
}
const put = async (url: string, params = {}): Promise<any> => {
return await $http(url, { method: 'PUT', body: params, }).then(res => res.data)
}
// 刷新token
async function refreshToken () {
const token = useToken()
const userInfo = useUserInfo()
const accessToken = localStorage.getItem(RefreshTokenKey) || ''
const res = await get('/user/refresh', { token: accessToken, })
localStorage.setItem(TokenKey, res.accessToken)
localStorage.setItem(RefreshTokenKey, res.refreshToken)
token.value = res.accessToken
const { nickname, homepage, intro, avatar, id: uid, role, } = res.user
userInfo.value = {
nickname,
homepage,
intro,
avatar,
uid,
role,
}
return res
}
export default { http: $http, get, post, put, del, awaitWrap, }
// 生成 token
async certificate(user: User) {
// 设置在token中的信息
const payload = {
id: user.id,
nickname: user.nickname,
mobile: user.mobile,
role: user.role,
};
// console.log(payload);
// 兼容老登录token
const token = this.jwtService.sign(payload);
const accessToken = this.jwtService.sign(payload, { expiresIn: '2s' });
const refreshToken = this.jwtService.sign(payload, { expiresIn: '7d' });
return {
token,
accessToken,
refreshToken,
};
}
async login(loginDTO: LoginDTO): Promise<any> {
// 用户信息
const user = await this.checkLoginForm(loginDTO);
// 密码和加盐不返回
delete user.password;
delete user.salt;
const data = await this.certificate(user);
return {
info: {
...data,
user,
},
};
}
/**
- 根据refreshToken刷新accessToken
- @param accessToken
*/
async refresh(token: string) {
try {
let user = this.jwtService.verify(token);
const data = await this.certificate(user);
user = await this.findById(user.id);
return {
...data,
user,
message: '刷新token成功',
};
} catch (e) {
throw new UnauthorizedException('token 失效,请重新登录');
}
}
import { showFailToast } from 'vant'
import axios from 'axios'
import { getToken } from './auth'
// import * as crypto from './crypto'
import 'vant/es/toast/style'
import { TOKEN_KEY, TOKEN_KEY2 } from '@/utils/auth'
// 默认 axios 实例请求配置
const configDefault = {
headers: {},
timeout: 0,
baseURL: import.meta.env.VITE_BASE_API,
data: {},
withCredentials: true
}
const SUCCESSCODE = 200
// 无感刷新token
let refreshing = false // 是否正在刷新token
let queue = []
// // 请求内容加密
// const requestEncrypt = (data) =>{
// console.log('请求报文: ',data)
// data = {
// key: crypto.sm2Encrypt(crypto.SM4KEY),
// content: crypto.sm4Encrypt(data),
// }
// return data
// }
// // 响应内容解密
// const responseDecrypt = (response) =>{
// // console.log('response==================>',response)
// const { key, content } = response.data
// let resData = crypto.sm4Decrypt(content,key)
// console.log('响应报文: ',resData)
// return resData
// }
class Http {
// 当前实例
static axiosInstance
// 请求配置
static axiosConfigDefault
// 请求拦截
httpInterceptorsRequest() {
this.axiosInstance.interceptors.request.use(
(config) => {
// 发送请求前,可在此携带 token
const token = getToken()
if (token) {
// config.headers['Authorization'] = 'Bearer ' + token
config.headers['Jwt-Token'] = token
}
return config
},
(error) => {
showFailToast(error.message)
return Promise.reject(error)
}
)
}
// 响应拦截
httpInterceptorsResponse() {
this.axiosInstance.interceptors.response.use(
(response) => {
// 开启加密
// let resData = responseDecrypt(response)
let resData = response.data
<span class="hljs-comment">// 与后端协定的返回字段</span>
<span class="hljs-keyword">const</span> { code, message } = resData
<span class="hljs-comment">// 判断请求是否成功</span>
<span class="hljs-keyword">const</span> isSuccess = response && <span class="hljs-title class_">Reflect</span>.<span class="hljs-title function_">has</span>(resData, <span class="hljs-string">'code'</span>) && code === <span class="hljs-variable constant_">SUCCESSCODE</span>
<span class="hljs-comment">// console.log(code, message, resData ,isSuccess,'-------------------')</span>
<span class="hljs-keyword">if</span> (isSuccess) {
<span class="hljs-keyword">return</span> resData.<span class="hljs-property">data</span>
} <span class="hljs-keyword">else</span> {
<span class="hljs-comment">// 处理请求错误</span>
<span class="hljs-title function_">showFailToast</span>(message)
<span class="hljs-keyword">return</span> <span class="hljs-title class_">Promise</span>.<span class="hljs-title function_">reject</span>(resData.<span class="hljs-property">data</span>)
}
},
<span class="hljs-keyword">async</span> (error) => {
<span class="hljs-keyword">try</span> {
<span class="hljs-comment">// console.log(error)</span>
<span class="hljs-keyword">let</span> { status, data, config } = error.<span class="hljs-property">response</span> || {}
<span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">'status========================>'</span>, status)
<span class="hljs-keyword">if</span> (refreshing) {
<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve</span>) =></span> {
queue.<span class="hljs-title function_">push</span>({
config,
resolve
})
})
}
<span class="hljs-keyword">let</span> message = <span class="hljs-string">''</span>
<span class="hljs-comment">// HTTP 状态码</span>
<span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">'==================>response.data'</span>, data)
<span class="hljs-keyword">switch</span> (status) {
<span class="hljs-keyword">case</span> <span class="hljs-number">400</span>:
message = <span class="hljs-string">'请求错误'</span>
<span class="hljs-keyword">break</span>
<span class="hljs-keyword">case</span> <span class="hljs-number">401</span>:
message = <span class="hljs-string">'未授权,请登录'</span>
<span class="hljs-keyword">if</span> (status === <span class="hljs-number">401</span> && !config.<span class="hljs-property">url</span>.<span class="hljs-title function_">includes</span>(<span class="hljs-string">'/user/refresh'</span>)) {
refreshing = <span class="hljs-literal">true</span>
<span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> <span class="hljs-title function_">refreshToken</span>()
refreshing = <span class="hljs-literal">false</span>
<span class="hljs-keyword">if</span> (res) {
queue.<span class="hljs-title function_">forEach</span>(<span class="hljs-function">(<span class="hljs-params">{ config, resolve }</span>) =></span> {
<span class="hljs-title function_">resolve</span>(<span class="hljs-variable language_">this</span>.<span class="hljs-title function_">axiosInstance</span>(config))
})
queue = []
<span class="hljs-keyword">return</span> <span class="hljs-variable language_">this</span>.<span class="hljs-title function_">axiosInstance</span>(config)
} <span class="hljs-keyword">else</span> {
message = data.<span class="hljs-property">message</span> || <span class="hljs-string">'登录过期,请重新登录'</span>
}
} <span class="hljs-keyword">else</span> {
<span class="hljs-keyword">return</span> error.<span class="hljs-property">response</span>
}
<span class="hljs-keyword">break</span>
<span class="hljs-keyword">case</span> <span class="hljs-number">403</span>:
message = data.<span class="hljs-property">message</span> || <span class="hljs-string">'拒绝访问'</span>
<span class="hljs-keyword">break</span>
<span class="hljs-keyword">case</span> <span class="hljs-number">404</span>:
message = data.<span class="hljs-property">message</span> || <span class="hljs-string">`请求地址出错: <span class="hljs-subst">${error.response?.config?.url}</span>`</span>
<span class="hljs-keyword">break</span>
<span class="hljs-keyword">case</span> <span class="hljs-number">408</span>:
message = data.<span class="hljs-property">message</span> || <span class="hljs-string">'请求超时'</span>
<span class="hljs-keyword">break</span>
<span class="hljs-keyword">case</span> <span class="hljs-number">500</span>:
message = data.<span class="hljs-property">message</span> || <span class="hljs-string">'服务器内部错误'</span>
<span class="hljs-keyword">break</span>
<span class="hljs-keyword">case</span> <span class="hljs-number">501</span>:
message = data.<span class="hljs-property">message</span> || <span class="hljs-string">'服务未实现'</span>
<span class="hljs-keyword">break</span>
<span class="hljs-keyword">case</span> <span class="hljs-number">502</span>:
message = data.<span class="hljs-property">message</span> || <span class="hljs-string">'网关错误'</span>
<span class="hljs-keyword">break</span>
<span class="hljs-keyword">case</span> <span class="hljs-number">503</span>:
message = data.<span class="hljs-property">message</span> || <span class="hljs-string">'服务不可用'</span>
<span class="hljs-keyword">break</span>
<span class="hljs-keyword">case</span> <span class="hljs-number">504</span>:
message = data.<span class="hljs-property">message</span> || <span class="hljs-string">'网关超时'</span>
<span class="hljs-keyword">break</span>
<span class="hljs-keyword">case</span> <span class="hljs-number">505</span>:
message = data.<span class="hljs-property">message</span> || <span class="hljs-string">'HTTP版本不受支持'</span>
<span class="hljs-keyword">break</span>
<span class="hljs-attr">default</span>:
message = data.<span class="hljs-property">message</span> || <span class="hljs-string">'网络连接故障'</span>
}
<span class="hljs-title function_">showFailToast</span>(message)
<span class="hljs-keyword">return</span> <span class="hljs-title class_">Promise</span>.<span class="hljs-title function_">reject</span>(error)
} <span class="hljs-keyword">catch</span> (error) {
<span class="hljs-keyword">return</span> <span class="hljs-title class_">Promise</span>.<span class="hljs-title function_">reject</span>(<span class="hljs-string">'网络连接故障'</span>)
}
}
)
}
constructor(config) {
this.axiosConfigDefault = config
this.axiosInstance = axios.create(config)
this.httpInterceptorsRequest()
this.httpInterceptorsResponse()
}
// 通用请求函数
request(paramConfig) {
const config = { ...this.axiosConfigDefault, ...paramConfig }
return new Promise((resolve, reject) => {
this.axiosInstance
.request(config)
.then((response) => {
resolve(response)
})
.catch((error) => {
reject(error)
})
})
}
post(url, data) {
// data = requestEncrypt(data)
return this.request({ url, method: 'post', data })
}
patch(url, data) {
return this.request({ url, method: 'patch', data })
}
get(url, params) {
return this.request({ url, method: 'get', params })
}
del(url, params) {
return this.request({ url, method: 'delete', params })
}
}
const refreshToken = async () => {
const refreshToken = localStorage.getItem(TOKEN_KEY2) || ''
const res = await http.get('/mobile/refreshToken', { token: refreshToken })
// console.log(res,'refreshToken>>>>>>>')
localStorage.setItem(TOKEN_KEY, res.accessToken)
localStorage.setItem(TOKEN_KEY2, res.refreshToken)
return res
}
export const http = new Http(configDefault)