![](https://jiang-xia.top/x-api/blog-server/static/uploads/2022-08-10/8ojhda8gzvyx3rdhgyq378-头像.jpg)
主 题
前言
主要步骤:
- 在nuxt中基于$fetch封装请求工具http类;
- 总的思路是请求方法为返回一个
Promise
;正常200响应的时在onResponse
回到中执行resolve(data)
返回数据;401响应时直接执行refreshToken
方法请求刷新token,成功则重新执行因为401报错的请求;然后执行resolve(data)
把当前Promise状态变为fulfilled; - 主要难点是当token过期时,已经发出了多个失败的请求,重新请求时如一一响应;正在刷新中时会把多个请求的
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, }
二、后端实现
- 接口一:验证用户登录之后生成一个过期时间为0.5h的accessToken和一个过期时间为7d的refreshToken;
- 接口二:当accessToken过期之后,用refreshToken请求该接口刷新两个token的过期时间;具体请求参考后端user模块代码 UserService
// 生成 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 失效,请重新登录');
}
}
全部评论(0)