NestJs服务搭建步骤记录(三):jwt、redis、单点登录示例

一、jwt使用实例

  1. 安装依赖包
$ yarn add passport @nestjs/jwt passport-jwt @types/passport-jwt @nestjs/passport 

2.保持干净地模块化,新建auth模块

$ nest g mo auth
$ nest g service auth
$ nest g co auth

相关文件:
auth.service.ts

/*
 * @Author: FORMAT-qi
 * @Date: 2022-05-02 10:29:28
 * @LastEditors: FORMAT-qi
 * @LastEditTime: 2022-05-02 13:09:27
 * @Description:
 */
import { Injectable, Logger } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { IResponse } from "src/interfaces/response.interface";
import { User } from "src/interfaces/user.interface";
import { encrypt } from "src/utils/encryption";
import { UserService } from "../user/user.service";

const logger = new Logger("auth.service");

@Injectable()
export class AuthService {
  private response: IResponse;
  constructor(
    private readonly userService: UserService,
    private jwtService: JwtService
  ) {}

  /**
   * @description: 用户登录
   * @param {User} user
   * @return {*}
   */
  public async login(user: User) {
    try {
      const username: string = user.username;
      const password: string = user.password;
      const userInfo = await this.userService.findOneByUsername(username);
      if (!userInfo) {
        this.response = { code: -1, msg: "用户不存在" };
        return;
      }
      const pass = encrypt(password, userInfo.salt);
      if (pass === userInfo.password) {
        this.response = {
          code: 1,
          msg: "登录成功",
          data: {
            userId: `${userInfo._id}`,
            token: await this.createToken(userInfo),
          },
        };
      } else {
        this.response = { code: -1, msg: "用户名密码错误" };
      }
    } catch (error) {
      this.response = { code: -1, msg: "登录失败" };
      logger.log(error);
    } finally {
      return this.response;
    }
  }

  /**
   * @description:创建token
   * @param {User} user
   * @return {*}
   */
  private async createToken(user: User) {
    const payload = { username: user.username, userId: `${user._id}` };
    return this.jwtService.sign(payload);
  }
}

auth.controller.ts

/*
 * @Author: FORMAT-qi
 * @Date: 2022-05-02 10:29:42
 * @LastEditors: FORMAT-qi
 * @LastEditTime: 2022-05-02 12:50:39
 * @Description:
 */
import { Body, Controller, Post } from "@nestjs/common";
import { ApiOperation, ApiTags } from "@nestjs/swagger";
import { User } from "src/interfaces/user.interface";
import { AuthService } from "./auth.service";
import { SkipAuth } from "./jwt-auth.guard";

@Controller("auth")
@ApiTags("用户验证")
export class AuthController {
  constructor(private authService: AuthService) {}
  // 登录
  @SkipAuth()
  @Post("login")
  @ApiOperation({
    summary: "用户登录",
  })
  async loginUser(@Body() userDto: User) {
    return await this.authService.login(userDto);
  }
}

auth.module.ts

/*
 * @Author: FORMAT-qi
 * @Date: 2022-05-02 10:27:57
 * @LastEditors: FORMAT-qi
 * @LastEditTime: 2022-05-02 12:42:52
 * @Description:
 */
import { Module } from "@nestjs/common";
import { APP_GUARD } from "@nestjs/core";
import { JwtModule } from "@nestjs/jwt";
import { UserService } from "../user/user.service";
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";
import { JwtAuthGuard } from "./jwt-auth.guard";
import { JWT_CONSTANT } from "./jwt.constant";
import { JwtStrategy } from "./jwt.strategy";

@Module({
  imports: [
    JwtModule.register({
      secret: JWT_CONSTANT.secret,
    }),
  ],
  providers: [
    AuthService,
    UserService,
    JwtStrategy,
    {
      provide: APP_GUARD,
      useClass: JwtAuthGuard, //挂载全局接口
    },
  ],
  controllers: [AuthController],
})
export class AuthModule {}

jwt-auth.guard.ts:JWT全局守卫。@SkipAuth 跳过JWT验证

/*
 * @Author: FORMAT-qi
 * @Date: 2022-05-02 11:45:36
 * @LastEditors: FORMAT-qi
 * @LastEditTime: 2022-05-02 12:51:33
 * @Description:
 */
import {
  ExecutionContext,
  Injectable,
  UnauthorizedException,
  SetMetadata,
} from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { AuthGuard } from "@nestjs/passport";

@Injectable()
export class JwtAuthGuard extends AuthGuard("jwt") {
  constructor(private reflector: Reflector) {
    super();
  }
  canActivate(context: ExecutionContext) {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (isPublic) {
      return true;
    }
    return super.canActivate(context);
  }

  handleRequest(err, user, info) {
    // You can throw an exception based on either "info" or "err" arguments
    if (err || !user) {
      throw err || new UnauthorizedException();
    }
    return user;
  }
}
// 生成跳过检测装饰器
export const IS_PUBLIC_KEY = "isPublic";
export const SkipAuth = () => SetMetadata(IS_PUBLIC_KEY, true);

jwt.constant.ts : JWT配置文件secret为加密的key //

export const JWT_CONSTANT = {
  secret: "NVGJb1$Z^xytmAvf",  
};

jwt.strategy.ts

/*
 * @Author: FORMAT-qi
 * @Date: 2022-05-02 11:13:01
 * @LastEditors: FORMAT-qi
 * @LastEditTime: 2022-05-02 11:58:32
 * @Description:
 */
import { ExtractJwt, Strategy } from "passport-jwt";
import { PassportStrategy } from "@nestjs/passport";
import { Injectable } from "@nestjs/common";
import { JWT_CONSTANT } from "./jwt.constant";
import { User } from "src/interfaces/user.interface";

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: JWT_CONSTANT.secret,
    });
  }

  async validate(payload: User) {
    return { userId: `${payload._id}`, username: payload.username };
  }
}

==swager修改 ==
// main.ts

  new DocumentBuilder() 
   .addBearerAuth()
    .build();
 

需要验证token的controller中增加装饰器@ApiBearerAuth,增加后swager请求header会携带Authorization参数。
@ApiBearerAuth() // Swagger 的 JWT 验证
效果图:

无token
在这里插入图片描述

有token
在这里插入图片描述
跳过验证的接口:login
在这里插入图片描述

二、使用redis,增加token过期和单点登录

1.安装redis依赖

$ yarn add ioredis

2.增加redis.ts工具类

/*
 * @Author: FORMAT-qi
 * @Date: 2022-05-02 14:44:41
 * @LastEditors: FORMAT-qi
 * @LastEditTime: 2022-05-02 15:16:03
 * @Description:
 */
import { Logger } from "@nestjs/common";
import Redis from "ioredis";

const logger = new Logger("auth.service");
const redisIndex = []; // 用于记录 redis 实例索引
const redisList = []; // 用于存储 redis 实例
const redisOption = {
  host: "xxx.xxx.xx.x",
  port: xxxx,
  password: "xxxxxxxx",
};
export class RedisInstance {
  static async initRedis(method: string, db = 0) {
    const isExist = redisIndex.some((x) => x === db);
    if (!isExist) {
      Logger.debug(
        `[Redis ${db}]来自 ${method} 方法调用 `
      );
      redisList[db] = new Redis({ ...redisOption, db });
      redisIndex.push(db);
    } else {
      Logger.debug(`[Redis ${db}]来自 ${method} 方法调用`);
    }
    return redisList[db];
  }

  static async setRedis(
    method: string,
    db = 0,
    key: string,
    val: string,
    timeout = 60 * 60
  ) {
    if (typeof val == "object") {
      val = JSON.stringify(val);
    }
    const redis = await RedisInstance.initRedis(method, db);
    redis.set(`${key}`, val);
    redis.expire(`${key}`, timeout);
  }
  static async getRedis(method: string, db = 0, key: string) {
    return new Promise(async (resolve, reject) => {
      const redis = await RedisInstance.initRedis(method, db);
      redis.get(`${key}`, (err, val) => {
        if (err) {
          reject(err);
          return;
        }
        resolve(val);
      });
    });
  }
}

auth.service.ts //增加生成token后存入redis,用户ID和当key,保证用户只有一个token

     //login方法中 ....
	if (pass === userInfo.password) {
        const token = await this.createToken(userInfo);

        //存储token到redis
        const redis = await RedisInstance.initRedis("auth.login", 0);
        const key = `${userInfo._id}-${user.username}`;
        await RedisInstance.setRedis("auth.login", 0, key, `${token}`);

        this.response = {
          code: 1,
          msg: "登录成功",
          data: {
            userId: `${userInfo._id}`,
            token,
          },
        };
      } else {
        this.response = { code: -1, msg: "用户名密码错误" };
      }

修改 jwt-auth.guard.ts 中 canActivate方法

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (isPublic) {
      return true;
    }
    // token对比
    const request = context.switchToHttp().getRequest();
    const authorization = request["headers"].authorization || void 0;
    let tokenNotTimeOut = true;
    if (authorization) {
      const token = authorization.split(" ")[1]; // authorization: Bearer xxx
      try {
        let payload: any = this.jwtService.decode(token);
        const key = `${payload.userId}-${payload.username}`;
        const redis_token = await RedisInstance.getRedis(
          "jwt-auth.guard.canActivate",
          0,
          key
        );
        if (!redis_token || redis_token !== token) {
          throw new UnauthorizedException("请重新登录");
        }
      } catch (err) {
        tokenNotTimeOut = false;
        throw new UnauthorizedException("请重新登录");
      }
    }
    return tokenNotTimeOut && (super.canActivate(context) as boolean);
  }

效果登陆后:redis中
在这里插入图片描述
修改token后(删除redis中token,修改值的等操作)请求结果
在这里插入图片描述
@干饭,明天继续

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值