无感刷新token流程图
无感刷新请求逻辑
- 请求login接口验证账号密码,获取token和refreshToken
- 请求index页面携带token进行请求,
- 如果成功正常获取数据
- 如果token过期则返回code: 401,msg: token过期
- 前端挂起当前index请求的回调
- 先请求refresh接口,携带token和refreshToken
- 如果refreshToken也过期,则直接失败
- 如果refreshToken没有过期,则返回新的token给前端
- 前端使用新token重新请求index页面
10.得到index页面结果后执行前面挂起的index请求的回调
11.整个页面token刷新就结束了
token无感刷新前端代码实现
// pinia 中的代码
actions: {
async login(user: LoginUserType) {
const result = await Login.login(user);
this.token = result.data.token
this.refreshToken = result.data.refreshToken
storage.set('token', result.data.token)
storage.set('refreshToken', result.data.refreshToken)
},
async restToken() {
const result = await Login.restTokenRequest();
this.token = result?.data?.token
storage.set('token', this.token)
},
axiosInstance.interceptors.request.use(
(config) => {
config.headers.Authorization = `Bearer ${storage.get('token')}`
if (config.url === '/loginmodule/login/restToken') {
config.headers.refreshToken = `Bearer ${storage.get('refreshToken')}`
}
return config;
},
(err) => {
ElMessage.error(`请求错误${err}`);
}
);
axiosInstance.interceptors.response.use(
async (response) => {
const { data, msg, code } = response.data;
if (code === 200) {
return response.data;
} else if (code === 400) {
const loginStore = useLogin()
await loginStore.restToken()
return await this.axiosInstance.request(response.config)
}
else if (code === 500) {
ElMessage.error(msg);
return;
} else {
ElMessage.error("服务器出现了未知错误");
return;
}
},
(err) => {
ElMessage.error(`${SERVER_ERR}${err}`);
return;
}
);
// login请求的封装函数
async restTokenRequest() {
if (LoginAPI.promise) {
return LoginAPI.promise
}
LoginAPI.promise = new Promise<AxiosResponse<Omit<LoginResponesType, "refreshToken">, any>>(async (resolve, reject) => {
const result = await LoginAPI.api.restToken()
resolve(result)
}).finally(() => {
LoginAPI.promise = null
})
return LoginAPI.promise
}
token无感刷新后端代码实现
后端接口代码是采用IOC框架的一种书写模式
// TS 装饰器 重构 Koa 路由中的方法装饰器
@Controller("/loginmodule")
class LoginController {
@post("/login")
async login(ctx: Context) {
if (ctx.user && ctx.token) {
return ctx.success({
user: ctx.user.username,
token: ctx.token,
});
}
const body = ctx.request.body as LoginUserType;
const isUser = await userDao.findLoginUser(body);
if (isUser && isUser.password === body.password) {
const token = UserService.generateToken(body.username);
const refreshToken = UserService.generateRefreshToken(body.username);
return ctx.success({
user: body.username,
token,
refreshToken,
});
}
ctx.fail("用户名不存在或者密码不正确");
}
@post("/login/restToken")
async restToken(ctx: Context) {
const token = ctx.header.authorization?.split(" ")[1];
const refreshToken = (ctx.header.refreshtoken as string)?.split(" ")[1];
if (token && refreshToken) {
try {
const result= UserService.checkToken(token) as JwtPayload;
return ctx.success({
username: result.username,
token: token
});
} catch (e: any) {
if (e.message === "jwt expired") {
try {
const resultRefresh = UserService.checkRefreshToken(refreshToken) as JwtPayload;
const updateToken = UserService.generateToken(resultRefresh.username);
return ctx.success({
username: resultRefresh.username,
token: updateToken,
});
} catch (e: any) {
ctx.fail("token非法或者已过期请重新登陆");
}
}
}
} else {
ctx.fail("token非法");
}
ctx.fail("token非法");
}
后端中间件处理
export class AuthorizationCheck {
static whiteList = [/.*\/login.*/];
static async check(ctx: Context, next: Koa.Next) {
const isDisable = AuthorizationCheck.whiteList.some((item) => {
const regexp = RegExp(item);
return regexp.test(ctx.request.url);
});
if (!isDisable) {
const token = ctx.header.authorization?.split(" ")[1] || "";
try {
const user = UserService.checkToken(token);
ctx.user = user as JwtPayload;
ctx.token = token;
return await next();
} catch (e: any) {
if (e.message === "jwt expired") {
return ctx.success({}, 'token已过期', 400)
} else {
return ctx.fail('token不合法')
}
}
}
await next();
}
}