东西装置
node 版别办理东西
nvm
是 node
版别办理东西
装置 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 源办理东西
nrm
是 npm
源办理东西
装置 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
,其间 schematic
为 nest
的模块类型,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
采用 express
的 http
服务
在 nestjs
国际中,一切的东西都是模块,一切的服务,路由都是和模块相相关的
项目进口是 src/main.ts
,根模块是 app.module.ts
src
- main.ts
- app.module.ts
app.module.ts
中包括 app.service.ts
和 app.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();
创立各模块
创立 module
、controller
、service
办法,运用官方的脚手架东西创立,会主动将创立的模块增加到 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 生命周期
各个声明周期的效果如下图:
- 运用
Module
来组织运用程序 -
@Module
装修器来描述模块 - 模块中有
4
大特色:-
imports
:一个模块导入其他模块 -
providers
:处理service
-
controllers
:处理恳求 -
exports
:一个模块需求被其他模块导入
-
接口服务逻辑
一个接口服务包括下面五大块:
- 恳求数据校验
- 恳求认证(鉴权规划)
- 路由
- 功用逻辑
- 数据库操作
如下图所示:
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";
}
}
解决办法有三种:
- 将
@Get("/:xxx")
的形式放到当时Controller
的最下面 - 用路径阻隔:
@Get("/path/:xxx")
- 单独写一个
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
,需求设置 isGlobal
为 true
,这样就能够在其他模块中运用 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
中设置 isGlobal
为 true
的话,就需求在运用的当地引进,才能在当时的 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
需求自己写读取办法
- 这个是多环境读取办法,运用
lodash.merge
合并不同装备文件 - 运用
js-yaml
解析yaml
文件 - 运用内置模块
fs
和path
读取文件
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
TypeOrm
是 nestjs
官方引荐的数据库操作库,需求装置 @nestjs/typeorm
、typeorm
、mysql2
这三个库
为了能够从装备文件中读取数据库的衔接办法,需求运用 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),
});
创立表
- 在类上打上
@Entity
装修器 - 主键运用
@PrimaryGeneratedColumn
装修器 - 其他的字段运用
@Column
装修器
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
username: string;
@Column()
password: string;
}
然后在 app.module.ts
中 TypeOrmModule.forRootAsync.entities
中引进 user.entity
,重启服务就能看到数据库中创立了 user
一对一
profile.user_id
需求对应 user.id
在 pofile
类中,不要显现指定 user_id
,而是运用 @OneToOne
装修器和 @JoinColumn
装修器
-
@OneToOne
装修器接纳一个函数,这个函数回来User
类 -
@JoinColumn
装修器接纳一个对象,这个对象的name
特色便是profile
表中的user_id
字段- 不运用
name
特色,默许将Profile.user
和User.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[];
}
查询
在 TypeOrm
中查询,需求运用 @InjectRepository
装修器,注入一个 Repository
对象
Repository
对象会包括各种对数据库的操作办法,比方 find
、findOne
、save
、update
、delete
等
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>;
}
- 查询悉数
this.userRepository.find()
- 查询某条数据
this.userRepository.findOne({ where: { id: 1 } })
- 新建数据
- 创立数据
const user = this.userRepository.create({ username: "xxx", password: "xxx" })
- 报错数据
this.userRepository.save(user)
- 创立数据
- 更新数据
this.userRepository.update(id, user)
- 删去数据
this.userRepository.delete(id)
- 一对一
this.userRepository.findOne({ where: { id }, relations: ["profile"] })
- 一对多
- 先查询出
user
实体const user = this.userRepository.findOne({ where: { id } })
- 在用
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"
);
处理字段不存在
当一个参数前端没有传递过来时,那么应该怎样写这个查询条件
能够用 sql
中 where 1=1
的办法拼接 sql
const queryBuilder = this.userRepository.createQueryBuilder("user").where(username ? "user.username = :username" ? "1=1", { username })
remove 和 delete 的区别
remove
能够一次删去单个或许多个实例,而且 remove
能够触发 BeforeRemove
和 AfterRemove
钩子
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
这个库
首要需求再用到的模块中注入,也便是在 UserModule
的 imports
中注入 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,
},
},
],
},
},
});
反常
当呈现了反常,运用 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
文件:
- 新建一个
HttpException
类,实现ExceptionFilter
接口- 实现
catch
办法,这个办法会在抛出反常的时候履行
- 实现
- 在类上运用
@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
,由三部分构成:Header
,Payload
,Signature
-
Header
:规则运用的加密办法和类型{ alg: "HS256", // 加密办法 typ: "JWT", // 类型 };
-
Payload
:包括一些用户信息{ sub: "2024-01-01", name: "Brian", admin: true, };
-
Signature
:base64
的Header
+base64
的Payload
+secret
生成的字符串HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret);
它的特色有三个:
- 防
CSRF
(主要是假造恳求,有Cookie
) - 合适移动运用
- 无状况,编码数据
JWT
作业原理,如图所示
装置依靠
装置 @nestjs/jwt
、passport-jwt
、passport
这三个库
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
中的管道分为三种:
- 控制器等级:对整个控制器收效
- 变量:只对某个变量收效
- 大局:对整个运用收效
运用办法如图所示:
运用
装置 class-validator
、class-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
- 新建文件
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 }; } }
- 在
auth.module.ts
中引进JwtStrategy
- 先引进
PassportModule
- 在
JwtModule.registerAsync
读到装备文件中的SECRET
- 在
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 {}
- 先引进
- 登录时生成
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(); } }
- 在需求鉴权的接口上运用
@UseGuards(AuthGuard("jwt"))
,AuthGuard
是passport
提供的一个护卫,jwt
是JwtStrategy
的姓名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;
}
}
装修器履行顺序:
- 从下往上履行
-
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,
});
})
);
}
}