思路
- 用户登录 返回给前端 ACCESS_TOKEN(1h)、REFRESH_TOKEN(7d)、和 ACCESS_TOKEN 的失效时间(1h)
- 其他的接口请求时 携带 ACCESS_TOKEN
- Axios处理:前端根据 ACCESS_TOKEN 的失效时间判断过期或者即将过期, 或者接口返回用户校验失败401的错误,前端向后端发送刷新 REFRESH_TOKEN 的接口刷新 ACCESS_TOKEN、REFRESH_TOKEN
- 后端nestjs:authGuard拦截接口,判断是否需要accessToken校验, 如果accessToken校验失败,返回401,token失效,前端接收401错误后发送refresh的接口刷新 ACCESS_TOKEN、REFRESH_TOKEN
- nestjs refresh接口: 接收请求后 判断REFRESH_TOKEN是否存在
- 不存在直接返回401错误
- 若REFRESH_TOKEN,判断和redis中存储的REFRESH_TOKEN是否一致,不一致返回401
- 解析REFRESH_TOKEN,若解析token失败,返回401
- 获取用户id,调用userService获取用户信息
- 重新生成ACCESS_TOKEN、REFRESH_TOKEN,并结合用户信息返回给客户端
axios 响应拦截实现接口无感刷新
401错误根据后台封装情况,是直接接口报错返回status:401 或者 接口请求成功 在data再返回code: 401
customAxios.interceptors.response.use(
(response) => {
const { data, config, status } = response
const { code } = data
const { expiresOut } = getCurrentUser()
const flag = expiresOut - Date.now() / 1000 < 60 * 10 // 判断过期时间距离accessToken过期时间10分钟之内
const pathFlag = !noTokenList.includes(location.pathname) // 不需要token的接口列表
if (pathFlag && (flag || code === 401 || status === 401)) { // status === 401 为多余项 非200的会被catch拦截
if (!isRefreshing) {
dealRefresh()
}
const retryOriginalRequest = new Promise((resolve) => {
addSubscriber(() => {
// 将当前接口放到重新请求的队列中
resolve(customAxios(config))
})
})
return retryOriginalRequest
}
if (location.pathname !== '/user/login' && code === 401) {
// token失效
// location.replace('/user/login')
}
return response.data
},
(error) => {
const { response } = error
const { status, data, config } = response
if (
location.pathname !== '/user/login' &&
(+status === 401 || +data.code === 401)
) {
// token失效
if (!isRefreshing) {
dealRefresh()
}
const retryOriginalRequest = new Promise((resolve) => {
addSubscriber(() => {
resolve(customAxios(config))
})
})
return retryOriginalRequest
}
// location.replace('/user/login')
return Promise.reject(error)
}
)
// 是否正在刷新的标记
export let isRefreshing = false
//重试队列
export let requests: any[] = []
// 发布订阅 执行请求池中的接口
export function onAccessTokenFetched() {
requests.forEach((callback: any) => {
callback && callback()
})
requests = [] // 清空重试队列
}
// 发布订阅 将未完成的接口放入请求池
export function addSubscriber(callback: any) {
requests.push(callback)
}
export const refreshAxios = axios.create({}) // 新建一个axios实例
// 刷新token的处理
export const dealRefresh = async () => {
isRefreshing = true // 正在刷新token
const url = `/api/sign/refresh?refreshToken=${getRefresh()}` // 自行处理 token刷新的接口路径
refreshAxios
.get(url)
.then(async (refresData) => {
const data = refresData.data.data
// 更新用户信息 处理localStorage的token信息
const updateUser = JSON.parse(localStorage.getItem('currentUser') || '{}')
updateUser.expiresOut = data.expiresOut
updateUser.accessToken = data.accessToken
await localStorage.setItem('token', data.accessToken)
await localStorage.setItem('refresh', data.refreshToken)
await localStorage.setItem('currentUser', JSON.stringify(updateUser))
isRefreshing = false
onAccessTokenFetched() // 处理重试队列
})
.catch((err) => {
const { response } = err
const { data } = response
const { code } = data
if (+code === 401) { // refreshToken过期或者无效 跳转登录页面
// 401 跳登录页
const { pathname } = location
pathname !== '/user/login' && location.replace('/user/login')
}
})
}
nestjs 处理
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { decrypt } from '@src/utils/secret';
import { verify } from 'jsonwebtoken';
import { REDIS_DB_0 } from '@src/modules/redis/redis';
import { Reflector } from '@nestjs/core';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const noAuth = this.reflector.get<boolean>('no-auth', context.getHandler());
if (noAuth) {
return true;
}
const accessToken =
context.switchToRpc().getData().headers['access-token'] ||
request.headers['access-token'] ||
request.body.token ||
request.query.token ||
request.params.token;
if (accessToken) {
let userInfo, access_token;
try {
access_token = decrypt(accessToken);
userInfo = verify(access_token, process.env.TOKEN_SECRET);
} catch {
throw new UnauthorizedException('token失效');
}
const REDIS_REFRESH_TOKEN = await REDIS_DB_0.get(userInfo.id);
if (accessToken !== REDIS_REFRESH_TOKEN) {
// 判断接收的accessToken是否和redis保存的一致
throw new UnauthorizedException('token失效');
}
try {
const decodeAccessToken: any = verify(
access_token,
process.env.TOKEN_SECRET,
);
// 解析access_token信息并存放到request.headers中
request.headers.user = decodeAccessToken;
} catch (err) {
const { expiredAt } = err;
throw new UnauthorizedException(
`token已于${new Date(+expiredAt).toLocaleString()}失效`,
);
}
} else {
throw new UnauthorizedException('token失效');
}
return true;
}
}
import {
Body,
Controller,
Get,
Post,
Query,
Request,
UnauthorizedException,
} from '@nestjs/common';
import { decrypt, encrypt } from '@src/utils/secret';
import { UserDto } from '../user/user.dto';
import { LoginService } from './login.service';
import { LoginRes } from './types';
import { verify, sign, JwtPayload } from 'jsonwebtoken';
import { REDIS_DB_0, REDIS_DB_1 } from '../redis/redis';
import { UserService } from '../user/user.service';
import { instanceToPlain } from 'class-transformer';
import { NoAuth } from '@src/utils/tools';
interface JwtParams extends JwtPayload {
id?: string;
type?: string;
}
@Controller('sign')
export class LoginController {
constructor(
private readonly loginService: LoginService,
private readonly userService: UserService,
) {}
@NoAuth()
@Post('login')
async login(@Body() userDto: UserDto): Promise<LoginRes> {
const user = await this.userService.findUser(userDto);
if (!user) {
throw new BadRequestException(`用户${userDto.name}未注册~`);
}
const isPwdCorrect = checkPassword(userDto.password, user.password);
if (!isPwdCorrect) {
throw new BadRequestException(`账号密码输入错误, 请修改后重试~`);
}
const userPlain = instanceToPlain(user);
const accessToken = sign(userPlain, process.env.TOKEN_SECRET, {
expiresIn: 60 * 60,
});
const refreshToken = sign(
{ type: 'refresh', id: userPlain.id },
process.env.TOKEN_SECRET,
{
expiresIn: '7d',
},
);
const nToken = encrypt(accessToken);
const nRefreshToken = encrypt(refreshToken);
await REDIS_DB_0.set(userPlain.id, nToken);
await REDIS_DB_1.set(userPlain.id, nRefreshToken);
return {
...user,
expiresOut: ~~((Date.now() + 60 * 60 * 1000) / 1000),
accessToken: nToken,
refreshToken: nRefreshToken,
};
}
@NoAuth()
@Get('refresh')
async refresh(
@Request() request,
@Query('refreshToken') refreshToken: string,
): Promise<any> {
if (refreshToken) {
try {
// 解密token
const refresh_token = await decrypt(refreshToken);
// 处理jwt信息
const decodeRefreshToken: string | JwtParams = await verify(
refresh_token,
process.env.TOKEN_SECRET,
);
const { id } = decodeRefreshToken as JwtParams;
const REDIS_REFRESH_TOKEN = await REDIS_DB_1.get(id); // 获取redis中储存的refreshToken
if (refreshToken !== REDIS_REFRESH_TOKEN) {
// 判断接收的refreshToken是否和redis保存的一致
throw new UnauthorizedException();
}
// 获取用户信息
const userInfo = await this.userService.findUserById({ id });
// 生成新的ACCESS_TOKEN、REFRESH_TOKEN
const newAccessToken = sign(
instanceToPlain(userInfo),
process.env.TOKEN_SECRET,
{
expiresIn: 60 * 60,
},
);
const newRefreshToken = sign(
{ type: 'refresh', id: userInfo.id },
process.env.TOKEN_SECRET,
{
expiresIn: '7d',
},
);
const nToken = encrypt(newAccessToken);
const nRefreshToken = encrypt(newRefreshToken);
await REDIS_DB_0.set(userInfo.id, nToken);
await REDIS_DB_1.set(userInfo.id, nRefreshToken);
return {
...userInfo,
expiresOut: ~~((Date.now() + 60 * 60 * 1000) / 1000),
accessToken: nToken,
refreshToken: nRefreshToken,
};
} catch (err) {
throw new UnauthorizedException();
}
} else {
throw new UnauthorizedException();
}
}
}