Token无感知刷新

前言

前一段时间要做一个小项目,项目的模板也有,从原来的项目上重新拉一个分支然后创建一个新的仓库,原来的项目是没有做token无感知刷新的,而是token过期重新跳转登录页进行登录。这样会导致有时候在进行业务操作时,突然跳转到登录页,然后要从头操作。所以就找后端说了一下使用双token进行token无感知刷新,后端大哥也很配合。

可能有人会说把token时间设置长一点不就行了,这样是解决的眼前的问题,当是不安全,会导致token被滥用

完整代码在后面。

正文

token无感刷新的原理也简单,使用双token,分别为accessTokenrefreshToken,正常都是携带accessToken进行验证。当返回状态码表示token过期时,再携带refreshToken重新获取accessToken,然后重新携带accessToken发起请求。

实现效果

  1. accessToken没有过期

Token无感知刷新

  1. accessToken过期但是refreshToken没有过期

Token无感知刷新

  1. accessTokenrefreshToken都过期

Token无感知刷新

Node后端环境搭建

这里的核心就是使用中间件进行token验证,并将无tokentoken过期的状态码设置为401。

let Koa = require('koa');
let app = new Koa();
let fs = require('fs')
let Router = require('koa-router')();
const cors = require("@koa/cors")
app.use(cors())
const jwt = require('jsonwebtoken');
//静态web服务//到public目录下找,返回资源链接
const path = require('path')
let koaStatic = require('koa-static');
app.use(koaStatic(path.join(__dirname, 'public')))
var bodyParser = require('koa-bodyparser');
app.use(bodyParser());
//秘钥
const tokenSecret = 'aaaaaaaaa'
/** 生成token*/
const createToken = () => {
let accessRule = {
iss: "lzt",
sub: "lzt",
aud: 'user',
exp: Math.floor(Date.now() / 1000) + 10, // 10秒后过期
}
let refreshRule = {
iss: "lzt",
sub: "lzt",
aud: 'user',
isRefresh: true,
exp: Math.floor(Date.now() / 1000) + 30, // 30秒后过期
}
let accessToken = jwt.sign(accessRule, tokenSecret);
let refreshToken = jwt.sign(refreshRule, tokenSecret);
return {
accessToken,
refreshToken
}
}
//使用中间件验证token
const verifyToken = (token) => {
return new Promise((resolve, reject) => {
jwt.verify(token, tokenSecret, (err, decode) => {
if (err) {
reject(err)
} else {
resolve(decode)
}
})
})
}
app.use(async (ctx, next) => { 
if (ctx.url === '/login') {
await next()
} else {
let token = ctx.get('Authorization')
if (token === '') {
//设置状态码
ctx.status = 401
ctx.body = {
code: 401,
msg: '没有token'
}
} else {
try {
let decode = await verifyToken(token)
console.log(decode);
await next()
} catch (error) {
ctx.status = 401
ctx.body = {
code: 401,
msg: 'token过期'
}
}
}
}
})
Router.get('/test', async (ctx) => { 
console.log(ctx);
ctx.body = {
code: 200,
msg: '测试'
}
})
Router.post('/refreshToken', async (ctx) => { 
let tokenObj = createToken()
ctx.body = {
code: 200,
data: {
accessToken: tokenObj.accessToken,
}
}
})
//登录接口
Router.get('/login', async (ctx) => {
let tokenObj = createToken()
ctx.body = {
code: 200,
data: {
tokenObj,
}
}
})
app
.use(Router.routes())   	//启动路由
.use(Router.allowedMethods());
app.listen(3000);

前端代码

主要是对请求进行响应拦截和请求拦截

  • 请求拦截:判断请求路径,为请求头添加对应token
  • 响应拦截:对响应的数据进行统一处理
import axios from "axios";
import { AxiosRetry } from './axiosClass'
axios.defaults.baseURL='http://127.0.0.1:3000'
// 添加请求拦截器
axios.interceptors.request.use(
function (config) {
// 在发送请求之前做些什么
// 在请求头中添加token
config.headers.Authorization = localStorage.getItem("accessToken");
if (config.url == "/refreshToken") {
config.headers.Authorization = localStorage.getItem("refreshToken");
}
return config;
},
);
/**先到拦截器*/
axios.interceptors.response.use(res => {
if (res.status != 200) {
return Promise.reject(res.data);
}
return Promise.resolve(res.data)
});
const axiosRetry = new AxiosRetry({
onSuccess: (res) => {
let { accessToken } = res.data
localStorage.setItem("accessToken", accessToken);
},
onError: () => {
console.log('refreshToken过期,需要重新登录');
},
});
export const request = (url: string) => {
return axiosRetry.requestWrapper(() => {
return axios({
method: "get",
url: `${url}`,
})
});
}

下面是token过期到重新发起请求的主要代码。主要是对token过期状态401进行判断并进行相应的处理

import { Axios } from 'axios';
import axios from 'axios';
export class AxiosRetry {
//相当于一个锁
private fetchNewTokenPromise: Promise<any> | null = null;
private onSuccess: (res: any) => any;
private onError: () => any;
constructor({
onSuccess,
onError,
}: {
onSuccess: (res: any) => any;
onError: () => any;
}) {
this.onSuccess = onSuccess;
this.onError = onError;
}
/** 发送请求*/
requestWrapper<T>(request: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
/** 将请求接口的函数保存*/
const requestFn = request;
return request().then((res) => {
//拦截器处理后的数据
resolve(res);
}).catch(err => {
//token过期或者没有token    
if (err.response.status === 401) {
if (!this.fetchNewTokenPromise) {
this.fetchNewTokenPromise = this.fetchNewToken();
}
this.fetchNewTokenPromise.then(() => {
return requestFn();
}).then((res) => {
resolve(res);
this.fetchNewTokenPromise = null;
}).catch((err) => {
reject(err);
this.fetchNewTokenPromise = null;
});
} else {
reject(err);
}
});
});
}
// 获取新的token
fetchNewToken() {
return axios({
method: "post",
url: `/refreshToken`,
}).then((res) => {
this.onSuccess(res)
}).catch((err) => {
this.onError();
//表示refreshToken过期,需要重新登录
if (err.response.status === 401) {
return Promise.reject(
new Error("refreshToken过期,需要重新登录")
);
}
//表示发生了其他错误
else {
return Promise.reject(err); 
}
})
}
}

结语

完整代码地址 : function-realization: 实现一些有趣的功能 (gitee.com)

感兴趣的可以去试试。

原文链接:https://juejin.cn/post/7317106006688448524 作者:卸任

(0)
上一篇 2023年12月27日 下午4:49
下一篇 2023年12月27日 下午5:00

相关推荐

发表回复

登录后才能评论