首页 学海无涯 Web前端 在Ant Design Pro中配合后端的JWT实现无感刷新Token
在Ant Design Pro中配合后端的JWT实现无感刷新Token
摘要 Jwt是后端常用的认证授权方案,常见模式是用户登录后返回一个accessToken和refreshToken,当accessToken过期后,需要使用refreshToken去获取新的accessToken,为了让用户有个良好的体验,我们需要做到无感知刷新Token,这里就需要前端进行相应的处理才能完成。

环境

ant-design-pro 5.2

umi 3.5.0

前言

阅读本文前,你至少应该了解什么是JWT。本文开发环境为AntDesign Pro框架,所以你应该对AntDesign Pro框架有一定了解,包括TypeScript、umi-request、ES6等。

本文只探讨前端无感知刷新Token的方案,关于后端的实现不做探讨。假定后端有两个接口,一个登录认证的接口(使用账号密码换取accessToken和refreshToken),一个刷新Token的接口(使用refreshToken换取新的accessToken和refreshToken)。accessToken使用JWT的格式设计,有效时间长,用于资源的访问;refreshToken格式无要求,有效时间长,仅用于调用刷新Token的接口。后端仅限于此,无其他额外功能。

正文

要实现前端无感知刷新token,通常是采用请求拦截器或者中间件的方式,正好umi-request有中间件和拦截器的功能,而AntDesign Pro也集成了umi-request,并且提供了比较方便的使用方式,这里我们使用拦截器的方式,并且使用的是请求拦截器:requestInterceptors。

在app.ts文件中,可以全局定义umi-request的运行时配置,有关umi-request的运行时配置可以查看链接:https://umijs.org/zh-CN/plugins/plugin-request

// 只需要在app.ts文件中export一个名称为request的对象即可,该对象包含对umi-request的全局配置。
export const request: RequestConfig = {
  // umi-request配置
};

一、定义一个请求拦截器,用来处理请求之前加上Jwt授权请求头(完整代码如下):

let isPending = false; // 是否正在刷新Token
const _cacheRequest: (() => void)[] = []; // 正在等待的请求
const authHeaderInterceptor: any = (url: string, options: RequestOptionsInit) => {
if (url === '/api/authentication/login') {
// 如果是登录,不做处理!
return {
url: url,
options: options,
};
}
if (url === '/api/authentication/refreshtoken') {
// 如果是刷新,不做处理,并且跳过默认的异常处理!
return {
url: url,
options: { ...options, skipErrorHandler: true },
};
}
const accessToken = localStorage.getItem('ACCESS_TOKEN');
if (accessToken) {
const authHeader = { Authorization: `Bearer ${accessToken}` };
const decodeToken = jwt_decode<{ exp: number }>(accessToken);
const { exp } = decodeToken;
const expTime = exp * 1000; // 过期时间戳
const nowTime = new Date().getTime(); // 当前时间戳

if (nowTime >= expTime) {
// accessToken 已过期
if (!isPending) {
isPending = true;
// 刷新accessToken
const refreshToken = localStorage.getItem('REFRESH_TOKEN');
if (refreshToken) {
// 异步调用刷新Token的接口
authenticationRefreshToken({
refreshToken,
})
.then((res) => {
const { success, data } = res;
if (success && data) {
// 刷新成功,保存accessToken和refreshToken
localStorage.setItem('ACCESS_TOKEN', data.accessToken);
localStorage.setItem('REFRESH_TOKEN', data.refreshToken);
} else {
// 刷新失败,清空accessToken和refreshToken
localStorage.removeItem('ACCESS_TOKEN');
localStorage.removeItem('REFRESH_TOKEN');
}
// 发起正在排队的请求
isPending = false;
_cacheRequest.map((req) => {
req();
});
})
.catch(() => {
// 清空accessToken和refreshToken
localStorage.removeItem('ACCESS_TOKEN');
localStorage.removeItem('REFRESH_TOKEN');
// 发起正在排队的请求
isPending = false;
_cacheRequest.map((req) => req());
});
} else {
// refreshToken为空,清空accessToken
localStorage.removeItem('ACCESS_TOKEN');
// 不做处理
isPending = false;
return {
url: url,
options: options,
};
}
}
// 返回Promise等待刷新token返回结果后再resolve
return new Promise((resolve) => {
_cacheRequest.push(() => {
// 重新添加Token请求头,以保证使用的是最新的accessToken
resolve({
url: `${url}`,
options: {
...options,
interceptors: true,
headers: { Authorization: `Bearer ${localStorage.getItem('ACCESS_TOKEN')}` },
},
});
});
});
}

// 添加Token请求头
return {
url: `${url}`,
options: { ...options, headers: authHeader },
};
}

// 不做处理
return {
url: url,
options: options,
};
};

简单说明一下实现逻辑:

1.先从localStorage中获取accessToken(通常用户登录后我们会将accessToken和refreshToken存储在localStorage中),如果accessToken存在,则走accessToken的逻辑;如果不存在,则不做处理,该重新登录就重新登录。

2.拿到accessToken后,判断accessToken是否过期(由于后台并没有返回accessToken的过期时间,所以需要前端做jwt的解析),这里我们使用npm安装包jwt-decode,它可以解析jwt,获取到过期时间等信息。如果accessToken已经过期,则需要走refreshToken的逻辑;如果没过期,则带上accessToken授权请求头。

3.如果accessToken已经过期,则需要进行refreshToken(无感刷新的核心逻辑)。我们需要定义一个正在刷新Token的变量(isPending)和一个缓存等待中请求的数组(_cacheRequest)。isPending作为是否正在刷新Token的标识,当isPending为false的时候,执行刷新Token的请求,并返回一个Promise,将本次请求放在_cacheRequest;当isPending为true时,直接将本次请求放在_cacheRequest中。接下来等待刷新Token的请求结果,如果获取到新的accessToken和refreshToken,就将其放在localStorage中,然后将之前等待的请求一并发起。如果刷新Token失败,就将localStorage中的accessToken和refreshToken清空,然后同样也将等待中的请求一并发起(这个时候没有accessToken和refreshToken,该重新登录就重新登录)。

主要逻辑是将请求通过Promise暂停一下,等待Token刷新完成后一并发起请求,无论刷新Token的结果怎样,都需要将等待中的请求一并发起。

二、将请求拦截器添加到umi-request全局配置中:

export const request: RequestConfig = {
requestInterceptors: [authHeaderInterceptor],
};

至此,使用umi-request的除了登录和刷新Token之外的每一个请求,都会附加上jwt授权请求头。

此外还需要加一个响应拦截器,拦截所有401错误码的响应,然后跳转到登录页面。

PS:1、关于JWT是否过期的判断,前端采用的是本机时间,如果时间和服务器时间不一致,那么肯定会出现判断的偏差,这个无法避免。2、写该篇文章时也是博主看了些别人写的文章,稍作修改后即刻发表,具体可行性还有待验证。

版权声明:本文由不落阁原创出品,转载请注明出处!

本文链接:http://www.leo96.com/article/detail/78

广告位

本文配乐
来说两句吧
最新评论

暂无评论,大侠不妨来一发?