为什么双token,一个token不就行了吗?
只有一个token
时,我们称为access_token
。因为access_token
需要频繁地通过网络请求来进行用户鉴权,所以容易被获取到。这时如果有效期设置得太长,那么access_token
被冒用的可能性很大,存在危险。
refresh_token
一般长期保存在本地,只会在请求新token
的和返回新token
的报文中出现,网络中出现的概率低,被获取的概率也就比较低,所以有效期可以设置长一点没事。
综上,因为access_token
如果有效期太短,用户就需要频繁地进行身份验证,用户体验差。设置得太长呢,一旦access_token
被获取之后被冒用的可能性大。所以使用refresh_token
就可以把access_token
的有效期缩短,在提高安全性的同时还保证了用户体验。
具体实现
后端
逻辑比较简单直接看代码~
@Post('login')
login(@Body() userDto: UserDto) {
const user = users.find((item) => item.username === userDto.username);
if (!user) {
throw new BadRequestException('用户不存在');
}
if (user.password !== userDto.password) {
throw new BadRequestException('密码错误');
}
const access_token = this.jwtService.sign(
{
username: user.username,
email: user.email,
},
{
expiresIn: '0.5h',
},
);
const refresh_token = this.jwtService.sign(
{
username: user.username,
},
{
expiresIn: '7d',
},
);
return {
userInfo: {
username: user.username,
email: user.email,
},
access_token,
refresh_token,
};
}
@Get('refresh')
refresh(@Query('token') token: string) {
try {
const data = this.jwtService.verify(token);
console.log(data);
const user = users.find((item) => item.username === data.username);
const access_token = this.jwtService.sign(
{
username: user.username,
email: user.email,
},
{
expiresIn: '0.5h',
},
);
const refresh_token = this.jwtService.sign(
{
username: user.username,
},
{
expiresIn: '7d',
},
);
return {
access_token,
refresh_token,
};
} catch (error) {
throw new UnauthorizedException('token 失效,请重新登录');
}
}
前端
//请求队列数据类型
interface PendingTask {
config: AxiosRequestConfig;
resolve: Function;
}
//用于判断当前是否处于刷新状态,避免多次刷新
let refreshing = false;
//定义请求队列
const queue: PendingTask[] = [];
//请求拦截器中自动添加authorization字段用于用户鉴权
axiosInstance.interceptors.request.use(function (config) {
const accessToken = localStorage.getItem("access_token");
if (accessToken) {
config.headers.authorization = "Bearer " + accessToken;
}
return config;
});
//基于axios响应拦截器实现无感刷新
axiosInstance.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
let { data, config } = error.response;
//如果处于刷新状态,则本次请求返回一个promise,并把当前config和resolve加到队列
if (refreshing) {
return new Promise((resolve) => {
queue.push({ config, resolve });
});
}
//如果后端返回401状态码且当前的请求不是刷新token的请求(避免刷新失败了还继续刷新的情况)
if (data.statusCode === 401 && !config.url.includes("/refresh")) {
refreshing = true;
const res = await refreshToken();
if (res.status === 200) {
//当刷新成功后,遍历异步请求队列清空所有请求
queue.forEach(({ config, resolve }) => {
resolve(axiosInstance(config));
});
return axiosInstance(config);
} else {
//刷新失败,则说明refresh_token过期,用户需要重新登录
alert(data || "登录过期,请重新登录");
}
} else {
return error.response;
}
}
);
// 刷新token函数
async function refreshToken() {
const res = await axiosInstance.get("/refresh", {
params: {
token: localStorage.getItem("refresh_token"),
},
});
const { access_token, refresh_token } = res.data;
localStorage.setItem("access_token", access_token);
localStorage.setItem("refresh_token", refresh_token);
return res;
}
//用户登录函数
export async function userLogin(username: string, password: string) {
return await axiosInstance.post("/login", {
username,
password,
});
}