前几天在项目中有用到Redis + JWT实现服务端对token的主动删除(退出登录功能)。故此介绍下如何在Nestjs中使用Redis,并做下总结。
知识准备
- 了解Redis - 网上很多简介。
- 了解Nestjs如何使用jwt生成token - 可移步看下我之前的文章
效果展示
一、mac安装与使用
示例代码用的本地的redis,所以介绍下mac如何安装redis和查看数据。
1. 安装
// 安装Redis
brew install redis
// 启动Redis -(这将作为后台服务运行)
brew services start redis
// 或者,用redis-server命令+路径来启动(关闭终端服务停止)
redis-server /usr/local/etc/redis.conf
// 验证Redis是否运行 (如果返回PONG,则表示Redis服务器正在运行)
redis-cli ping
// 停止redis
brew services stop redis
2. mac使用 RedisInsight
官网下载 https://redis.io/insight/#insight-form
效果图如下
二、在nestjs中简单使用
1. 参考
- redis实现token过期:https://juejin.cn/post/7260308502031433786
2. 装包
pnpm install @nestjs/cache-manager cache-manager cache-manager-redis-yet redis -S
pnpm install @types/cache-manager -D
3. 配置环境变量
-
.env
REDIS
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DB=test
// 本地没有密码
REDIS_PASSWORD=123456redis存储时的前缀 公司:项目:功能
REDIS_PREFIX=vobile:video-watermark-saas-node
2. 配置config, src/config/config.ts
export default () => {
return {
// ....
redis: {
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT, 10),
// username: process.env.DATABASE_USERNAME,
password: process.env.REDIS_PASSWORD,
database: process.env.REDIS_DB,
perfix: process.env.REDIS_PREFIX,
},
};
};
4. 创建目录使用
1. 创建目录
nest g resource modules/redis
2. 处理redis.module
import { Module, Global } from '@nestjs/common';
import { RedisService } from './redis.service';
import { RedisController } from './redis.controller';
import { CacheModule } from '@nestjs/cache-manager';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { redisStore } from 'cache-manager-redis-yet';
import type { RedisClientOptions } from 'redis';
@Global() // 这里我们使用@Global 装饰器让这个模块变成全局的
@Module({
controllers: [RedisController],
providers: [RedisService],
imports: [
CacheModule.registerAsync<RedisClientOptions>({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => {
const store = await redisStore({
socket: {
host: configService.get<string>('redis.host'),
port: configService.get<number>('redis.port'),
},
});
return {
store,
};
},
}),
],
exports: [RedisService],
})
export class RedisModule { }
3. 处理service
import { Inject, Injectable } from '@nestjs/common';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
import { formatSuccess } from 'src/util';
@Injectable()
export class RedisService {
constructor(@Inject(CACHE_MANAGER) private readonly cacheManager: Cache) { }
async get<T>(key: string): Promise<T> {
return await this.cacheManager.get(key);
}
async set(key: string, value: any, ttl?: number): Promise<void> {
console.log('set===');
const res = await this.cacheManager.set(key, value, ttl);
console.log('res1: ', res);
return res;
}
async testredis() {
const res = await this.set('aaa1', 'aaa', 60 * 60 * 1000);
console.log('res: ', res);
return formatSuccess('aa')
}
}
4. 处理controller
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { RedisService } from './redis.service';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { Public } from '../auth/decorators/public.decorator';
@ApiTags('redis')
@Controller('redis')
export class RedisController {
constructor(private readonly redisService: RedisService) { }
// 测试redis
@ApiOperation({ summary: '测试redis', description: '测试redis' })
@Public()
@Post('testredis')
testredis() {
return this.redisService.testredis();
}
}
简单的配置到此结束,测试正常
三、进阶:退出登录token失效
背景:
- 当退出登录时jwt的token并未失效
- token无法被服务器主动作废
环境变量、创建目录参考上方。
下边展示核心
1. redis.service中增加删除功能
import { Inject, Injectable } from '@nestjs/common';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
import { formatSuccess } from 'src/util';
@Injectable()
export class RedisService {
constructor(@Inject(CACHE_MANAGER) private readonly cacheManager: Cache) { }
async get<T>(key: string): Promise<T> {
return await this.cacheManager.get(key);
}
async set(key: string, value: any, ttl?: number): Promise<void> {
return await this.cacheManager.set(key, value, ttl);
}
// 删除
async del(key: string): Promise<void> {
return await this.cacheManager.del(key);
}
async testredis() {
await this.set('aaa2', 'aaa', 60 * 60 * 1000);
return formatSuccess('ok');
}
}
2. 生成token时存入redis,退出登录删除token
auth.service,1.登录生成token后存入redis 2.退出登录删除redis里的token
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { UserService } from '../user/user.service';
import * as md5 from 'md5';
import { JwtService } from '@nestjs/jwt';
import { formatError, formatSuccess } from 'src/util';
import { CreateUserDto } from '../user/dto/create-user.dto';
import { RedisService } from '../redis/redis.service'
import { ConfigService } from '@nestjs/config';
@Injectable()
export class AuthService {
constructor(
private userService: UserService,
private jwtService: JwtService,
private redisService: RedisService,
private configService: ConfigService,
) { }
// 登录
async signIn(createUserDto: CreateUserDto): Promise<any> {
const user: any = await this.userService.findOne(createUserDto.name);
if (!user) return formatError({ msg: 'The user does not exist' });
if (user?.password !== md5(createUserDto.password)) return formatError({ msg: 'wrong password' });
// 生成token
const payload = { id: user?.id, name: user?.name, password: user?.password };
const token = await this.jwtService.signAsync(payload);
// 将token存入redis
await this.redisService.set(`${this.configService.get('redis.perfix')}:token_${user?.id}`, token, 30 * 24 * 60 * 60 * 1000);
return formatSuccess({
token,
userInfo: {
id: user?.id,
name: user?.name,
},
});
}
// 退出登录
async logout(userid) {
this.redisService.del(`${this.configService.get('redis.perfix')}:token_${userid}`)
return formatSuccess('logout success')
}
}
添加退出登录接口
auth.controller.ts
// 退出登录
@ApiOperation({ summary: '退出登录' })
@Post('logout')
logout(@Body() body: any, @Request() req: any) {
return this.authService.logout(req?.user?.id);
}
3. jwt鉴权时比对redis里的token
修改:auth.guard.ts
// ...
import { ConfigService } from '@nestjs/config';
import { RedisService } from '../redis/redis.service'
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
// ...
private readonly configService: ConfigService,
private redisService: RedisService,
) { }
async canActivate(context: ExecutionContext): Promise<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) return true;
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
console.log('token1111: ', token);
if (!token) throw new UnauthorizedException();
try {
const payload = await this.jwtService.verifyAsync(
token,
{ secret: this.configService.get('jwt.secret') },
);
r
request['user'] = payload;
} catch {
throw new UnauthorizedException();
}
return true;
}
}
到此,大工告成。