当前位置:   article > 正文

Nestjs使用Redis的最佳实践_nestjs redis

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

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
  • 1
  • 2

3. 配置环境变量

  1. .env
# REDIS
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DB=test
// 本地没有密码
REDIS_PASSWORD=123456
# redis存储时的前缀 公司:项目:功能
REDIS_PREFIX=vobile:video-watermark-saas-node
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

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,
    },
  };
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

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 { }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

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')
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

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();
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

简单的配置到此结束,测试正常
在这里插入图片描述

三、进阶:退出登录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');
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

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')
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

添加退出登录接口
auth.controller.ts

  // 退出登录
  @ApiOperation({ summary: '退出登录' })
  @Post('logout')
  logout(@Body() body: any, @Request() req: any) {
    return this.authService.logout(req?.user?.id);
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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;
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

到此,大工告成。
在这里插入图片描述

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/一键难忘520/article/detail/923177
推荐阅读
相关标签
  

闽ICP备14008679号