目录
前言
给自己的项目做登录功能,正好丰富一下经验,决定用一下无感登录
我大概参考了这位大佬的文章:https://www.cnblogs.com/sunyan97/p/17887134.html
(一)介绍
无感登录,就是无感刷新token
登录成功后,后端发送两个token给前端:access_token 和 refresh_token
access_token有效时间很短,一般为30min,refresh_token有效时间较长,大概是7days
- access_token用于带到请求头进行权限请求
- 当后端检测到过期时,通知前端acess_token已过期(401)
- 前端携带refresh_token发起刷新请求,后端根据refresh_token发送新的access_token
- 如果refresh_token也过期,就返回401给前端,通知前端退出登录
为什么不采用前端定时器定时发起刷新请求?
定时刷新对浏览器性能消耗过大;用到了再刷新,将刷新决定权交给后端,节省资源;
如何存储?
access_token可以存储在storage里,refresh_token尽量存储的保密一点,比如httpOnly-token等(没试过这个
(二)具体实现
前端采用react、后端采用nest,代码可能有部分删减,并不完整
1.后端登录成功发送token
使用jwt签发token
pnpm add @nestjs/jwt
在auth.controller.ts:
// 生成 token
async login(user: any) {
// 临时token
const access_token = this.jwtService.sign({
email: user.email,
sub: user.userId,
},{
expiresIn: '1m',
},
);
// 刷新token
const refresh_token = this.jwtService.sign({
sub: user.userId,
},{
expiresIn: '10m',
},
);
// 无感登录
return {
access_token,
refresh_token,
};
}
2.前端接收token存储
这里用到了ahooks的useRequest发起请求
为了简单省事,access_token采用localStorage存储,refresh_token采用cookie存储
pnpm add ahooks js-cookie
const navigate = useNavigate();
// 发起注册请求
const { run:signUpRun,loading:signUpLoading } = useRequest(
(params)=>{
return register(params)
},
{
manual:true,
onSuccess(res:any,params:any) {
toast({
title: "登录成功!",
duration: 1000,
})
// 保存token
saveToken(res.data)
// 跳转到主页
navigate(`/dashboard`, { replace: false })
},
onError(err:any) {}
}
});
const saveToken = ({ access_token, refresh_token }: { access_token: string; refresh_token: string }) => {
localStorage.setItem('access_token', access_token);
Cookies.set('refresh_token', refresh_token);
};
3.前端携带access_token发请求
在service配置页:
// 请求拦截
this.instance.interceptors.request.use(
config => {
// 携带access_token
const access_token =localStorage.getItem('access_token');
if (access_token) {
config.headers.Authorization = `Bearer ${access_token}`;
}
return config;
},
err => {
return Promise.reject(err);
}
);
4.后端配置守卫
配置guard自动检测传来的请求头内的access_token是否过期,过期自动返回401
在guard/login.guard.ts:
import { CanActivate, ExecutionContext, Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { Observable } from 'rxjs';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
@Injectable()
export class LoginGuard implements CanActivate {
@Inject(JwtService)
private jwtService: JwtService;
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request: Request = context.switchToHttp().getRequest();
const authorization = request.headers.authorization;
if (!authorization) {
throw new UnauthorizedException('用户未登录');
}
try {
console.log('authorization', authorization);
const token = authorization.split(' ')[1];
const data = this.jwtService.verify(token);
console.log('data', data);
return true;
} catch (e) {
throw new UnauthorizedException('token失效,请重新登录');
}
}
}
写一个接口使用一下guard
在user.controller.ts:
@UseGuards(LoginGuard)
@Get('getUserByEmail')
async getUserByEmail(@Query('email') email: string) {
const {password,...result} = await this.userService.findByEmail(email);
return result
}
当access_token有效的时候,可以直接返回数据,如果失效就返回401
下面开始实现 “无感刷新” 的功能
5.后端刷新token逻辑
用jwtService.verify验证refresh_token是否过期,过期返回401,没过期返回新的access_token
在auth.controller.ts:
@Post('refresh')
async refresh(@Body('refresh_token') refreshToken: string) {
try {
const data = this.jwtService.verify(refreshToken);
const user = await this.userService.findById(data.sub);
// 重新签发access_token
const access_token = this.jwtService.sign(
{ sub: user.id,email: user.email, },
{ expiresIn: '30m' },
);
return {
access_token,
};
} catch (error) {
throw new UnauthorizedException('token已失效,请重新登录');
}
}
6.前端响应拦截器实现刷新
在axios的响应拦截器里实现刷新
原因如下:
请求响应后判断请求路径是否为需带权限请求路径,并判断状态码,满足要求就发起自主发起刷新请求,可以实现无感刷新;
如果不在响应拦截器里设置刷新请求,那么每次写一个带权限的请求,都需要在处理函数里增加判断权限的操作,非常复杂;而在响应器里设置,只需要配置相关路径(需要前后端规范
// 响应拦截
this.instance.interceptors.response.use(
(response) => response.data,
async (err) => {
let { data, config } = err.response;
if (data.statusCode === 401 && config.url.includes("/user/getUserByEmail")) {
const res:any = await refresh({ refresh_token: getRefreshToken() });
if (res.statusCode === 200) {
saveAccessToken(res.data.access_token);
// 重新发起请求
return this.instance(config);
} else {
alert("登录过期,请重新登录");
removeToken()
window.location.href = '/auth/login'
return Promise.reject(res.data);
}
} else {
return Promise.reject(err);
}
}
);
测试测试:
无感登录!欧了!
(三)总结
无感登录大概就是这样,不过我没有对token进行加密,直接存在cookie里还是会有安全问题
用户权限的路由守卫还没写,后面补上,挥挥~