NestJs 权限管理系统一、 基础配置

303 阅读7分钟

初始化

1. sql

2. 基本配置

2.1 环境配置
  1. 安装 pnpm i @nestjs/config -D

@nestjs/config 默认会从项目根目录载入并解析一个 .env 文件,从 .env 文件和 process.env 合并环境变量键值对,并将结果存储到一个可以通过 ConfigService 访问的私有结构。forRoot() 方法注册了 ConfigService 提供者,ConfigService: get 方法读取这些解析/合并的配置变量。

  1. app.module.ts 中引入

import { Module } from '@nestjs/common';

import { ConfigModule } from '@nestjs/config';

  


@Module({

imports: [

ConfigModule.forRoot(),

],

})

export class AppModule {}

  1. 可以使用 自定义yaml 环境配置
  • 设置 ConfigModule.forRoot({ ignoreEnvFile: true, });

  • 安装 pnpm i yaml

  • 新建: 根目录下 config/.dev.yamlconfig/.prod.yamlconfig/.uat.yaml

  • 新建: src/utils/config.ts

  • 安装 cross-env(cross-env 指定运行环境来使用对应环境的配置变量): pnpm i cross-env -D

  • 新增 scripts 指令: "start:dev": "cross-env NODE_ENV=dev nest start --watch"

2.2 Fastify(提供 web 服务)
  1. 安装

pnpm i --save @nestjs/platform-fastify

引入适配器 FastifyAdapter


// main.ts

import { NestFactory } from '@nestjs/core';

import {

FastifyAdapter,

NestFastifyApplication,

} from '@nestjs/platform-fastify';

import { AppModule } from './app.module';

  


async function bootstrap() {

const app = await NestFactory.create<NestFastifyApplication>(

AppModule,

new FastifyAdapter(),

);

await app.listen(3000);

}

bootstrap();

2.3 热重载(Nestjs 支持 webpack HMR)
  1. 安装 pnpm add webpack-node-externals run-script-webpack-plugin webpack -D

  2. 根目录下新建 webpack-hmr.config

  3. main.ts 配置


declare const module: any;

  


async function bootstrap() {

const app = await NestFactory.create(AppModule);

await app.listen(3000);

  


if (module.hot) {

module.hot.accept();

module.hot.dispose(() => app.close());

}

}

bootstrap();

  1. 修改启动脚本:

"start:dev": "nest build --webpack --webpackPath webpack-hmr.config.js --watch"

2.4 文档(Swagger)
  1. 安装
  • API 文档使用 swagger: pnpm install --save @nestjs/swagger
  1. 新建 src/doc.ts

  2. 引入到 main.ts

2.5 统一处理返回接口结果
  1. 新建 common/interceptors/transform.interceptor.ts, 新增自定义拦截器(@Interceptor)TransformInterceptor
  • 设置统一格式: common/class/response.class.ts -> ResponseDto
  1. 设置全局拦截器后需要在main.ts中引入实例

app.useGlobalInterceptors(new TransformInterceptor())

  1. 如果不需要处理返回结果,加一个@Keep自定义装饰器
  • common/constants/decorator.constants.ts -> TRANSFORM_KEEP_KEY_METADATA

  • common/decorators/keep.decorator.ts -> Keep

  • 修改 TransformInterceptor

  • 传入 Reflector 实例: app.useGlobalInterceptors(new TransformInterceptor(new Reflector()))

2.6 全局异常拦截统一处理 (HttpException)
  1. 自定义异常

在许多情况下,不需要编写自定义异常,并且可以使用内置的 Nest HTTP 异常。具体根据你业务需求定。

  • 新建 constants/business.codes.ts(自定义业务错误码)

  • 新建 common/exceptions/business.exception.ts

  1. 统一异常返回数据(使用异常过滤器)
  • 新建 common/filters/business-exception.filter.ts
  1. main.ts 引入
  • app.useGlobalFilters(new BusinessExceptionFilter())
2.7 验证请求的数据

使用 class-validator 内置的验证装饰器对需要验证的 DTO 参数:

  1. 安装 pnpm i --save class-validator class-transformer

  2. 全局使用


// main.ts

app.useGlobalPipes(

new ValidationPipe({

transform: true, // 是否将验证后的数据转换为DTO对象。如果设置为true,则验证后的数据将被自动转换为DTO对象,否则将保持原始的请求数据格式

whitelist: true, // 是否允许只验证DTO中定义的属性。如果设置为true,则只验证DTO中定义的属性,其他属性将被忽略。如果设置为false,则验证整个请求数据,包括DTO中未定义的属性

forbidNonWhitelisted: true, // 是否禁止验证未定义的属性。如果设置为true,则验证时会检查请求数据中是否存在未定义的属性,如果存在则返回验证失败。如果设置为false,则允许请求数据中存在未定义的属性。

errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, // 验证失败时返回的HTTP状态码。如果设置为HttpStatus.UNPROCESSABLE_ENTITY,则返回422状态码。如果设置为其他状态码,则返回相应的状态码。

exceptionFactory: (errors: ValidationError[]) => { // 自定义验证失败时返回的异常信息

// 过滤掉没有约束条件的错误信息

return new UnprocessableEntityException(

errors

.filter((e) => !!e.constraints)

.flatMap((e) => Object.values(e.constraints))

.join('; '),

);

},

}),

);

3. 自定义日志

Nest 带有一个内置的基于文本的记录器,如果只是普通使用,直接开启日志功能即可。


const app = await NestFactory.create(AppModule, {

logger: true,

});

await app.listen(3000);

因为该项目使用了 Fastify,所以性能使用 Fastify 来替换底层框架之后,需要开启 Fastify 的日志系统


const app = await NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter({

logger: true

}));

为了满足实际业务需求(如日志格式化、日志类型、日志级别、快速定位日志、本地存储日志、日志轮转、上传日志到服务器等),所以需要自定义日志。

这里我们使用 Winston。以下是简化版的。复杂版可以看代码shared/logger/*

  1. 安装
  • pnpm install winston --save

  • pnpm install nest-winston --save

  • pnpm install winston-daily-rotate-file (日志轮转是为了解决日志文件过大的问题,防止日志文件占用过多磁盘空间。通过日志轮转,可以将日志文件按照一定的规则进行分割,例如按照日期或大小,使得每个日志文件都保持在一个合适的大小,方便管理和备份。此外,日志轮转还可以保留一定数量的旧日志文件,以便后期进行故障排查或数据分析。)

  1. main.ts中引入

import { Module } from '@nestjs/common';

import { ConfigModule } from '@nestjs/config';

import { getConfig } from './utils/config';

import { LoggerFactory } from './common/logger.factory';

import { WinstonModule } from 'nest-winston';

import { transports, format } from 'winston';

import 'winston-daily-rotate-file';

@Module({

imports: [

...

WinstonModule.forRoot({

transports: [

new transports.DailyRotateFile({

filename: `logs/%DATE%-error.log`,

level: 'error',

format: format.combine(format.timestamp(), format.json()),

datePattern: 'YYYY-MM-DD',

zippedArchive: false, // don't want to zip our logs

maxFiles: '30d', // will keep log until they are older than 30 days

}),

// 所有等级

new transports.DailyRotateFile({

filename: `logs/%DATE%-combined.log`,

format: format.combine(format.timestamp(), format.json()),

datePattern: 'YYYY-MM-DD',

zippedArchive: false,

maxFiles: '30d',

}),

new transports.Console({

format: format.combine(

format.cli(),

format.splat(),

format.timestamp(),

format.printf((info) => {

return `${info.timestamp} ${info.level} ${info.message}`;

}),

),

}),

],

})

],

controllers: [],

providers: [],

})

export class AppModule {}

  


复杂版用这个

HTTP 模块

  1. 安装

pnpm install @nestjs/axios

  1. 导入
  • 新建 src/shared/shared.module.ts(全局共享模块 使用@Guard) 或 通过命令 nest g mo shared

  • 导入


import { HttpModule } from '@nestjs/axios';

import { Global, Module } from '@nestjs/common';

  


/**

* 全局共享模块

*/

@Global()

@Module({

imports: [HttpModule.register({ timeout: 5000, maxRedirects: 5 })],

exports: [HttpModule],

})

export class SharedModule {}

鉴权与登录(全局的)

安装

pnpm install @nestjs/jwt

配置

  • config/.dev.yaml 新增 JWT_CONFIG 配置

JWT_CONFIG:

secret: 'admin123456'

  • src/config/configuration.ts 新增 jwt配置

...

jwt: {

secret: JWT_CONFIG.secret || '123456',

},

...

  • src/config/configuration.ts 里新增两个 ts 类型 ConfigurationTypeConfigurationKeyPaths, 在 types/utils.d.ts 中新增 NestedKeyOf 类型

导入


import { JwtModule } from '@nestjs/jwt';

...

JwtModule.registerAsync({

imports: [ConfigModule],

useFactory: (configService: ConfigService) => ({

secret: configService.get<string>('jwt.secret'),

}),

inject: [ConfigService],

}),

...

鉴权和登录模块

准备:
  1. 这里使用验证码登录,需要创建验证码,需要使用svg-captcha

  2. 创建验证码后需要缓存到redis里提供高性能数据访问的临时数据存储,并在全局使用,这里选用 ioredis包,并且可以对不同环境进行一些参数配置,如hostportpassword等。

开始:
  1. 安装
  • pnpm i svg-captcha

  • pnpm i ioredis

  • pnpm i @nestjs/cache-manager cache-manager

  1. 缓存配置(Nest 为各种缓存存储提供者提供统一的 API。 内置的是内存数据存储。 但是,你可以轻松切换到更全面的解决方案,例如 Redis)

导入并注册

方法一:

安装 pnpm install cache-manager-redis-store


// shared.module.ts

  


CacheModule.registerAsync({

isGlobal: true,

useFactory: (configService: ConfigService<ConfigurationKeyPaths>) => {

const store = redisStore({

socket: {

host: configService.get('redis.host'),

port: configService.get('redis.port'),

},

database: configService.get('redis.db'),

password: configService.get('redis.password'),

ttl: 60 * 60 * 64 * 7,

});

  


return {

store: {

create() {

return store as unknown as CacheStore;

},

},

};

},

inject: [ConfigService],

}),

方法二: 引入 iorredis 和 redis 模块编写 (redis 属于全局功能,处理登录需要缓存,其他有些功能也需要做临时缓存)

  • 新建 src/shared/services/redis.*.ts

  • shared.module.ts 引入并配置

  1. login 功能

新建 modules/admin/login.service.tsnest g service modules/admin/login


import { Injectable } from '@nestjs/common';

import * as svgCaptcah from 'svg-captcha';

import { isEmpty } from 'lodash';

import { ImageCaptchaDto } from './login.dto';

import { UtilService } from '@/shared/services/util.service';

import { RedisService } from '@/shared/redis/redis.service';

  


@Injectable()

export class LoginService {

constructor(

private utilService: UtilService,

private redisService: RedisService,

) {}

  


async createImageCaptcha(captcha: ImageCaptchaDto) {

const svg = svgCaptcah.create({

size: 4,

color: true,

noise: 4,

width: isEmpty(captcha.width) ? 100 : captcha.width,

height: isEmpty(captcha.height) ? 50 : captcha.height,

charPreset: '1234567890',

});

try {

const result = {

img: `data:image/svg+xml;base64,${Buffer.from(svg.data).toString(

'base64',

)}`, // data转换为base64编码格式的字符串

id: this.utilService.genterateUUID(),

};

  


// 放入缓存并设置过期时间

this.redisService.getRedis().set(`admin:captcha:img:${result.id}`, svg.text, 'EX', 60 * 5);

return result;

} catch (error) {

console.log(error);

throw new Error(error);

}

}

}

新增 modules/admin/login.module.tsmodule/admin/login.controller.ts


// login.module.ts

import { Module } from '@nestjs/common';

import { LoginService } from './login.service';

import { LoginController } from './login.controller';

  


@Module({

imports: [],

providers: [LoginService],

exports: [LoginService],

controllers: [LoginController],

})

export class LoginModule {}


// login.controller.ts

import { Controller, Get } from '@nestjs/common';

import { ApiTags, ApiOperation } from '@nestjs/swagger';

import { LoginService } from './login.service';

import { Query } from '@nestjs/common';

import { ImageCaptchaDto } from './login.dto';

import { ImageCaptcha } from './login.class';

  


@ApiTags('登录模块')

@Controller('login')

export class LoginController {

constructor(private loginService: LoginService) {}

  


@ApiOperation({ summary: '获取登录图片验证码' })

@Get('captcha/img')

async getCaptchaByImg(@Query() dto: ImageCaptchaDto): Promise<ImageCaptcha> {

return await this.loginService.createImageCaptcha(dto);

}

}

运行: pnpm run start:dev

浏览器访问: localhost:8000/swagger-api

20230925173645.jpg

查看本地redis存的key可以在 vscode中安装插件: Redis, 连接

redis.jpg

redis2.jpg 注意

  1. shared/shared.module.ts 中要注入 RedisService

providers: [RedisService],

exports: [..., RedisService],

Tips

  1. 在 NestJS 中,使用 register 方法和 forRoot 方法都可以用来注册模块,但它们之间有一些区别:

register 方法是用于注册一个普通的模块,并且可以在模块中使用任何提供者。这意味着可以在模块中使用自己的提供者、共享提供者或全局提供者。register 方法返回一个 DynamicModule 对象,可以包含 module、providers 和 exports 等属性。

forRoot 方法是用于注册一个根模块,并且只能在根模块中使用全局提供者。这意味着在根模块中提供的任何提供者都可以在整个应用程序中使用。forRoot 方法返回一个 ModuleMetadata 对象,可以包含 imports、providers 和 exports 等属性。

register 方法可以在任何模块中使用,并且可以在任何时候注册模块。这使得 register 方法非常灵活,并且可以在应用程序中的任何地方使用。

forRoot 方法只能在根模块中使用,并且必须在应用程序启动时注册。这意味着 forRoot 方法只能用于注册全局提供者,并且必须在应用程序的早期阶段注册。

register 方法可以接受任意数量的参数,并允许将提供者动态注入到模块中。这使得 register 方法非常灵活,并且可以根据应用程序的需求进行自定义。

forRoot 方法只接受一个参数,并且必须是一个包含 providers 属性的静态对象。这意味着 forRoot 方法只能用于注册静态的全局提供者,并且不支持动态注入。

综上所述,register 方法和 forRoot 方法都可以用于注册模块,但它们之间有一些区别。register 方法更加灵活,并且可以在任何模块中使用,而 forRoot 方法更加简单,并且只能在根模块中使用全局提供者。开发者可以根据应用程序的需求选择合适的方法进行模块注册。

git地址

参考