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;
  }
}

到此,大工告成。

相关推荐
搬码后生仔2 分钟前
SQLite 是一个轻量级的嵌入式数据库,不需要安装服务器,直接使用文件即可。
数据库·sqlite
码农君莫笑3 分钟前
Blazor项目中使用EF读写 SQLite 数据库
linux·数据库·sqlite·c#·.netcore·人机交互·visual studio
江上挽风&sty5 分钟前
【Django篇】--动手实践Django基础知识
数据库·django·sqlite
向阳12189 分钟前
mybatis 动态 SQL
数据库·sql·mybatis
胡图蛋.10 分钟前
什么是事务
数据库
小黄人软件13 分钟前
20241220流水的日报 mysql的between可以用于字符串 sql 所有老日期的,保留最新日期
数据库·sql·mysql
张声录118 分钟前
【ETCD】【实操篇(三)】【ETCDCTL】如何向集群中写入数据
数据库·chrome·etcd
无为之士24 分钟前
Linux自动备份Mysql数据库
linux·数据库·mysql
小汤猿人类37 分钟前
open Feign 连接池(性能提升)
数据库
呆呆小雅1 小时前
C#关键字volatile
java·redis·c#