上一篇文章我们通过 JWT 生成了 token 返回给前端进行鉴权,并且给 token 设置了过期时间。同时留下了一个问题:如果用户一直在操作页面,一直在调用我们的接口,过期时间一到突然让用户退出重新登录显然是不合理的,所以这一节给大家介绍在用户操作页面的过程中如何给 token 续期。
做过后端的同学可能会知道可以使用双 token 进行鉴权:
用户登录成功后,生成两个不同的 token:
Access Token和Refresh Token返回给前端。Access Token用于访问受保护的资源,通常具有较短的有效期。Refresh Token用于获取新的Access Token,通常具有较长的有效期。后端可以验证Access Token是否有效,如果Access Token失效,则告诉前端使用Refresh Token调用刷新 token 接口获取新的Access Token和Refresh Token。
这种方案当然是可行的,但是需要维护两个 token,前后端都有些繁琐,那么有没有只返回给前端一个 token 的同时实现续期呢? 当然是有的,我们的项目中也会采取这种方案实现 token 续期。
具体实现逻辑如下:
用户登录成功后,后端生成 token 后,将 token 以
{token:token}的形式存入 redis 或者 session 中并将 token 返回前端。验证 token 的时候根据前端传来的 token 从缓存中取得 token 值进行 JWT 验证并获取到 token 的过期时间
当
过期时间-当前时间小于某个值的时候我们重新进行 JWT 注册生成新的 newToken 并以{token:newToken}的形式存如 redis 或者 session 中。那么只要我们在
过期时间-当前时间内调用过需要鉴权的接口,那么真正的newToken就会刷新过期时间
下面我们来看一下在项目中的具体实现
在user.service.ts中,用户登录的时候,将 token 存入 redis,同时为了便于测试我们先将验证码逻辑注释
然后在 token 验证逻辑user.guard.ts先从 redis 中拿到缓存的 token 值进行 token 验证,验证不同则返回验证不通过。验证通过获取到过期时间于当前时间做对比,如果小于一小时(这里便于测试先写 30 秒)就重新生成 token,并以前端传来的 token 为 key,新生成的 token 为 value 存入 redis,同时打印一下间隔时间
同样的为了测试先将 app.module.ts 中 jwt 的过期时间改为 60 秒
那么此时登录的逻辑就是只要在30s~60s之间调用过接口那么 token 就会重新注册,否则 token 就会过期
我们试一下效果,先调用登录接口获取到 token
然后将 token 放到测试接口/user/test头里,请求一下发现 token 验证通过了
看下控制台打印了间隔时间及是否小于 30 秒
等待一会,30s~60s之后再调用两次发现间隔时间小于 30s ,那么 token 重新注册,过期时间就重新计算了
如果用户 60s 没都没调用接口那么 token 便真的过期了
相关代码如下:
- user.service.ts
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { User } from './entities/user.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ApiException } from 'src/common/filter/http-exception/api.exception';
import { ApiErrorCode } from 'src/common/enums/api-error-code.enum';
import { LoginDto } from './dto/login-dto';
import { JwtService } from '@nestjs/jwt';
import encry from '../utils/crypto';
import generateCaptcha from 'src/utils/generateCaptcha';
import { CacheService } from 'src/cache/cache.service';
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
private jwtService: JwtService,
private cacheService: CacheService,
) {}
//注册
async create(createUserDto: CreateUserDto) {
const { username, password, captcha, id } = createUserDto;
//缓存的验证码
const cacheCaptcha = await this.cacheService.get(id);
if (captcha !== cacheCaptcha) {
throw new ApiException('验证码错误', ApiErrorCode.COMMON_CODE);
}
const existUser = await this.userRepository.findOne({
where: { username },
});
if (existUser)
throw new ApiException('用户已存在', ApiErrorCode.USER_EXIST);
try {
const newUser = new User();
newUser.username = username;
newUser.password = password;
await this.userRepository.save(newUser);
return '注册成功';
} catch (error) {
throw new HttpException(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
async findOne(username: string) {
const user = await this.userRepository.findOne({
where: { username },
});
if (!user)
throw new ApiException('用户名不存在', ApiErrorCode.USER_NOTEXIST);
return user;
}
async login(loginDto: LoginDto) {
const { username, password, captcha, id } = loginDto;
//缓存的验证码
const cacheCaptcha = await this.cacheService.get(id);
// if (captcha !== cacheCaptcha) {
// throw new ApiException('验证码错误', ApiErrorCode.COMMON_CODE);
// }
const user = await this.findOne(username);
if (user.password !== encry(password, user.salt)) {
throw new ApiException('密码错误', ApiErrorCode.PASSWORD_ERR);
}
const payload = { username: user.username, sub: user.id };
const token = await this.jwtService.signAsync(payload);
this.cacheService.set(token, token, 7200);
return token;
}
getCaptcha() {
const { id, captcha } = generateCaptcha();
this.cacheService.set(id, captcha.text, 60);
console.log(captcha.text);
return { id, img: captcha.data };
}
}
- user.guard.ts
import {
CanActivate,
ExecutionContext,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';
import { Request } from 'express';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { Reflector } from '@nestjs/core';
import { CacheService } from 'src/cache/cache.service';
@Injectable()
export class UserGuard implements CanActivate {
constructor(
private jwtService: JwtService, // JWT服务,用于验证和解析JWT token
private configService: ConfigService, // 配置服务,用于获取JWT_SECRET
private reflector: Reflector,
private cacheService: CacheService,
) {}
/**
* 判断请求是否通过身份验证
* @param context 执行上下文
* @returns 是否通过身份验证
*/
async canActivate(context: ExecutionContext): Promise<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
//即将调用的方法
context.getHandler(),
//controller类型
context.getClass(),
]);
if (isPublic) {
return true;
}
const request = context.switchToHttp().getRequest(); // 获取请求对象
const token = this.extractTokenFromHeader(request); // 从请求头中提取token
if (!token) {
throw new HttpException('验证不通过', HttpStatus.FORBIDDEN); // 如果没有token,抛出验证不通过异常
}
const realToken = await this.cacheService.get(token);
try {
const payload = await this.jwtService.verifyAsync(realToken, {
secret: this.configService.get('JWT_SECRET'), // 使用JWT_SECRET解析token
});
//获取token过期时间
const { exp } = payload;
const nowTime = Math.floor(new Date().getTime() / 1000);
console.log(exp - nowTime);
const isExpired = exp - nowTime < 3600;
console.log(isExpired);
if (isExpired) {
const newPayLoad = { username: payload.username, sub: payload.sub };
const newToken = await this.jwtService.sign(newPayLoad);
this.cacheService.set(token, newToken, 7200);
}
request['user'] = payload; // 将解析后的用户信息存储在请求对象中
} catch {
throw new HttpException('token验证失败', HttpStatus.FORBIDDEN); // token验证失败,抛出异常
}
return true; // 身份验证通过
}
/**
* 从请求头中提取token
* @param request 请求对象
* @returns 提取到的token
*/
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? []; // 从Authorization头中提取token
return type === 'Bearer' ? token : undefined; // 如果是Bearer类型的token,返回token;否则返回undefined
}
}
完整代码地址:redis 实现 token 续期
后续将移除JWT,以Redis实现