nestJS项目-用户权限认证登录

安装nestjs

npm i -g @nestjs/cli

创建项目

nest new project-name

可以选择npm、yarn、npmp,这里选择yarn
image.png
再依赖安装完毕之后,可以使用如下命令启动 NestJS 应用,然后浏览器即可访问 http://localhost:3000/ :出现如下界面即代表项目已经正常启动了。

数据库

安装依赖

选用mysql数据库,安装数据库依赖。

yarn add mysql typeorm @nestjs/typeorm

配置数据库

配置数据库,在app.module.ts

// 根模块用于处理其他类的引用与共享。
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './user/user.module';
import { AuthModule } from './auth/auth.module';
import { MenuModule } from './menu/menu.module';
import { RoleModule } from './role/role.module';
import { UserRoleModule } from './user_role/user_role.module';
import { join } from 'path';

@Module({
  imports: [
    // 加载连接数据库
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: '123456',
      database: 'vite_node',
      entities: [__dirname + '/**/*.entity{.ts,.js}'],
      synchronize: true,
      logging: false,
    }),
    // 子模块
    UserModule,
    AuthModule,
    MenuModule,
    RoleModule,
    UserRoleModule,
  ],
  controllers: [AppController],
  providers: [AppService],
  
})
export class AppModule {}

数据表

以下是基础数据表
1676528065(1).jpg
menu.sqlrole.sqlrole_menu.sqluser.sqluser_role.sql

使用swagger

安装依赖

yarn add @nestjs/swagger swagger-ui-express

配置swagger

nest-cli.jsonmain.ts文件中进行配置,然后启动项目访问http:localhost:3001/docs 即可。

{
  "$schema": "https://json.schemastore.org/nest-cli",
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "generateOptions": {
    "spec": false
  },
  "compilerOptions": {
    "plugins": ["@nestjs/swagger"]
  }
}
// 应用程序入口文件。它使用 NestFactory 用来创建 Nest 应用实例。
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
// Swagger 模块
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { NestExpressApplication } from '@nestjs/platform-express';

//配置 swagger
const setupSwagger = (app) => {
  const config = new DocumentBuilder()
    .addBearerAuth()
    .setTitle('NEST API')
    .setDescription('nest-test的 API 文档')
    .setVersion('1.0')
    .build();
  // 创建
  const document = SwaggerModule.createDocument(app, config);
  // 启动
  SwaggerModule.setup('doc', app, document, {
    swaggerOptions: {
      persistAuthorization: true,
    },
  });
};
async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  // 传入app 访问http:localhost:3001/doc
  setupSwagger(app);
  await app.listen(3001);
}
bootstrap();

根据数据库数据表生成entity-typeorm-model-generator

"db": "npx typeorm-model-generator -h localhost -d vite_node -p 3306 -u root -x 123456 -e mysql -o ./src/entities --noConfig true --ce pascal --cp camel -a"
  • npx typeorm-model-generator :如果全局安装了,npx就不用
  • -h localhost :ip
  • -d vite_node :数据库名字
  • -p 3306 :数据库端口号
  • -u root :用户名
  • -x 123456 :密码
  • -e mysql :什么数据库
  • -o ./src/entities :生成实体的地址
  • –noConfig true :表示不生成ormconfig.jsontsconfig.json文件
  • –ce pascal :类名转换成首字母大写的驼峰命名
  • –cp camel :将数据库中的字段也转换成驼峰命名,比如user_role转换成userRole
  • -a:表示实体会继承BaseEntity的类

类如User,关于一些注解的说明。

  • @PrimaryGeneratedColumn:主键id
  • @ApiProperty:关于这个字段的说明
  • @Column
    • nullable:是否可以为空。
    • name:此字段表示在数据库中的列名,类如驼峰命名转换:login_name ——> loginaName。
    • select:查询时是否返回此字段
  • @Exclude:使用此注解时,可以实现对返回的数据过滤掉此列,需要在controller层对某一个方法上加上注解 @UseInterceptors(ClassSerializerInterceptor)对应@Exclude()列,表示此列隐藏。
  • @OneToMany:一对多的关系
import {
  BaseEntity,
  Column,
  Entity,
  OneToMany,
  PrimaryGeneratedColumn,
} from "typeorm";
import { ApiProperty } from '@nestjs/swagger';
import { UserRole } from "./UserRole.entity";
import { Exclude } from "class-transformer";
import { IsEnum } from "class-validator";

@Entity("user", { schema: "vite_node" })
export class User extends BaseEntity {
  @ApiProperty({ description: '自增 id' })
  @PrimaryGeneratedColumn({ type: "int", name: "id" })
  id: number;

  // 表示select: false 查询user时不会返回这个字段,隐藏此列,也可以使用
 // @Column({ select: false })
  @ApiProperty({ description: '密码' })
  @Exclude()
  @Column("varchar", { name: "password", length: 255 })
  password: string;

  @ApiProperty({ description: '性别',  example: 1,  required: false,  enum: [0, 1], }) 
  @IsEnum(
    {: 0,: 1 },
    {
      message: ' sex 只能传入数字0或1',
    },
  )
  @Column("int", { default: 1, name: "sex",})
  sex?: number;

  @ApiProperty({ description: '邮箱',  required: false })
  @Column("varchar", { name: "email", nullable: true, length: 255 })
  email: string | null;

  @ApiProperty({ description: '地址' })
  @Column("varchar", { name: "address", nullable: true, length: 255 })
  address: string | null;

  @ApiProperty({ description: '登录名' })
  @Column("varchar", { name: "login_name", length: 255 })
  loginName: string;

  @ApiProperty({ description: '用户名' })
  @Column("varchar", { name: "user_name", length: 255 })
  userName: string;

  @OneToMany(() => UserRole, (userRole) => userRole.user)
  userRoles: UserRole[];
}

创建资源 CRUD 生成器

这里先创建user表的

nest g resource [name]
// 简写 res
nest g res user 

image.png]

登录-权限-认证-守卫 jwt

安装依赖

  • @nestjs/passport:模块将该框架包装在一个 Nest 风格的包中,使其易于集成到 Nest 应用程序中。
  • @nestjs/jwt :身份认证
  • @types/passport-jwt:编写 TypeScript 代码时提供了帮助
  • passport-jwt:策略包(还有passport-local)
  • passport:是node目前最流行的身份认证库,与使用@nestjs/passport的nestjs结合使用非常简单。

策略有本地策略和jwt策略,这里使用的是jwt

yarn add @nestjs/jwt passport-jwt @types/passport-jwt @nestjs/passport passport

创建auth相关模块(module、service、controller)

nest g mo auth
nest g s auth
nest g c auth

image.png
AuthService中需要进行登录验证的时候需要操作UserService,所以在UserModule的exports中将其暴露出去,以供其他模块使用。然后在AuthModule的providers中添加UserService

import { AuthModule } from '../auth/auth.module';
import { UserRole } from '../entities/UserRole.entity';
import {
  Module,
  NestModule,
  MiddlewareConsumer,
  RequestMethod,
  forwardRef,
} from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'
import { User } from '../entities/User.entity';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { LoggerMiddleware } from '../middle-ware/logger.middleware';

@Module({
  imports: [TypeOrmModule.forFeature([User, UserRole])],
  controllers: [UserController],
  providers: [UserService],
  // 将 UserService 暴露出去
  exports: [UserService]
})
export class UserModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes({ path: 'user', method: RequestMethod.GET });
  }
}
import { UserModule } from './../user/user.module';
import { Module } from "@nestjs/common";
import { APP_GUARD } from "@nestjs/core";
import { JwtModule } from "@nestjs/jwt";
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,
      signOptions: { expiresIn: '4h' }
    }),
    // 使用 UserModule 的内容
    UserModule
  ],
  providers: [
    AuthService,
    JwtStrategy,
    {
      provide: APP_GUARD,
      useClass: JwtAuthGuard, //挂载全局接口
    },
  ],
  controllers: [AuthController],
})
export class AuthModule {}

然后需要创建jwt.strategy.ts写jwt的验证策略。


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/entities/User.entity";

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

  async validate(payload: User) {
    return { loginName: payload.loginName, id: payload.id };
  }
}

然后在写一个守卫来做一个验证。

import {
  ExecutionContext,
  Injectable,
  UnauthorizedException,
  SetMetadata,
} from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { JwtService } from "@nestjs/jwt";
import { AuthGuard } from "@nestjs/passport";
import { RedisInstance } from "src/utils/redis";
// @SkipAuth 跳过JWT验证
@Injectable()
export class JwtAuthGuard extends AuthGuard("jwt") {
  constructor(private reflector: Reflector,
    private jwtService: JwtService) {
    super();
  }
  async canActivate(context: ExecutionContext) {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (isPublic) {
      return true;
    }
    const request = context.switchToHttp().getRequest();
    console.log('---re--',request);
    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.id}-${payload.loginName}`;
        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);
    // return super.canActivate(context);
  }

  handleRequest(err, user, info) {
    // You can throw an exception based on either "info" or "err" arguments
    console.log(err, user, info);
    
    if (err || !user) {
      throw err || new UnauthorizedException();
    }
    return user;
  }
}
// swager装饰器:需要验证token的controller中增加装饰器@ApiBearerAuth,增加后swager请求header会携带Authorization参数。
// 生成跳过检测装饰器 @SkipAuth()
export const IS_PUBLIC_KEY = "isPublic";
export const SkipAuth = () => SetMetadata(IS_PUBLIC_KEY, true);

然后我们将其引入到auth模块中,再写登录接口

import { UserModule } from './../user/user.module';
import { Module } from "@nestjs/common";
import { APP_GUARD } from "@nestjs/core";
import { JwtModule } from "@nestjs/jwt";
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,
      signOptions: { expiresIn: '4h' }
    }),
    // 使用 UserModule 的内容
    UserModule
  ],
  providers: [
    AuthService,
    JwtStrategy,
    {
      provide: APP_GUARD,
      useClass: JwtAuthGuard, //挂载全局接口
    },
  ],
  controllers: [AuthController],
})
export class AuthModule {}
import { Body, Controller, Post } from "@nestjs/common";
import { ApiBody, ApiOperation, ApiTags } from "@nestjs/swagger";
import { LoginUserDto } from "./dto/login-user.dto";
import { AuthService } from "./auth.service";
import { SkipAuth } from "./jwt-auth.guard";// 跳过登录

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

这里需要注意的是,登录时密码加密使用的是crypto,token存储使用redis来进行对比,下面部分会单独说明redis的引入。
AuthService中使用UserService中方法findOneByLoginNamegetUserRole。( private readonly userService: UserService)

import { Injectable, Logger } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { LoginUserDto } from "./dto/login-user.dto";
import { Encrypt } from 'src/utils/crypto'
import { UserService } from "../user/user.service";
import { RedisInstance } from "src/utils/redis";

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

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

  /**
   * @description: 用户登录
   * @param {User} loginUserDto
   * @return {*}
   */
  public async login(loginUserDto: LoginUserDto) {
    let data = {}
    try {
      const loginName: string = loginUserDto.loginName;
      const password: string = loginUserDto.password;

      const userInfo = await this.userService.findOneByLoginName(loginName);
      if (userInfo.length === 0) {
        data = { flag: false, msg: "用户不存在" };
        return;
      }
      // 加密
      // const pass = Encrypt(password)

      // if (pass === userInfo[0].password) {
      if (password === userInfo[0].password) {
        const token = await this.createToken(userInfo[0]);

        //存储token到redis
        const redis = await RedisInstance.initRedis("auth.login", 0);
        const key = `${userInfo[0].id}-${loginUserDto.loginName}`;
        await RedisInstance.setRedis("auth.login", 0, key, `${token}`);
        data = {
          flag: true,
          msg: "登录成功",
          user: {
            userInfo: userInfo[0],
          },
          token,
        };
      } else {
        data = { flag: false, msg: "用户密码错误" };
      }
    } catch (error) {
      logger.log(error);
      data = { flag: false, msg: "登录失败" };
    } finally {
      return data;
    }
  }

  /**
   * @description:创建token
   * @param loginUserDto 
   * @returns 
   */
  private async createToken(loginUserDto: LoginUserDto) {
    const payload = {
      loginName: loginUserDto.loginName,
      password: loginUserDto.password,
      id: loginUserDto.id
    };
    return this.jwtService.sign(payload);
  }
}

UserService 中实现以上所调用的方法。

import { Injectable } from '@nestjs/common';
import { UpdateUserDto } from './dto/update-user.dto';
import { CreateUserDto } from './dto/create-user.dto';
import { User } from '../entities/User.entity';

import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Like } from 'typeorm';
// 测试加密
import { Encrypt } from 'src/utils/crypto'
// import { User } from 'src/entities/User.entity';

@Injectable()
export class UserService {
  // 使用InjectRepository装饰器并引入Repository这样就可以使用typeorm的操作了
  // 引入 InjectRepository typeOrm 依赖注入 接受一个实体
  // 引入类型 Repository 接受实体泛型
  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
  ) { }

  /**
   * @description: 创建用户
   * @param createUserDto 
   * @returns 
   */
  async create(createUserDto: CreateUserDto) {
    const { userName, loginName, password } = createUserDto;
    const userInfo = await this.findOneByLoginName(loginName);
    if (userInfo.length > 0) {
      return { flag: false, msg: "登录名已存在" }
    }
    const user = new User();
    user.userName = userName;
    user.loginName = loginName;
    user.password = Encrypt(password);
    user.sex = 1;

    return this.userRepository.save(user);
  }

  /**
   * @description: 查询所有用户
   * @param query 
   * @returns 
   */
  async findAll(query: { keyWord: string, pageCurrent: number, pageSize: number }) {
    // async findAll(query: { keyWord: string, pageCurrent: number, pageSize: number }): Promise<User[]> {
    const list = await this.userRepository.find({
      where: [
        { loginName: Like(`%${query.keyWord}%`) },
        { userName: Like(`%${query.keyWord}%`) }
      ],
      order: {
        id: "DESC"
      },
      skip: (query.pageCurrent - 1) * query.pageSize,
      take: query.pageSize
    })
    const total = list.length
    return { list, total }
    // return await this.userRepository.query('select * from user');
  }

  /**
   * @description: 查询是否存在此用户,登录名不重复
   * @param loginName 
   * @returns 
   */
  async findOneByLoginName(loginName: string): Promise<User[]> {
    return await this.userRepository.find({
      where: {
        loginName: loginName
      }
    });
  }
  /**
   * @description: 根据id查找用户信息
   * @param id 
   * @returns 
   */
  async findOne(id: number) {
    return await this.userRepository.find({
      where: {
        id: id
      }
    });
  }

  /**
   * @description 更新用户
   * @param id 
   * @param updateUserDto 
   * @returns 
   */
  update(id: number, updateUserDto: UpdateUserDto) {
    return this.userRepository.update(id, updateUserDto);
  }

  /**
   * @description 删除用户
   * @param id 
   * @returns 
   */
  remove(id: number) {
    return this.userRepository.delete(id);
  }
}

// 跨域
  app.enableCors()

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

安装依赖

yarn add ioredis

下载redis

下载redishttps://github.com/MSOpenTech/redis/releases

设置密码

到redis目录下,命令行运行:redis-server.exe redis.windows.conf
image.png
另起窗口redis-cli.exe -h 127.0.0.1 -p 6379 查看密码 config get requirepass
设置密码:config set requirepass password
image.png
再登录有密码的redis: redis-cli -p 6379 -a password
redis-cli.exe -h 127.0.0.1 -p 6379 -a password

项目中引用resid

配置redis

然后创建redis.ts 文件

import { Logger } from "@nestjs/common";
import Redis from "ioredis";

const logger = new Logger("auth.service");
const redisIndex = []; // 用于记录 redis 实例索引
const redisList = []; // 用于存储 redis 实例
const redisOption = {
  host: "127.0.0.1",
  port: 6379,
  password: "password",
};
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);
      });
    });
  }
}
  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
NestJS Cache Manager is a module for caching data in NestJS applications. It provides a simple interface for caching data using various caching strategies such as memory, Redis, and others. Redis is one of the caching strategies that NestJS Cache Manager supports. Redis is an in-memory data structure store that can be used as a database, cache, and message broker. It is often used as a cache because of its high performance and scalability. Redis stores data in memory, which makes it faster than traditional disk-based databases. Using Redis as a caching strategy in NestJS Cache Manager is easy. First, you need to install the Redis module: ``` npm install cache-manager-redis-store ``` Next, you need to configure the Redis cache in your NestJS application: ``` import { CacheModule, Module } from '@nestjs/common'; import * as redisStore from 'cache-manager-redis-store'; @Module({ imports: [ CacheModule.register({ store: redisStore, host: 'localhost', port: 6379, }), ], }) export class AppModule {} ``` In this example, we import the CacheModule and configure it to use the Redis store. We set the host and port to connect to the Redis instance. Now, you can use the cache in your NestJS application: ``` import { CacheInterceptor, CacheTTL, Controller, Get, UseInterceptors } from '@nestjs/common'; @Controller('cats') export class CatsController { @Get() @UseInterceptors(CacheInterceptor) @CacheTTL(60) findAll(): string[] { return ['Cat 1', 'Cat 2', 'Cat 3']; } } ``` In this example, we use the CacheInterceptor to cache the response of the findAll() method for 60 seconds. This means that subsequent requests for the same resource will be served from the cache, which improves performance and reduces the load on the server. Overall, using Redis as a caching strategy in NestJS Cache Manager is a great way to improve the performance and scalability of your NestJS application.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值