Nestjs使用Redis的最佳实践

前几天在项目中有用到Redis + JWT实现服务端对token的主动删除(退出登录功能)。故此介绍下如何在Nestjs中使用Redis,并做下总结。

知识准备

  1. 了解Redis - 网上很多简介。
  2. 了解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. 参考

  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. 配置环境变量

  1. .env

    REDIS

    REDIS_HOST=localhost
    REDIS_PORT=6379
    REDIS_DB=test
    // 本地没有密码
    REDIS_PASSWORD=123456

    redis存储时的前缀 公司:项目:功能

    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失效

背景:

  1. 当退出登录时jwt的token并未失效
  2. 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;
  }
}

到此,大工告成。

相关推荐
IT项目管理1 小时前
达梦数据库DMHS介绍及安装部署
linux·数据库
你都会上树?1 小时前
MySQL MVCC 详解
数据库·mysql
大春儿的试验田1 小时前
高并发收藏功能设计:Redis异步同步与定时补偿机制详解
java·数据库·redis·学习·缓存
likeGhee1 小时前
python缓存装饰器实现方案
开发语言·python·缓存
hqxstudying1 小时前
Redis为什么是单线程
java·redis
C182981825752 小时前
OOM电商系统订单缓存泄漏,这是泄漏还是溢出
java·spring·缓存
Ein hübscher Kerl.2 小时前
虚拟机上安装 MariaDB 及依赖包
数据库·mariadb
醇醛酸醚酮酯2 小时前
Qt项目锻炼——TODO清单(二)
开发语言·数据库·qt
GreatSQL社区3 小时前
用systemd管理GreatSQL服务详解
数据库·mysql·greatsql
掘根3 小时前
【MySQL进阶】错误日志,二进制日志,mysql系统库
数据库·mysql