东西装置

node 版别办理东西

nvmnode 版别办理东西

装置 nvm

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash

装置完 nvm 之后将下面这段写入 ~/.profile 文件中,然后重启终端或许 source ~/.profile

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"  # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && . "$NVM_DIR/bash_completion"  # This loads nvm bash_completion

运用:

nvm ls-remote # 检查一切可用版别
nvm install v14.18.1 # 装置指定版别
nvm uninstall v14.18.1 # 卸载指定版别
nvm use v14.18.1 # 运用指定版别
nvm ls # 检查已装置版别
nvm alias node@14 v14.18.1 # 设置别名

npm 源办理东西

nrmnpm 源办理东西

装置 nrm

pnpm add -g nrm

nrm 运用

nrm add xxx http://xxx # 增加源
nrm ls # 检查源
nrm use xxx # 运用源

docker

运转 docker-compose up -d 发动

version: "3.1"
services:
  db:
    image: mysql
    command: --default-authentication-plugin=mysql_native_password
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: example # 设置 root 用户的暗码
    ports:
      - 3090:3306 # 映射到宿主机的端口:容器内部的端口
  adminer:
    image: adminer
    restart: always
    ports:
      - 8090:8080

经过 docker-compose 创立的容器,他们是处于同一个网段的

能够经过 docker inspect <container_name> 检查容器信息,其间 Networks 字段便是容器的网络信息

 Networks: {
  nestjs_default: { # 网络称号
    IPAMConfig: null,
    Links: null,
    Aliases: ["nestjs-adminer-1", "adminer", "7ac06954919d"],
    NetworkID: "c5cf9bbe26800889a449708bb027e009409b43abc0e1f2090ed7d8d7a57d45ca",
    EndpointID: "776561fc5ef781c3a2fa3a468c46a352bed24a000adb89c1d8ed848158a0a9c5",
    Gateway: "172.20.0.1",
    IPAddress: "172.20.0.3", # 容器的 ip 地址
    IPPrefixLen: 16,
    IPv6Gateway: "",
    GlobalIPv6Address: "",
    GlobalIPv6PrefixLen: 0,
    MacAddress: "02:42:ac:14:00:03",
    DriverOpts: null,
  },
}

网络称号能够经过 docker network ls 检查

22f3062e96ae   bridge           bridge    local
e4ef76612531   ghost_ghost      bridge    local
f4afa56878b3   host             host      local
c5cf9bbe2680   nestjs_default   bridge    local   # 这个便是上面的网络称号
66ccce26b0ed   network1         bridge    local
e5f933620687   none             null      local
74520c9f00b4   wxcb0            bridge    local

docker inspect <network_name> 网络信息,其间 Containers 字段便是该网络下的容器信息

Containers: {
  "7ac06954919d215f3fc13bed1efdbee3895a544198f018b6fdedc338b24c50b2": {
    Name: "nestjs-adminer-1",
    EndpointID: "776561fc5ef781c3a2fa3a468c46a352bed24a000adb89c1d8ed848158a0a9c5",
    MacAddress: "02:42:ac:14:00:03",
    IPv4Address: "172.20.0.3/16", # 容器的 ip 地址
    IPv6Address: "",
  },
  "9943fd72d3d12b4883adb699408d6745ea6ecf0df14cf465e27fd7b69f27d06f": {
    Name: "nestjs-db-1",
    EndpointID: "45ccf42c7f866df1a5628d9e45b88e212a1cdc3bb44061533d0312a6068eb18e",
    MacAddress: "02:42:ac:14:00:02",
    IPv4Address: "172.20.0.2/16", # 容器的 ip 地址
    IPv6Address: "",
  },
}

nestjs/cli

装置官方脚手架东西 nestjs/cli

pnpm add -g @nestjs/cli

nestjs 创立项目

nest new <project-name>

运用 nest 创立模块能够运用 nest g <schematic> xxx,其间 schematicnest 的模块类型,xxx 为模块称号

 generate|g [options] <schematic> [name] [path]  Generate a Nest element.
    Schematics available on @nestjs/schematics collection:
      ┌───────────────┬─────────────┬──────────────────────────────────────────────┐
      │ name          │ alias       │ description                                  │
      │ application   │ application │ Generate a new application workspace         │
      │ class         │ cl          │ Generate a new class                         │
      │ configuration │ config      │ Generate a CLI configuration file            │
      │ controller    │ co          │ Generate a controller declaration            │
      │ decorator     │ d           │ Generate a custom decorator                  │
      │ filter        │ f           │ Generate a filter declaration                │
      │ gateway       │ ga          │ Generate a gateway declaration               │
      │ guard         │ gu          │ Generate a guard declaration                 │
      │ interceptor   │ itc         │ Generate an interceptor declaration          │
      │ interface     │ itf         │ Generate an interface                        │
      │ library       │ lib         │ Generate a new library within a monorepo     │
      │ middleware    │ mi          │ Generate a middleware declaration            │
      │ module        │ mo          │ Generate a module declaration                │
      │ pipe          │ pi          │ Generate a pipe declaration                  │
      │ provider      │ pr          │ Generate a provider declaration              │
      │ resolver      │ r           │ Generate a GraphQL resolver declaration      │
      │ resource      │ res         │ Generate a new CRUD resource                 │
      │ service       │ s           │ Generate a service declaration               │
      │ sub-app       │ app         │ Generate a new application within a monorepo │
      └───────────────┴─────────────┴──────────────────────────────────────────────┘

热重载

nest 装备 webpack 实现热重载,文档:recipes/hot-reload

在后端傍边实现热重载的意义不是很大,了解即可

运用 vscode 调试

在项目根目录下创立 .vscode/launch.json 文件,内容如下:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Launch via NPM",
      "request": "launch",
      "runtimeArgs": ["run-script", "start:debug"], // 这儿填写脚本称号
      "runtimeExecutable": "pnpm", // 这儿填写包办理器称号
      "runtimeVersion": "20.11.0", // 这儿填写 node 版别
      "internalConsoleOptions": "neverOpen", // 不用默许 debug 终端
      "console": "integratedTerminal", // 运用自己装备的终端
      "skipFiles": ["<node_internals>/**"],
      "type": "node"
    }
  ]
}

运用 chrome 调试

视频教程:运用 chrome 调试

初识 nest

nest 采用 expresshttp 服务

nestjs 国际中,一切的东西都是模块,一切的服务,路由都是和模块相相关的

项目进口是 src/main.ts,根模块是 app.module.ts

src
  - main.ts
  - app.module.ts

app.module.ts 中包括 app.service.tsapp.controller.ts

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

发动服务,访问 localhost:3000/app 就能够看到 Hello world

@Controller("app")
export class AppController {
  constructor(private UserService: UserService) {}
  @Get()
  getUser(): any{
    return {
      code: 0,
      data: "Hello world",
      msg: 'ok',
    };
  }
}

增加接口前缀

增加接口前缀:app.setGlobalPrefix('api/v1')

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.setGlobalPrefix("api/v1");
  await app.listen(3000);
}
bootstrap();

创立各模块

创立 modulecontrollerservice 办法,运用官方的脚手架东西创立,会主动将创立的模块增加到 app.module.ts 中,和放入本身的 xxx.module.ts

比方创立 user 模块

nest g controller user --no-spec # 创立 controller, --no-spec 表明不生成测试文件
nest g service user --no-spec # 创立 service, --no-spec 表明不生成测试文件
nest g module user --no-spec # 创立 module, --no-spec 表明不生成测试文件
// 运用 nest g module user 创立 user.module.ts,会主动将 userModule 增加到 AppModule 中
@Module({
  imports: [UserModule],
  controllers: [],
  providers: [],
})
export class AppModule {}
// 运用 nest g controller user 创立的 user.controller.ts,会主动将 UserController 增加到 user.module.ts 中
// 运用 nest g service user 创立的 user.service.ts,会主动将 UserService 增加到 user.module.ts 中
@Module({
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule {}

nest 生命周期

nestjs 根本运用

各个声明周期的效果如下图:

nestjs 根本运用

  • 运用 Module 来组织运用程序
  • @Module 装修器来描述模块
  • 模块中有 4 大特色:
    • imports:一个模块导入其他模块
    • providers:处理 service
    • controllers:处理恳求
    • exports:一个模块需求被其他模块导入

接口服务逻辑

一个接口服务包括下面五大块:

  • 恳求数据校验
  • 恳求认证(鉴权规划)
  • 路由
  • 功用逻辑
  • 数据库操作

如下图所示:

nestjs 根本运用

nestjs 中常见用装修器

一个恳求中,用到的装修器如下图所示:

nestjs 根本运用

  • @Post 装修器表明这是一个 POST 恳求,假如是 GET 恳求,用 @Get 装修器,其他的恳求办法同理
  • @Params 装修器获取恳求路径上的参数
  • @Query 装修器获取恳求查询参数
  • @Body 装修器获取恳求体中的参数
  • @Headers 装修器获取恳求头中的参数
  • @Req 装修器获取恳求中一切的参数

在运用恳求办法的装修器时,要注意,假如路径是 /:xxx 的形式可能会呈现问题, 如下所示,恳求不会进入到 getProfile 办法中

@Controller("user")
export class UserController {
  @Get("/:id")
  getUser() {
    return "hello world";
  }
  @Get("/profile")
  getProfile() {
    return "profile";
  }
}

解决办法有三种:

  1. @Get("/:xxx") 的形式放到当时 Controller 的最下面
  2. 用路径阻隔:@Get("/path/:xxx")
  3. 单独写一个 Controller

装备文件

dotenv

dotenv 这个库是用来读取本地的 .env 文件

.env 计划的缺点是,无法运用嵌套的办法书写装备,当装备比较多时,不太好办理

require("dotenv").config();
console.log(process.env);

config

config 这个库的装备文件是 json 的形式,默许读取当时目录下的 config/default.json 文件

const config = require("config");
console.log(config.get("db")); // 读取到装备文件中的 db 特色

它能够经过 export NODE_ENV=production 办法读取 production.json,然后就会将 production.json 中的装备合并到 default.json

@nestjs/config

官方也提供了装备文件的解决计划 @nestjs/config,它是内部是根据 dotenv

app.module.ts 中引进 ConfigModule,需求设置 isGlobaltrue,这样就能够在其他模块中运用 ConfigService

import { ConfigModule } from "@nestjs/config";
@Module({
  imports: [
    ConfigModule.forRoot({
      // 需求设置为 true
      isGlobal: true,
    }),
    UserModule,
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

运用:

import { ConfigService } from "@nestjs/config";
@Controller("user")
export class UserController {
  constructor(private UserService: UserService, private configService: ConfigService) {}
  @Get()
  getUser() {
    // 就能够获取到 .env 中的 USERNAME 特色
    console.log(this.configService.get("USERNAME"));
    return this.UserService.getUsers();
  }
}

假如不在 app.module.ts 中设置 isGlobaltrue 的话,就需求在运用的当地引进,才能在当时的 controller 中运用

合并不同的 .env 文件

当装备项需求区分不同环境时,比方 .env.development.env.production 时,有些功用的装备假如在这两个装备文件中都写一遍的话,就会造成代码冗余,维护本钱也比较大

我们能够经过 load 参数实现加载公共装备文件,envFilePath 加载对应环境的装备文件

首要在 package.json 中装备履行各环境的命令,这儿是借助 cross-env 这个库来实现的:

{
  // dev 环境装备 NODE_ENV=development
  "start:dev": "cross-env NODE_ENV=development nest start --watch",
  // prod 环境装备 NODE_ENV=production
  "start:prod": "cross-env NODE_ENV=production node dist/main"
}

然后在 app.module.ts 中就能够经过 proces.env.NODE_ENV 来获取当时环境值,然后拼接 .env 文件名,传入给 envFilePath 参数

加载公共装备文件运用 load 参数,传入一个函数

import * as dotenv from "dotenv";
const envFilePath = `.env.${process.env.NODE_ENV || ""}`;
@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      // 加载对应环境的装备参数
      envFilePath,
      // 加载公共装备参数
      load: [() => dotenv.config({ path: ".env" })],
    }),
    UserModule,
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

加载 yaml 文件

nestjs 中加载 yaml 需求自己写读取办法

  1. 这个是多环境读取办法,运用 lodash.merge 合并不同装备文件
  2. 运用 js-yaml 解析 yaml 文件
  3. 运用内置模块 fspath 读取文件
import { readFileSync } from "fs";
import * as yaml from "js-yaml";
import { join } from "path";
import { merge } from "lodash";
const YAML_COMMON_CONFIG_FILENAME = "config.yaml";
const filePath = join(__dirname, "../config", YAML_COMMON_CONFIG_FILENAME);
const envPath = join(__dirname, "../config", `config.${process.env.NODE_ENV || ""}.yaml`);
const commonConfig = yaml.load(readFileSync(filePath, "utf8"));
const envConfig = yaml.load(readFileSync(envPath, "utf8"));
// 由于 ConfigModule 的 load 参数接纳的是函数
export default () => merge(commonConfig, envConfig);

装备文件参数验证

装备文件参数验证,运用 Joi

const schema = Joi.object({
  // 校验端口是不是数字,而且是不是 3305,3306,3307,默许运用 3306
  PORT: Joi.number().valid(3305, 3306, 3307).default(3306),
  // 这个值会被动态加载
  DATABASE: Joi.string().required(),
});
@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      // 加载对应环境的装备参数
      envFilePath,
      // 加载公共装备参数
      load: [
        // 动态加载的装备文件参数校验
        () => {
          const values = dotenv.config({ path: ".env" });
          const { error } = schema.validate(values?.parsed, {
            // 允许未知的环境变量
            allowUnknown: true,
            // 假如有过错,不要立即中止,而是搜集一切过错
            abortEarly: false,
          });
          if (error) throw new Error(`Validation failed - Is there an environment variable missing?${error.message}`);
          return values;
        },
      ],
      validationSchema: schema,
    }),
    UserModule,
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

TypeOrm

TypeOrmnestjs 官方引荐的数据库操作库,需求装置 @nestjs/typeormtypeormmysql2 这三个库

为了能够从装备文件中读取数据库的衔接办法,需求运用 TypeOrmModule.forRootAsync 办法

import { TypeOrmModule, TypeOrmModuleOptions } from "@nestjs/typeorm";
TypeOrmModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (configService: ConfigService) =>
    ({
      type: configService.get("DB_TYPE"),
      host: configService.get("DB_HOST"),
      port: configService.get("DB_PORT"),
      username: configService.get("DB_USERNAME"),
      password: configService.get("DB_PASSWORD"),
      database: configService.get("DB"),
      entities: [],
      synchronize: configService.get("DB_SYNC"),
      logging: ["error"],
    } as TypeOrmModuleOptions),
});

创立表

  1. 在类上打上 @Entity 装修器
  2. 主键运用 @PrimaryGeneratedColumn 装修器
  3. 其他的字段运用 @Column 装修器
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;
  @Column()
  username: string;
  @Column()
  password: string;
}

然后在 app.module.tsTypeOrmModule.forRootAsync.entities 中引进 user.entity,重启服务就能看到数据库中创立了 user

一对一

profile.user_id 需求对应 user.id

pofile 类中,不要显现指定 user_id,而是运用 @OneToOne 装修器和 @JoinColumn 装修器

  1. @OneToOne 装修器接纳一个函数,这个函数回来 User
  2. @JoinColumn 装修器接纳一个对象,这个对象的 name 特色便是 profile 表中的 user_id 字段
    1. 不运用 name 特色,默许将 Profile.userUser.id 驼峰拼接
import { Column, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn } from "typeorm";
import { User } from "./user.entity";
@Entity()
export class Profile {
  @PrimaryGeneratedColumn()
  id: number;
  @Column()
  gender: number;
  @Column()
  photo: string;
  @Column()
  address: string;
  @OneToOne(() => User)
  @JoinColumn({ name: "user_id" })
  user: User;
}

一对多

一对多运用 @OneToMany 装修器,第一个参数和 @OneToOne 相同

主要是第二个参数,第二个参数的效果是告诉 TypeOrm 应该和 Logs 中哪个字段进行相关

import { Logs } from "src/logs/logs.entity";
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;
  @Column()
  username: string;
  @Column()
  password: string;
  @OneToMany(() => Logs, (logs) => logs.user)
  logs: Logs[];
}

Logs 表便是多对一的联系,运用 @ManyToOne,和 User.logs 相关

import { User } from "src/user/user.entity";
import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class Logs {
  @PrimaryGeneratedColumn()
  id: number;
  @Column()
  path: string;
  @Column()
  method: string;
  @Column()
  data: string;
  @Column()
  result: string;
  @ManyToOne(() => User, (user) => user.logs)
  @JoinColumn()
  user: User;
}

多对多

多对多运用 @ManyToMany@JoinTable 两个装修器

用法和 @OneToMany 相同

@JoinTable 装修器的效果是创立一张中心表,用来记载两个表之间的相关联系

import { Logs } from "src/logs/logs.entity";
import { Roles } from "src/roles/roles.entity";
import { Column, Entity, ManyToMany, OneToMany, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;
  @Column()
  username: string;
  @Column()
  password: string;
  @ManyToMany(() => Roles, (role) => role.users)
  roles: Roles[];
}
import { User } from "src/user/user.entity";
import { Column, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class Roles {
  @PrimaryGeneratedColumn()
  id: number;
  @Column()
  name: string;
  @ManyToMany(() => User, (user) => user.roles)
  @JoinTable({ name: "users_roles" })
  users: User[];
}

查询

nestjs 根本运用

TypeOrm 中查询,需求运用 @InjectRepository 装修器,注入一个 Repository 对象

Repository 对象会包括各种对数据库的操作办法,比方 findfindOnesaveupdatedelete

import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { User } from "./user.entity";
import { Repository } from "typeorm";
@Injectable()
export class UserService {
  @InjectRepository(User)
  private readonly userRepository: Repository<User>;
}
  1. 查询悉数 this.userRepository.find()
  2. 查询某条数据 this.userRepository.findOne({ where: { id: 1 } })
  3. 新建数据
    1. 创立数据 const user = this.userRepository.create({ username: "xxx", password: "xxx" })
    2. 报错数据 this.userRepository.save(user)
  4. 更新数据 this.userRepository.update(id, user)
  5. 删去数据 this.userRepository.delete(id)
  6. 一对一 this.userRepository.findOne({ where: { id }, relations: ["profile"] })
  7. 一对多
    1. 先查询出 user 实体 const user = this.userRepository.findOne({ where: { id } })
    2. 在用 user 实体作为 where 条件,this.logsRepository.find({ where: { user }, relations: ["user"] })

运用 QueryBuilder 查询

this.logsRepository
  .createQueryBuilder("logs")
  .select("logs.result", "result")
  .addSelect("COUNT(logs.result)", "count")
  .leftJoinAndSelect("logs.user", "user")
  .where("user.id = :id", { id })
  .groupBy("logs.result")
  .orderBy("count", "DESC")
  .getRawMany();

这个 QueryBuilder 句子对应下的 sql 句子

select logs.result as result, COUNT(logs.result) as count from logs, user where user.id = logs.user_id and user.id = 2 group by logs.result order by count desc;

假如要运用原生 sql 句子,能够运用 this.logsRepository.query 办法

this.logsRepository.query(
  "select logs.result as result, COUNT(logs.result) as count from logs, user where user.id = logs.user_id and user.id = 2 group by logs.result order by count desc"
);

处理字段不存在

当一个参数前端没有传递过来时,那么应该怎样写这个查询条件

能够用 sqlwhere 1=1 的办法拼接 sql

const queryBuilder = this.userRepository.createQueryBuilder("user").where(username ? "user.username = :username" ? "1=1", { username })

remove 和 delete 的区别

remove 能够一次删去单个或许多个实例,而且 remove 能够触发 BeforeRemoveAfterRemove 钩子

  • await repository.remove(user);
  • await repository.remove([user1, user2, user3]);

delete 能够一次删去单个或许多个 id 实例,或许给定的条件

  • await repository.delete(user.id);
  • await repository.delete([user1.id, user2.id, user3.id]);
  • await repository.delete({ username: "xxx" });

日志

日志依照等级可分为 5 类:

  • Log:通用日志,按需进行记载(打印)
  • Warning:正告日志,比方屡次进行数据库操作
  • Error:过错日志,比方数据库衔接失利
  • Debug:调试日志,比方加载数据日志
  • Verbose:详细日志,一切的操作与详细信息(非必要不打印)

依照功用分类,可分为 3 类:

  • 过错日志:便利定位问题,给用户友好提示
  • 调试日志:便利开发人员调试
  • 恳求日志:记载灵敏行为

NestJS 大局日志

NestJS 默许是开启日志的,假如需求关闭的话,在 NestFactory.create 办法中,传递第二个参数 { logger: false }

import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
async function bootstrap() {
  const app = await NestFactory.create(AppModule, { logger: false });
  app.setGlobalPrefix("api/v1");
  console.log(123);
  await app.listen(3000);
}
bootstrap();

logger

NestJS 官方提供了一个 Logger 类,能够用来打印日志

  • logger.warn():用黄色打印出信息
  • logger.error():用红色打印出信息
  • logger.log():用绿色打印出信息
const logger = new Logger();
logger.warn();

controller 中记载日志,需求显现运用 new 创立一个 logger 实例

@Controller("user")
class UserController {
  private logger = new Logger(UserController.name);
  constructor(private UserService: UserService, private configService: ConfigService) {
    this.logger.log("UserController created");
  }
  @Get()
  getUsers() {
    this.logger.log("getUsers 恳求成功");
    return this.UserService.findOne(1);
  }
}

nestjs-pino

NestJS 中运用第三方日志库 pino,需求 nestjs-pino 这个库

首要需求再用到的模块中注入,也便是在 UserModuleimports 中注入 pino 模块

@Module({
  imports: [TypeOrmModule.forFeature([User, Logs]), LoggerModule.forRoot()],
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule {}

然后在 UserController 中导入,就能够运用了,默许一切恳求都会记载

@Controller("user")
class UserController {
  constructor(private UserService: UserService, private configService: ConfigService, private logger: Logger) {
    this.logger.log("UserController created");
  }
}

日志美化运用 pino-pretty,日志记载在文件中运用 pino-roll,运用办法如下:

LoggerModule.forRoot({
  pinoHttp: {
    transport: {
      targets: [
        {
          level: "info",
          target: "pino-pretty",
          options: {
            colorize: true,
          },
        },
        {
          level: "info",
          target: "pino-roll",
          options: {
            file: join("logs", "log.txt"),
            frequency: "daily",
            size: "10m",
            mkdir: true,
          },
        },
      ],
    },
  },
});

nestjs 根本运用

反常

当呈现了反常,运用 HttpException 抛出反常

@Controller("user")
export class UserController {
  constructor(private UserService: UserService) {}
  @Get()
  getUsers() {
    const user = { isAdmin: false };
    if (!user.isAdmin) {
      throw new HttpException("Forbidden", HttpStatus.FORBIDDEN);
    }
    return this.UserService.findOne(1);
  }
}

经过 throw new HttpException() 的办法抛出反常,能够在大局的过滤器中捕获

新建 http-exception.filter.ts 文件:

  1. 新建一个 HttpException 类,实现 ExceptionFilter 接口
    1. 实现 catch 办法,这个办法会在抛出反常的时候履行
  2. 在类上运用 @Catch 装修器,传入需求捕获的反常类
import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from "@nestjs/common";
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    // 获取到上下文
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();
    const status = exception.getStatus();
    response.status(status).json({
      code: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      method: request.method,
      message: exception.message,
    });
  }
}

JWT

JWT 全称 JSON Web Token,由三部分构成:HeaderPayloadSignature

  • Header:规则运用的加密办法和类型
    {
      alg: "HS256", // 加密办法
      typ: "JWT",   // 类型
    };
    
  • Payload:包括一些用户信息
    {
      sub: "2024-01-01",
      name: "Brian",
      admin: true,
    };
    
  • Signaturebase64Header + base64Payload + secret 生成的字符串
    HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret);
    

它的特色有三个:

  • CSRF(主要是假造恳求,有 Cookie
  • 合适移动运用
  • 无状况,编码数据

JWT 作业原理,如图所示

nestjs 根本运用

装置依靠

装置 @nestjs/jwtpassport-jwtpassport 这三个库

pnpm i @nestjs/jwt passport-jwt passport

auth 模块运用 userService

UserModule 中将 UserService 导出,也便是写在 exports 特色上面

@Module({
  imports: [TypeOrmModule.forFeature([User, Logs])],
  controllers: [UserController],
  providers: [UserService],
  // 导出 UserService
  exports: [UserService],
})
export class UserModule {}

AuthModule 中导入 UserModule,也便是写在 imports 特色上面

@Module({
  imports: [UserModule],
  providers: [AuthService],
  controllers: [AuthController],
})
export class AuthModule {}

AuthService 中注入,就能够运用 AuthService 中的办法了

@Injectable()
export class AuthService {
  @Inject()
  private userService: UserService;
  async signin(username: string, password: string) {
    return await this.userService.findAll({ username } as getUserDto);
  }
}

管道

nestjs 会在调用办法前插入一个管道,管道会先阻拦办法的调用参数,进行转化或许是验证处理,然后用转化好或许是验证好的参数调用原办法

管道有两个运用场景:

  • 转化:管道将输入数据转化为所需的数据输出(例如,将字符串转化为整数)
  • 验证:对输入数据进行验证,假如验证成功持续传递;验证失利则抛出反常

nestjs 中的管道分为三种:

  • 控制器等级:对整个控制器收效
  • 变量:只对某个变量收效
  • 大局:对整个运用收效

运用办法如图所示:

nestjs 根本运用

运用

装置 class-validatorclass-transformer 两个库

pnpm i class-transformer class-validator

在大局增加管道,参数 whitelist 特色效果是过滤掉前端传过来的字段中后端在 dto 中没有界说的字段

app.useGlobalPipes(
  new ValidationPipe({
    // 过滤掉前端传过来的脏数据
    whitelist: true,
  })
);

界说一个 dto

自界说过错信息,message 中有几个变量:

  • $value:当时用户传入的值
  • $property:当时特色名
  • $target:当时类
  • $constraint1:第一个束缚
import { IsNotEmpty, IsString, Length } from "class-validator";
export class SigninUserDto {
  @IsString()
  @IsNotEmpty()
  @Length(6, 20, {
    message: "用户名长度必须在 $constraint1 到 $constraint2 之间,当时传的值是 $value",
  })
  username: string;
  @IsString()
  @IsNotEmpty()
  @Length(6, 20, {
    message: "暗码长度必须在 $constraint1 到 $constraint2 之间,当时传的值是 $value",
  })
  password: string;
}

过滤掉不需求的字段

@Body() 中运用管道 CreateUserPipe

@Controller("user")
@UseFilters(new TypeormFilter())
export class UserController {
  constructor(private UserService: UserService) {}
  @Post()
  // 是用管道 CreateUserPipe
  addUser(@Body(CreateUserPipe) dto: CreateUserPipe): any {
    const user = dto as User;
    return this.UserService.create(user);
  }
}

运用 nest/cli 东西创立管道

nest g pi user/pipes/create-user --no-spec

CreateUserPipe 实现 PipeTransform 接口,这个接口有一个 transform 办法,这个办法会在管道中履行

import { ArgumentMetadata, Injectable, PipeTransform } from "@nestjs/common";
import { CreateUserDto } from "src/user/dto/create-user.dto";
@Injectable()
export class CreateUserPipe implements PipeTransform {
  transform(value: CreateUserDto, metadata: ArgumentMetadata) {
    if (value.roles && value.roles instanceof Array && value.roles.length > 0) {
      if (value.roles[0]["id"]) {
        value.roles = value.roles.map((role) => role.id);
      }
    }
    return value;
  }
}

集成 jwt

  1. 新建文件 auth.strategy.ts,写下如下办法:
    import { PassportStrategy } from "@nestjs/passport";
    import { Strategy, ExtractJwt } from "passport-jwt";
    import { ConfigService } from "@nestjs/config";
    import { Injectable } from "@nestjs/common";
    @Injectable()
    export class JwtStrategy extends PassportStrategy(Strategy) {
      constructor(protected configService: ConfigService) {
        super({
          jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
          ignoreExpiration: false,
          secretOrKey: configService.get<string>("SECRET"),
        });
      }
      async validate(payload: any) {
        return { userId: payload.sub, username: payload.username };
      }
    }
    
  2. auth.module.ts 中引进 JwtStrategy
    1. 先引进 PassportModule
    2. JwtModule.registerAsync 读到装备文件中的 SECRET
    3. providers 中引进 JwtStrategy
    import { Module } from "@nestjs/common";
    import { AuthService } from "./auth.service";
    import { AuthController } from "./auth.controller";
    import { UserModule } from "src/user/user.module";
    import { PassportModule } from "@nestjs/passport";
    import { JwtModule } from "@nestjs/jwt";
    import { ConfigModule, ConfigService } from "@nestjs/config";
    import { JwtStrategy } from "./auth.strategy";
    @Module({
      imports: [
        UserModule,
        PassportModule,
        JwtModule.registerAsync({
          imports: [ConfigModule],
          useFactory: async (configService: ConfigService) => {
            return {
              // 密钥
              secret: configService.get<string>("SECRET"),
              // 过期时刻 大局
              signOptions: {
                expiresIn: "1d",
              },
            };
          },
          inject: [ConfigService],
        }),
      ],
      providers: [AuthService, JwtStrategy],
      controllers: [AuthController],
    })
    export class AuthModule {}
    
  3. 登录时生成 token,将 jwtService 注入到 auth.service.ts 中,调用 this.jwtService.signAsync 办法生成 token,供 signin 接口调用
    import { Inject, Injectable, UnauthorizedException } from "@nestjs/common";
    import { UserService } from "../user/user.service";
    import { JwtService } from "@nestjs/jwt";
    @Injectable()
    export class AuthService {
      @Inject()
      private userService: UserService;
      @Inject()
      private jwtService: JwtService;
      async signin(username: string, password: string) {
        const user = await this.userService.find(username);
        if (user && user.password === password) {
          return await this.jwtService.signAsync(
            {
              username: user.username,
              sub: user.id,
            }
            // 部分过期时刻,一般用于 refresh token
            // { expiresIn: "1d" }
          );
        }
        throw new UnauthorizedException();
      }
    }
    
  4. 在需求鉴权的接口上运用 @UseGuards(AuthGuard("jwt"))AuthGuardpassport 提供的一个护卫,jwtJwtStrategy 的姓名
    import { Controller, Get, UseFilters, UseGuards } from "@nestjs/common";
    import { UserService } from "./user.service";
    import { TypeormFilter } from "src/filters/typeorm.filter";
    import { AuthGuard } from "@nestjs/passport";
    @Controller("user")
    @UseFilters(new TypeormFilter())
    export class UserController {
      constructor(private UserService: UserService) {}
      @Get("profile")
      @UseGuards(AuthGuard("jwt")) // 仅仅验证是否带有 token
      getUserProfile() {
        return this.UserService.findProfile(2);
      }
    }
    

验证用户是不是有权限进行后续操作

运用 nest/cli 东西创立 guard 护卫

nest g gu guards/admin --no-spec

判别当时用户是否是 人物2

import { CanActivate, ExecutionContext, Inject, Injectable } from "@nestjs/common";
import { User } from "src/user/user.entity";
import { UserService } from "src/user/user.service";
@Injectable()
export class AdminGuard implements CanActivate {
  @Inject()
  private readonly userService: UserService;
  async canActivate(context: ExecutionContext): Promise<boolean> {
    // 获取恳求对象
    const req = context.switchToHttp().getRequest();
    // 获取恳求中的用户信息,进行逻辑上的判别 -> 人物判别
    const user = (await this.userService.find(req.user.id)) as User;
    console.log("user", user);
    // 判别用户是否是人物2
    if (user.roles.filter((role) => role.id === 2).length > 0) {
      return true;
    }
    return false;
  }
}

装修器履行顺序:

  1. 从下往上履行
  2. UseGuards 传递了多个护卫,那么会从左往右履行
import { Controller, Get, UseFilters, UseGuards } from "@nestjs/common";
import { UserService } from "./user.service";
import { TypeormFilter } from "src/filters/typeorm.filter";
import { AuthGuard } from "@nestjs/passport";
import { AdminGuard } from "../guards/admin/admin.guard";
@Controller("user")
@UseFilters(new TypeormFilter())
export class UserController {
  constructor(private UserService: UserService) {}
  @Get("profile")
  // 第一个是验证有没有 token,第二个是验证有没有人物2的权限
  @UseGuards(AuthGuard("jwt"), AdminGuard)
  getUserProfile() {
    return this.UserService.findProfile(2);
  }
}

暗码加密

暗码加密运用 argon2

装置:

pnpm i argon2

加码:

userTmp.password = await argon2.hash(userTmp.password);

验证:

const isPasswordValid = await argon2.verify(user.password, password);

阻拦器

运用 nest/cli 创立阻拦器

nest g itc interceptors/serialize --no-spec
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
import { Observable, map } from "rxjs";
@Injectable()
export class SerializeInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    // 阻拦器履行之前
    return next.handle().pipe(
      map((data) => {
        // 阻拦器履行之后
        return data;
      })
    );
  }
}

大局运用:

app.useGlobalInterceptors(new SerializeInterceptor());

部分运用,在路由或许控制器上:

@useInterceptors(SerializeInterceptor)

序列化

在路由上运用

import { ClassSerializerInterceptor } from "@nestjs/common";
@UseInterceptors(ClassSerializerInterceptor)

不需求响应给前端的字段,在 entity 中运用 @Exclude 装修器

import { Logs } from "src/logs/logs.entity";
import { Roles } from "src/roles/roles.entity";
import { Column, Entity, ManyToMany, OneToMany, OneToOne, PrimaryGeneratedColumn } from "typeorm";
import { Profile } from "./profile.entity";
import { Exclude } from "class-transformer";
@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;
  @Column({ unique: true })
  username: string;
  @Column()
  @Exclude() // 不需求导出的字段
  password: string;
  @OneToMany(() => Logs, (logs) => logs.user)
  logs: Logs[];
  @ManyToMany(() => Roles, (role) => role.users)
  roles: Roles[];
  @OneToOne(() => Profile, (profile) => profile.user, { cascade: true })
  profile: Profile;
}

处理输入的数据,运用 @Expose 装修器,在路由中只要 msg 字段能够被获取到

class Test {
  @Expose()
  msg: string;
}
@UseInterceptors(new SerializeInterceptor(Test))
getUsers(@Query() query: getUserDto): any {
  console.log(query);
  return this.UserService.findAll(query);
}
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
import { plainToClassFromExist } from "class-transformer";
import { Observable, map } from "rxjs";
@Injectable()
export class SerializeInterceptor implements NestInterceptor {
  constructor(private dto: any) {}
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map((data) => {
        return plainToClassFromExist(this.dto, data, {
          excludeExtraneousValues: true,
        });
      })
    );
  }
}