Logger日志 node服务端常用的日志收集工具:winston 、log4js 、bunyan 、npmlog 等
Nest内置了一个基于文本的日志记录器:Logger
类结构:
LoggerService
是接口标准,如果要替换掉内置的日志类,最低要求是得符合这个接口。
ConsoleLogger
日志主要业务类,主要负责处理格式化日志字符串输出。
Logger
更高层封装,加入了输出缓存,并统一管理日志等级。自身有一个ConsoleLogger的默认单例。
配置Logger 在创建Nest应用时,通过NestApplicationOptions.logger
来配置Logger,用于系统日志记录。
注意: 这些配置不会影响到自定义Logger ,只作用于内置Logger类 。
1 logger?: LoggerService | LogLevel [] | false ;
设置为false 时,将禁用 Logger日志
1 2 3 const app = await NestFactory .create (AppModule , { logger : false , });
还可以指定输出的最低日志等级 ,优先级更高的也将被输出。
1 2 3 const app = await NestFactory .create (AppModule , { logger : ['log' ], });
Logger默认提供了6个日志等级
1 2 3 4 5 6 7 8 9 10 type LogLevel = 'log' | 'error' | 'warn' | 'debug' | 'verbose' | 'fatal' ;const LOG_LEVEL_VALUES = { verbose : 0 , debug : 1 , log : 2 , warn : 3 , error : 4 , fatal : 5 , };
自定义Logger 实现了LoggerService
接口的自定义Logger,可以被Nest应用使用,相当于完全重写并覆盖 内置Logger
1 2 3 4 5 6 7 8 9 interface LoggerService { log (message : any , ...optionalParams : any []): any ; error (message : any , ...optionalParams : any []): any ; warn (message : any , ...optionalParams : any []): any ; debug?(message : any , ...optionalParams : any []): any ; verbose?(message : any , ...optionalParams : any []): any ; fatal?(message : any , ...optionalParams : any []): any ; setLogLevels?(levels : LogLevel []): any ; }
若只需要扩展 内置的Logger,可以继承ConsoleLogger
类,覆盖需要扩展的方法
1 2 3 4 5 6 class MyLogger extends ConsoleLogger { error (message: any , stack?: string , context?: string ) { super .error (...arguments ); } }
在Nest应用中使用自定义Logger,用于系统日志记录
1 2 3 4 const app = await NestFactory .create (AppModule , { logger : new MyLogger (), });
可以通过 set*()
等实例方法配置自定义Logger,如 setLogLevels()
设置输出的最低日志等级。
1 this .logger .setLogLevels (['error' ]);
日志格式 Logger默认的日志格式如下:
1 [AppName ] [PID ] [Timestamp ] [LogLevel ] [Context ] Message [+ms]
AppName 应用程序名,被固定为[Nest]
PID :系统分配的进程编号
Timestamp :当前日志输出的格式化系统时间
LogLevel :日志等级文本
Context :上下文
Message :输出的消息,可以是对象类型输出
+ms :两次输出日志的时间间隔,时间戳
使用Logger 通过Logger
类手动实例化一个Logger对象,用于输出日志,还可以使用app.useLogger()
1 2 3 4 5 class Logger implements LoggerService { constructor (context: string , options?: { timestamp?: boolean ; } );}
其构造函数接受两个参数:
context :上下文,用于标识日志输出的位置
options :配置项,只有一个timestamp
属性,表示是否输出 +ms ,默认为false
其日志方法接收两个参数:
message :输出的消息,可以是对象类型,配合format()
方法,用于格式化输出多个消息
context :上下文,用于标识日志输出的位置。
注意: 当Logger实例也配置了context时,以Logger实例的context为准,并且会在输出的日志中追加一条日志方法的context信息。
1 2 3 4 5 6 7 import { Logger } from '@nestjs/common' ;const logger = new Logger ('app' , { timestamp : true , }); logger.log (format ('%s %s %s' , 'GET' , '/test' , 'test' ), 'log' );
依赖注入Logger 手动实例化Logger对象,破坏了单例,不利于统一日志。
包装一层 LoggerModule
,通过依赖注入的方式,统一管理Logger实例。
1 2 3 4 5 6 7 8 import { Module } from '@nestjs/common' ;import { MyLoggerService } from './my-logger.service' ;@Module ({ providers : [MyLoggerService ], exports : [MyLoggerService ], }) export class MyLoggerModule {}
1 2 3 4 import { ConsoleLogger , Injectable } from '@nestjs/common' ;@Injectable ()export class MyLoggerService extends ConsoleLogger {}
设置为全局模块,或在需要的地方导入使用,使用 setContext()
设置上下文。
1 2 3 4 5 6 7 8 9 10 11 12 13 import { MyLoggerService } from './my-logger/my-logger.service' ;@Controller ()export class AppController { constructor ( private readonly logger: MyLoggerService, ) { this .logger .setContext (AppController .name ); } @Get ('test' ) getTest ( ) { this .logger .log (format ('%s %s %s' , 'GET' , '/test' , 'test' )); } }
若需用于系统日志记录,还需要通过 app.useLogger()
应用自定义Logger,并开启 bufferLogs
日志缓冲。
1 2 3 4 5 const app = await NestFactory .create (AppModule , { bufferLogs : true , }); app.useLogger (app.get (MyLoggerService ));
app.get()
可以检索某个类型的单例实例,依赖于首先在另一个模块中注入该实例。无需再重复实例化。
ModuleRef模块引用 ModuleRef 类用于检索模块中的提供者实例 ,并使用注入令牌作为查找键名来获取实例的引用。
ModuleRef
可以通过常规方法注入到类中。
1 2 3 4 import { ModuleRef } from '@nestjs/core' ;class AppController { constructor (private readonly moduleRef: ModuleRef ) {} }
获取实例 ModuleRef.get()
使用注入标记/类名检索当前模块中存在(已实例化)的提供者、控制器或可注入项(例如,守卫、拦截器等)
若该提供者是其它模块定义并暴露的,需要将strict
设置为false ,从全局上下文中检索。
1 2 import { UserService } from './user/user.service' ;moduleRef.get (UserService , { strict : false });
ModuleRef
无视了模块的依赖关系,能直接从全局上下文中检索提供者实例。 若需要另一个模块的提供者实例,可以不暴露该提供者、不导入该模块,直接通过ModuleRef
检索。
1 2 3 4 5 6 7 8 9 10 11 12 13 @Controller ('download' )export class DownloadController { private userService : UserService ; constructor ( private readonly moduleRef: ModuleRef, ) { this .userService = moduleRef.get (UserService , { strict : false }); } @Get () get ( ) { return this .userService .findOne (1 ); } }
注意: 不能通过get()
方法检索作用域提供者(瞬态或请求作用域)
解析作用域提供者 通过 ModuleRef.resolve()
方法解析作用域提供者,返回该提供者的唯一实例 (Promise)。
多次调用该方法,返回的是不同的实例。
1 2 3 4 5 const transientServices = await Promise .all ([ this .moduleRef .resolve (TransientService ), this .moduleRef .resolve (TransientService ), ]); console .log (transientServices[0 ] === transientServices[1 ]);
为了在多个 resolve()
调用之间生成单个实例 ,可以通过 ContextIdFactory
创建一个上下文标识,并传递给 resolve()
方法。相同的上下文标识,返回的是相同的实例。
1 2 3 4 5 6 7 const contextId = ContextIdFactory .create ();const transientServices = await Promise .all ([ this .moduleRef .resolve (TransientService , contextId), this .moduleRef .resolve (TransientService , contextId), ]); console .log (transientServices[0 ] === transientServices[1 ]);
请求提供者 若通过 resolve()
解析请求作用域提供者 ,会导致 REQUEST
提供者注入为 undefined
。因为它们不是由 Nest 依赖注入系统实例化和管理的。
一个请求作用域提供者 1 2 3 4 5 6 7 8 9 10 11 import { REQUEST } from '@nestjs/core' ;@Injectable ()export class AppService { @Inject (REQUEST ) res : Request ; getHello (): string { console .log (this .res .url ); return 'Hello World!' ; } }
需要使用 registerRequestByContextId()
方法,先将请求对象注册到上下文中,再通过 resolve()
方法解析请求作用域提供者。
1 2 3 const contextId = ContextIdFactory .create ();this .moduleRef .registerRequestByContextId (, contextId);await this .moduleRef .resolve (TransientService , contextId),
若在请求提供者中解析另一个请求提供者,可以更方便地通过 getByRequest()
方法基于请求对象创建一个上下文标识,并将其传递给 resolve()
调用:
1 2 const contextId = ContextIdFactory .getByRequest (this .request );await this .moduleRef .resolve (TransientService , contextId);
动态实例化 ModuleRef.create()
动态实例化一个之前没有注册为提供者的类。
即使该类已经被注册为提供者且已经实例化,也会创建一个新的实例(Promise)。
1 await this .moduleRef .create (oneService);
与直接 new
不同,create()
会处理依赖注入,但这些依赖必须是执行 create()
所在的模块中能访问到的,缺失的依赖需要导入到该模块中。
生命周期事件 Nest 应用以及每个应用元素都有一个由 Nest 管理的生命周期。文档
Nest 提供了许多钩子方法,用于在生命周期的不同阶段执行自定义逻辑。
onModuleInit()
一旦解决了模块的依赖,就会调用。
onApplicationBootstrap()
在所有模块初始化后调用,但在监听连接之前。
onModuleDestroy()
* 在收到终止信号(如 SIGINT)或 app.close()
后调用。
beforeApplicationShutdown()
* 在所有 onModuleDestroy 完成后调用。
onApplicationShutdown()
* 在连接关闭后调用。
注意: 请求作用域的类没有生命周期钩子。它们专门为每个请求创建,并在发送响应后自动进行垃圾回收。
*若没有显式调用 app.close()
,则三个关闭钩子默认是不会监听系统信号(如 SIGINT)的,监听系统信号会消耗较多的系统资源。可以通过 app.enableShutdownHooks()
启用对系统信号的监听,但信号在windows上可能无法预期地工作。
使用钩子 每个生命周期钩子都由一个接口表示。
以 onModuleInit()
为例:需要实现 OnModuleInit
接口。
1 2 3 4 5 6 @Injectable ()export class UserService implements OnModuleInit { onModuleInit ( ) { console .log (`The UserService has been initialized.` ); } }
如果生命周期钩子返回一个 Promise,Nest 将等待这个 Promise 完成(或者解决)之后再继续生命周期。
所以onModuleInit()
和onApplicationBootstrap()
钩子可以是异步的,以推迟 模块的初始化,可以完成如数据库连接等异步工作后,在完成应用初始化、监听连接。
1 2 3 4 5 6 7 8 9 10 class UserService implements OnModuleInit { async onModuleInit (): Promise <void > { return await new Promise ((resolve ) => { setTimeout (() => { console .log ('初始化完成' ); resolve (); }, 3000 ); }); } }
整体案例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 export class AppModule { onModuleInit (): any { console .log ('onModuleInit.' ); } onApplicationBootstrap (): any { console .log ('onApplicationBootstrap down.' ); } onModuleDestroy (): any { console .log ('onModuleDestroy' ); } beforeApplicationShutdown (signal?: string ): any { console .log ('beforeApplicationShutdown:' , signal); } onApplicationShutdown (signal?: string ): any { console .log ('OnApplicationShutdown:' , signal); } }
1 2 3 4 5 6 onModuleInit. onApplicationBootstrap down. [Nest] 20808 - 2024/02/17 18:03:51 LOG [NestApplication] Nest application successfully started onModuleDestroy beforeApplicationShutdown: SIGINT OnApplicationShutdown: SIGINT
swagger接口文档 Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务的接口文档。
Swagger UI 用于将 Swagger 规范生成的文档呈现为交互式的、动态的 API 文档。
在Nset中使用需安装@nestjs/swagger ,npm i -S @nestjs/swagger
,文档
使用 SwaggerModule
类初始化 Swagger 文档,DocumentBuilder
类配置文档。
main.ts 基本使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import { SwaggerModule , DocumentBuilder } from '@nestjs/swagger' ;async function bootstrap ( ) { const app = await NestFactory .create (AppModule ); const config = new DocumentBuilder () .setTitle ('接口文档' ) .setDescription ('一个API文档' ) .setVersion ('1.0' ) .build (); const document = SwaggerModule .createDocument (app, config); SwaggerModule .setup ('api' , app, document ); await app.listen (3000 ); }
DocumentBuilder
提供了一系列的方法用于构建符合 OpenAPI 规范的基本文档。
DocumentBuilder类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class DocumentBuilder { private readonly logger; private readonly document ; setTitle (title : string ): this ; setDescription (description : string ): this ; setVersion (version : string ): this ; setTermsOfService (termsOfService : string ): this ; setContact (name : string , url : string , email : string ): this ; setLicense (name : string , url : string ): this ; addServer (url : string , description?: string , variables?: Record <string , ServerVariableObject >): this ; setExternalDoc (description : string , url : string ): this ; setBasePath (path : string ): this ; addTag (name : string , description?: string , externalDocs?: ExternalDocumentationObject ): this ; addExtension (extensionKey : string , extensionProperties : any ): this ; addSecurity (name : string , options : SecuritySchemeObject ): this ; addGlobalParameters (...parameters : ParameterObject []): this ; addSecurityRequirements (name : string | SecurityRequirementObject , requirements?: string []): this ; addBearerAuth (options?: SecuritySchemeObject , name?: string ): this ; addOAuth2 (options?: SecuritySchemeObject , name?: string ): this ; addApiKey (options?: SecuritySchemeObject , name?: string ): this ; addBasicAuth (options?: SecuritySchemeObject , name?: string ): this ; addCookieAuth (cookieName?: string , options?: SecuritySchemeObject , securityName?: string ): this ; build (): Omit <OpenAPIObject , 'paths' >; }
SwaggerModule.createDocument
返回的是一个符合OpenAPI 规范的 OpenAPIObject
可序列化对象,不仅可以通过 HTTP 进行托管,还可以将其保存为 JSON/YAML 文件,并以不同的方式使用。
SwaggerModule类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 class SwaggerModule { private static readonly metadataLoader; static createDocument (app : INestApplication , config : Omit <OpenAPIObject , 'paths' >, options?: SwaggerDocumentOptions ): OpenAPIObject ; static loadPluginMetadata (metadataFn : () => Promise <Record <string , any >>): Promise <void >; private static serveStatic; private static serveDocuments; static setup (path : string , app : INestApplication , documentOrFactory : OpenAPIObject | (() => OpenAPIObject ), options?: SwaggerCustomOptions ): void ; } type OperationIdFactory = (controllerKey: string , methodKey: string , version?: string ) => string ;interface SwaggerDocumentOptions { include?: Function []; extraModels?: Function []; ignoreGlobalPrefix?: boolean ; deepScanRoutes?: boolean ; operationIdFactory?: OperationIdFactory ; } interface SwaggerCustomOptions { useGlobalPrefix?: boolean ; explorer?: boolean ; swaggerOptions?: SwaggerUiOptions ; customCss?: string ; customCssUrl?: string | string []; customJs?: string | string []; customJsStr?: string | string []; customfavIcon?: string ; customSwaggerUiPath?: string ; swaggerUrl?: string ; customSiteTitle?: string ; validatorUrl?: string ; url?: string ; urls?: Record <'url' | 'name' , string >[]; jsonDocumentUrl?: string ; yamlDocumentUrl?: string ; patchDocumentOnRequest?: <TRequest = any, TResponse = any > (req: TRequest, res: TResponse, document: OpenAPIObject) => OpenAPIObject; }
Swagger 提供了许多 @Api*()
装饰器,用于描述 API,在Swagger UI中显示。
下面是常用的装饰器简介。详细文档
@ApiTags()
将控制器、路由方法分组。
1 2 3 @ApiTags ('login' )@Controller ('login' )export class LoginController {}
ApiOperation描述路由 @ApiOperation()
描述路由方法。
1 2 3 4 @ApiOperation ({ summary : '创建登录' , description : '创建登录' , })
描述参数 有一些装饰器用于描述路由方法的参数,也就是接口所需的参数。作用于路由方法。
@ApiParam()
描述动态路由参数。
@ApiQuery()
描述查询字符串参数。
@ApiBody()
描述请求体参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Get ()@ApiParam ({ name : 'id' , description : '用户id' , required : true , }) findOne (@Param ('id' , ParseIntPipe) id: number ) {}@Get ()@ApiQuery ({ name : 'page' , description : '页码' , required : false , }) findAll (@Query ('page' , new DefaultValuePipe(0 ), ParseIntPipe) page: number ) {}@Post ()@ApiBody ({ type : CreateLoginDto , }) create (@Body () createLoginDto: CreateLoginDto ) {}
ApiProperty描述属性 在Nest中使用DTO来约束、验证请求体的结构。使用 @ApiProperty()
描述DTO的属性。
ApiBody 和ApiProperty 共同完成请求体的描述。有些时候ApiBody 可以省略。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class CreateLoginDto { @ApiProperty ({ description : '用户名' , example : 'user1' , minLength : 3 , maxLength : 10 , required : true , }) @IsNotEmpty () @IsString () @Length (3 , 10 , { message : '用户名长度必须为3到10位' , }) username : string ; @ApiProperty ({ description : '密码' , example : '123456' , required : true , }) @IsNotEmpty () @IsString () password : string ; }
通过 @ApiProperty()
可以设置属性的描述、示例、必填等信息,但对每个属性手动添加 @ApiProperty()
过于繁琐,且与 class-validator
的验证装饰器的意义重复。一旦拥有大量的DTO类,代码会变得冗长且难以维护。
Nest提供了CLI插件 (Swagger插件)与 class-validator
结合使用,自动生成DTO描述。而无需手动添加 @ApiProperty()
。
CLI插件 CLI插件 (Swagger插件)将自动执行以下操作:
除非使用@ApiHideProperty
,否则会对所有DTO属性进行注解,使用@ApiProperty
。
根据问号(例如 name?: string)设置required属性(将设置为false)。
根据类型设置type或enum属性(还支持数组)。
根据分配的默认值设置default属性。
根据class-validator
装饰器设置多个验证规则(如果将classValidatorShim
设置为true)。
为每个具有适当状态和type(响应模型)的端点添加响应装饰器。
根据注释为属性和端点生成描述(如果将introspectComments
设置为true)。
根据注释为属性生成示例值(如果将introspectComments
设置为true)。
修改 nest-cli.json
配置文件,启用插件。
1 2 3 4 5 6 7 8 9 10 11 12 13 { "compilerOptions" : { "plugins" : [ { "name" : "@nestjs/swagger" , "options" : { "classValidatorShim" : true , "introspectComments" : true } } ] } }
options
属性用于自定义插件的行为。
1 2 3 4 5 6 7 8 interface PluginOptions { dtoFileNameSuffix?: string []; controllerFileNameSuffix?: string []; classValidatorShim?: boolean ; dtoKeyOfComment?: string ; controllerKeyOfComment?: string ; introspectComments?: boolean ; }
启用注释自省introspectComments
功能后,CLI插件将根据注释为属性生成描述和示例值。
1 2 3 4 5 6 7 8 9 @ApiProperty ({ description : `用户名` , example : 'user1' , })
一些注意事项:
文件名必须为 .dto.ts
或 .entity.ts
后缀,以便插件识别。也可以通过dtoFileNameSuffix
自定义后缀名。
在更新插件选项时,请确保删除dist文件夹并重新构建应用程序。
即使启用了插件,仍然可以手动添加@ApiProperty()
装饰器,以扩展 或覆盖 插件生成的描述。这对于添加描述和示例值非常有用。
在DTO中使用映射类型工具(例如PartialType
)时,应该从@nestjs/swagger
导入,而不是@nestjs/mapped-types
,以便插件能够获取模式信息。
如果不使用CLI,而是使用自定义的webpack配置,可以将此插件与ts-loader
结合使用
ApiResponse描述响应 @ApiResponse()
描述路由方法的响应。
1 2 3 4 5 @ApiResponse ({ status : 200 , description : '成功' , type : String , })
Nest提供了一些简写的API响应装饰器,它们都继承自@ApiResponse
装饰器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @ApiOkResponse ()@ApiCreatedResponse ()@ApiAcceptedResponse ()@ApiNoContentResponse ()@ApiMovedPermanentlyResponse ()@ApiFoundResponse ()@ApiBadRequestResponse ()@ApiUnauthorizedResponse ()@ApiNotFoundResponse ()@ApiForbiddenResponse ()@ApiMethodNotAllowedResponse ()@ApiNotAcceptableResponse ()@ApiRequestTimeoutResponse ()@ApiConflictResponse ()@ApiPreconditionFailedResponse ()@ApiTooManyRequestsResponse ()@ApiGoneResponse ()@ApiPayloadTooLargeResponse ()@ApiUnsupportedMediaTypeResponse ()@ApiUnprocessableEntityResponse ()@ApiInternalServerErrorResponse ()@ApiNotImplementedResponse ()@ApiBadGatewayResponse ()@ApiServiceUnavailableResponse ()@ApiGatewayTimeoutResponse ()@ApiDefaultResponse ()
要为请求指定返回模型,必须创建一个DTO类,并将其传递给 type
属性。
1 2 3 4 5 6 7 8 9 10 11 12 export class CreateLoginResDto { @ApiProperty ({ description : '用户名' , example : 'user1' , }) username : string ; @ApiProperty ({ description : '消息' , example : '登陆成功' , }) message : string ; }
1 2 3 4 5 @ApiResponse ({ status : 200 , description : '成功' , type : CreateLoginResDto , })
@ApiHeader()
描述请求头。@ApiHeaders()
传入数组,描述多个请求头。
1 2 3 4 5 @ApiHeader ({ name : 'Authorization' , description : 'token' , required : true , })
全局参数 addGlobalParameters()
添加全局参数,它们将出现在每个路由方法的参数中。
1 2 3 4 5 6 new DocumentBuilder ().addGlobalParameters ({ name : 'Authorization' , description : 'token' , required : true , in : 'header' , })
安全机制 @ApiSecurity()
定义特定操作应使用的安全机制
1 2 3 @ApiSecurity ('basic' )@Controller ('login' )export class LoginController {}
需要提前在 DocumentBuilder
中配置安全机制。
1 2 3 4 new DocumentBuilder ().addSecurity ('basic' , { type : 'http' , scheme : 'basic' , })
一些常用的身份验证机制是内置的,而不必手动定义,如basic
、bearer
等。
@ApiBasicAuth()
基本身份验证
@ApiBearerAuth()
Bearer身份验证
@ApiOAuth2()
OAuth2身份验证
@ApiCookieAuth()
Cookie身份验证
还需要在 DocumentBuilder
中添加安全定义。
1 2 3 4 5 6 7 8 9 10 11 @ApiBasicAuth ()new DocumentBuilder ().addBasicAuth ();@ApiBearerAuth ()new DocumentBuilder ().addBearerAuth ();@ApiOAuth2 (['pets:write' ])new DocumentBuilder ().addOAuth2 ();@ApiCookieAuth ()new DocumentBuilder ().addCookieAuth ('optional-session-id' );
swagger-typescript-api swagger-typescript-api 可以根据 Swagger 规范生成接口的 TS 类型和axios/fetch请求函数。
该库提供了全局命令行工具,也可以导入为模块,使用 generateApi
方法生成接口文件。
1 2 3 4 5 6 7 8 9 10 11 12 async function bootstrap ( ) { const app = await NestFactory .create (AppModule ); await app.listen (3000 ); await generateApi ({ name : 'api.ts' , output : resolve (process.cwd (), './api' ), url : 'http://localhost:3000/api-json' , httpClientType : 'axios' , }); } bootstrap ();
生成的接口文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 export interface CreateLoginDto { username : string ; password : string ; }
JWT token 一个通用的后端通常使用JWT(JSON Web Token)进行会话控制、身份验证。至于session,详见session案例
NodeJS接口、会话控制-token 、JSON Web Token 入门教程-阮一峰
流程: 服务端登陆接口验证账号密码,签发token,客户端携带token访问受保护的资源,服务端通过守卫验证token,放行受保护的路由。在token中保存角色身份,以实现基于角色的鉴权。
在Nest中使用JWT 安装 @nestjs/jwt 用于生成和验证JWT。文档
JwtModule
用于配置JWT。
src\auth\auth.module.ts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import { JwtModule } from '@nestjs/jwt' ;@Module ({ imports : [ JwtModule .register ({ global : true , secret : '123456' , signOptions : { expiresIn : 60 * 60 * 24 , }, }), ], controllers : [AuthController ], providers : [AuthService ], exports : [AuthService ], }) export class AuthModule {}
JwtService
用于生成和验证JWT
JwtService.sign()
生成JWT,Async为异步方法
JwtService.verify()
验证JWT
在 AuthService
中使用 sign()
生成token,将用户名、角色、id等信息保存在token中。使用 verify()
验证token。
src\auth\auth.service.ts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import { JwtService } from '@nestjs/jwt' ;@Injectable ()export class AuthService { constructor (private readonly jwtService: JwtService ) {} async signIn (user: any ) { const payload = { username : user.username , id : user.userId , role : user.role , }; return { access_token : await this .jwtService .signAsync (payload), }; } async verifyToken (token: string ) { return this .jwtService .verifyAsync (token, { secret : '123456' , }); } }
测试登陆接口,获取token。实际应该通过账号密码,验证成功后生成token。
1 2 3 4 5 6 7 8 9 10 11 @Post ('testLogin' )testLogin ( ) { return this .authService .signIn ({ username : 'user1' , userId : 1 , role : 'admin' , }); }
生成后的token一般由客户端保存在本地,每次请求时携带token,以一定格式放在请求头 Authorization
字段中。
token放在请求头中 1 Authorization: Bearer eyJhbGciOiJIUzI1NiIsI....
在 AuthGuard
守卫中调用 AuthService.verifyToken()
验证token。 并将用户信息存入请求对象,以便后续守卫(角色鉴权)、路由(获取用户信息)等使用。
src\auth\auth.guard.ts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import { AuthService } from './auth.service' ;@Injectable ()export class AuthGuard implements CanActivate { constructor (private readonly authService: AuthService ) {} async canActivate (context : ExecutionContext ): Promise <boolean > { const request = context.switchToHttp ().getRequest <Request >(); const token = this .extractTokenFromHeader (request); if (!token) { throw new UnauthorizedException (); } try { const payload = await this .authService .verifyToken (token); request['user' ] = payload; } catch (e) { throw new UnauthorizedException (); } return true ; } private extractTokenFromHeader (request : Request ): string | undefined { const [type , token] = request.get ('authorization' )?.split (' ' ) ?? []; return type === 'Bearer' ? token : undefined ; } }
测试受保护的路由
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @ApiBearerAuth ()@UseGuards (AuthGuard )@Get ('list' )list (@Req () req: Request ) { console .log (req['user' ]); return 'list' ; }
config配置 一些关键的信息如jwt的加密密钥、数据库链接等,应该通过配置提供,而不是硬编码,12-Factor应用原则
在Node中,可以使用dotenv ,将环境变量保存在.env
文件中,通过process.env
读取。
Nest提供了更好的做法,导入 ConfigModule
模块,使用 ConfigService
服务,该服务加载适当的 .env
文件,这样一个通用的配置模块Nest已经提供了@nestjs/config ,其底层也使用了dotenv 。文档
安装: npm i -S @nestjs/config
键冲突:当一个键同时存在于运行时环境变量和 .env 文件中时,运行时环境变量优先。
使用ConfigModule ConfigModule
模块用于加载配置文件,通常导入到根模块中,并设为全局模块。
1 2 3 4 5 6 7 8 9 10 11 12 13 static forRoot (options?: ConfigModuleOptions ): DynamicModule ;interface ConfigModuleOptions { cache?: boolean ; isGlobal?: boolean ; ignoreEnvFile?: boolean ; ignoreEnvVars?: boolean ; envFilePath?: string | string []; validate?: (config: Record<string , any > ) => Record <string , any >; validationSchema?: any ; validationOptions?: Record <string , any >; load?: Array <ConfigFactory >; expandVariables?: boolean | DotenvExpandOptions ; }
该模块使用 forRoot()
静态方法来控制其行为。
isGlobal
是否全局模块
envFilePath
环境变量文件
ignoreEnvFile
忽略环境变量文件
load
自定义加载配置文件
expandVariables
扩展变量
cache
缓存环境变量
validationSchema
模式验证
validationOptions
配置验证选项
validate
自定义验证环境变量
配置案例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import { ConfigModule } from '@nestjs/config' ;import configuration from './configuration' ;@Module ({ imports : [ ConfigModule .forRoot ({ isGlobal : true , envFilePath : '.env' , load : [configuration], expandVariables : true , cache : true , validationOptions : { allowUnknown : false , abortEarly : true , }, validate (config ) { return config; }, }), ], }) export class AppModule {}
env相关选项 envFilePath
用于指定环境变量文件,string | string[]
。
1 2 envFilePath : '.development.env' ,envFilePath : ['.env.development.local' , '.env.development' ],
ignoreEnvFile
忽略环境变量文件,只使用运行时环境变量。ignoreEnvVars
忽略所有环境变量。
expandVariables
启用扩展变量,允许在env文件中嵌套变量。
1 2 APP_URL=mywebsite.com SUPPORT_EMAIL=support@${APP_URL} # 'support@mywebsite.com'
自定义配置文件 load
用于自定义加载配置文件,值为 ConfigFactory
数组,它是一个工厂函数,返回一个配置对象。
在 ConfigFactory
中可以访问到已经解析完环境变量的 process.env
。
src/configuration.ts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const configFactory = ( ) => ({ port : parseInt (process.env .PORT , 10 ) || 3000 , database : { host : process.env .DATABASE_HOST , port : parseInt (process.env .DATABASE_PORT , 10 ) || 5432 , }, jwt : { secret : '123456' , expiresIn : 60 * 60 * 24 , }, }); export type Configuration = ReturnType <typeof configFactory>;export default (): Configuration => configFactory;
1 2 import configuration from './configuration' ;load : [configuration],
配置文件除了导出一个工厂函数外,还应该导出一个配置类型,以便在其他地方使用,获得类型提示。
注意: 多个配置文件最终会合并为一个配置对象。要区分不同的配置文件,可以使用命名空间。
配置命名空间 load
允许加载多个配置文件,每个配置文件可以返回一个对象,这些对象将被合并到一个配置对象中。
为了避免冲突,可以通过 registerAs()
为每个配置文件指定一个命名空间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import { registerAs } from '@nestjs/config' ;const configFactory = ( ) => ({ port : parseInt (process.env .PORT , 10 ) || 3000 , database : { host : process.env .DATABASE_HOST , port : parseInt (process.env .DATABASE_PORT , 10 ) || 5432 , }, jwt : { secret : '123456' , expiresIn : 60 * 60 * 24 , }, }); export type Configuration = ReturnType <typeof configFactory>;export const configToken = 'configuration' ;export default registerAs (configToken, configFactory);
使用ConfigService ConfigService
服务使用 get()
读取配置对象中的值。
对于普通的 ConfigFactory
配置文件,可以直接获取配置对象的属性。 对于 registerAs()
创建的具有命名空间的配置,需要先获取命名空间,再获取属性,或者使用点表示法获取属性。
下面以jwt配置为例。将jwt的密钥通过配置对象获取。
src\auth\auth.service.ts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import { Configuration , configToken } from 'src/configuration' ;@Injectable ()export class AuthService { constructor ( private readonly jwtService: JwtService, private readonly configService: ConfigService, ) {} async verifyToken (token: string ) { return this .jwtService .verifyAsync (token, { secret : this .configService .get <Configuration >(configToken).jwt .secret , }); } }
对于 JwtModule
的配置,也可以通过 ConfigService
获取。
为了注入 ConfigService
,需使用 JwtModule.registerAsync()
异步配置方法。问题参考
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import { JwtModule } from '@nestjs/jwt' ;import { ConfigService } from '@nestjs/config' ;import { Configuration , configToken } from 'src/configuration' ;@Module ({ imports : [ JwtModule .registerAsync ({ useFactory : (configService: ConfigService ) => ({ secret : configService.get <Configuration >(configToken).jwt .secret , signOptions : { expiresIn : configService.get <Configuration >(configToken).jwt .expiresIn , }, }), inject : [ConfigService ], }), ], controllers : [AuthController ], providers : [AuthService ], exports : [AuthService ], }) export class AuthModule {}
get()
方法还可以接收第二个参数,用于定义默认值。当键不存在时,将返回该默认值。
ConfigService
可以接收两个泛型
第一个泛型用于防止访问不存在的配置属性
第二个泛型为boolean,以消除tsconfig选项 strictNullChecks
打开时,可能返回的所有undefined类型
1 2 3 4 5 6 7 8 interface EnvironmentVariables { PORT : number ; TIMEOUT : string ; } constructor (private configService: ConfigService<EnvironmentVariables> ) { const port = this .configService .get ('PORT' , { infer : true }); }
validate验证环境变量 validate(config)
是同步 的,参数为所有环境变量 ,用于验证所需的环境变量是否符合某些验证规则,防止预期之外的环境变量值被传入配置对象。
与自定义类验证管道类似,使用 class-validator
和 class-transformer
库。
编写验证类和验证函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import { plainToInstance } from 'class-transformer' ;import { IsDefined , IsNumber , IsString , validateSync } from 'class-validator' ;class EnvironmentVariables { @IsNumber () PORT ?: number ; @IsString () @IsDefined () DATABASE_HOST : string ; @IsNumber () DATABASE_PORT ?: number ; } export function validate (config: Record<string , unknown> ) { const validatedConfig = plainToInstance (EnvironmentVariables , config, { enableImplicitConversion : true , }); const errors = validateSync (validatedConfig, { skipMissingProperties : true , }); if (errors.length > 0 ) { throw new Error (errors.toString ()); } return validatedConfig; }
因为 validate()
是同步 的,所以必须使用 validateSync()
同步方法。
应用验证函数 1 2 3 4 import { validate } from './config.validation' ;ConfigModule .forRoot ({ validate, }),
若验证失败,则会直接抛出错误,并终止应用程序。
1 2 3 4 5 6 7 8 Error: An instance of EnvironmentVariables has failed the validation: - property NODE_ENV has failed the following constraints: isEnum ,An instance of EnvironmentVariables has failed the validation: - property PORT has failed the following constraints: isNumber ,An instance of EnvironmentVariables has failed the validation: - property DATABASE_HOST has failed the following constraints: isNumber ,An instance of EnvironmentVariables has failed the validation: - property DATABASE_PORT has failed the following constraints: isNumber
在main中使用 在 main.ts
中通过 app.get()
获取已存在的 ConfigService
实例引用
1 2 const configService = app.get (ConfigService );await app.listen (configService.get <Configuration >(configToken).port );
局部注册配置文件 一些配置文件可能只在特定模块中使用,可以使用 forFeature()
方法注册配置文件。而不必将所有配置文件都在 forRoot()
中注册。
1 2 3 4 5 6 7 8 9 10 export default registerAs ('db' , () => ({ s1 : 222 , s2 : 111 , })); import dbConfig from './db.configuration' ;@Module ({ imports : [ConfigModule .forFeature (dbConfig)], }) export class DbModule {}
在该模块的控制器中访问配置。
1 2 3 4 5 @Get ()findAll ( ) { console .log (this .configService .get ('db' ).s1 ); return this .dbService .findAll (); }
统一注册和局部注册效果是差不多的 ,都能在所需的地方通过注入 ConfigService
来访问,只是局部注册需要注意模块依赖和模块的初始化顺序。
若在依赖该模块 的模块中访问局部注册的配置,可能需要使用 onModuleInit()
钩子,而不是在构造函数中,因为 forFeature()
方法在模块初始化期间运行,而模块初始化的顺序是不确定的,这些配置所依赖的模块可能尚未初始化。onModuleInit()
方法仅在所有依赖的模块都已初始化后才运行,因此是安全的。
所以配置文件不是太多的话,还是统一注册比较方便。