认证(Authentication)

身份验证是大多数现有应用程序的重要组成部分。有许多不同的方法、策略和方法来处理用户授权。任何项目采用的方法取决于其特定的应用程序要求。本章介绍了几种可以适应各种不同要求的身份验证方法。

passport 是目前最流行的 node.js 认证库,为社区所熟知,并相继应用于许多生产应用中。将此工具与 Nest 框架集成起来非常简单。为了演示,我们将设置 passport-http-bearer 和 passport-jwt 策略。

Passport是最流行的 node.js 身份验证库,为社区所熟知,并成功地应用于许多生产应用程序中。将这个库与使用 @nestjs/passport 模块的 Nest 应用程序集成起来非常简单。在较高级别,Passport 执行一系列步骤以:

  • 通过验证用户的”证”(例如用户名/密码、JSON Web令牌( JWT )或身份提供者的身份令牌)来验证用户的身份。

  • 管理经过身份验证的状态(通过发出可移植的令牌,例如 JWT,或创建一个 Express 会话)

  • 将有关经过身份验证的用户的信息附加到请求对象,以便在路由处理程序中进一步使用

Passport具有丰富的策略生态系统,可实施各种身份验证机制。 尽管概念上很简单,但是您可以选择的 Passport 策略集非常多,并且有很多种类。 Passport 将这些不同的步骤抽象为标准模式,而 @nestjs/passport 模块将该模式包装并标准化为熟悉的Nest构造。

在本章中,我们将使用这些强大而灵活的模块为 RESTful API服务器实现完整的端到端身份验证解决方案。您可以使用这里描述的概念来实现 Passport 策略,以定制您的身份验证方案。您可以按照本章中的步骤来构建这个完整的示例。您可以在这里找到带有完整示例应用程序的存储库。

身份认证

让我们充实一下我们的需求。对于此用例,客户端将首先使用用户名和密码进行身份验证。一旦通过身份验证,服务器将发出 JWT,该 JWT 可以在后续请求的授权头中作为 token发送,以验证身份验证。我们还将创建一个受保护的路由,该路由仅对包含有效 JWT 的请求可访问。

我们将从第一个需求开始:验证用户。然后我们将通过发行 JWT 来扩展它。最后,我们将创建一个受保护的路由,用于检查请求上的有效 JWT

首先,我们需要安装所需的软件包。Passport 提供了一种名为 Passport-local 的策略,它实现了一种用户名/密码身份验证机制,这符合我们在这一部分用例中的需求。

  1. $ npm install --save @nestjs/passport passport passport-local
  2. $ npm install --save-dev @types/passport-local

对于您选择的任何 Passport 策略,都需要 @nestjs/PassportPassport 包。然后,需要安装特定策略的包(例如,passport-jwtpassport-local),它实现您正在构建的特定身份验证策略。此外,您还可以安装任何 Passport策略的类型定义,如上面的 @types/Passport-local 所示,它在编写 TypeScript 代码时提供了帮助。

Passport 策略

现在可以实现身份认证功能了。我们将首先概述用于任何 Passport 策略的流程。将 Passport 本身看作一个框架是有帮助的。框架的优雅之处在于,它将身份验证过程抽象为几个基本步骤,您可以根据实现的策略对这些步骤进行自定义。它类似于一个框架,因为您可以通过提供定制参数(作为 JSON 对象)和回调函数( Passport 在适当的时候调用这些回调函数)的形式来配置它。 @nestjs/passport 模块将该框架包装在一个 Nest 风格的包中,使其易于集成到 Nest 应用程序中。下面我们将使用 @nestjs/passport ,但首先让我们考虑一下 vanilla Passport 是如何工作的。

vanilla Passport 中,您可以通过提供以下两项配置策略:

  1. 组特定于该策略的选项。例如,在 JWT 策略中,您可以提供一个秘令来对令牌进行签名。

  2. “验证回调”,在这里您可以告诉 Passport 如何与您的用户存储交互(在这里您可以管理用户帐户)。在这里,验证用户是否存在(或创建一个新用户),以及他们的凭据是否有效。Passport 库期望这个回调在验证成功时返回完整的用户消息,在验证失败时返回 null(失败定义为用户没有找到,或者在使用 Passport-local 的情况下,密码不匹配)。

使用 @nestjs/passport ,您可以通过扩展 PassportStrategy 类来配置 passport 策略。通过调用子类中的 super() 方法传递策略选项(上面第1项),可以选择传递一个 options 对象。通过在子类中实现 validate() 方法,可以提供verify 回调(上面第2项)。

我们将从生成一个 AuthModule 开始,其中有一个 AuthService :

  1. $ nest g module auth
  2. $ nest g service auth

当我们实现 AuthService 时,我们会发现在 UsersService 中封装用户操作是很有用的,所以现在让我们生成这个模块和服务:

  1. $ nest g module users
  2. $ nest g service users

替换这些生成文件的默认内容,如下所示。对于我们的示例应用程序,UsersService 只是在内存中维护一个硬编码的用户列表,以及一个根据用户名检索用户列表的 find 方法。在真正的应用程序中,这是您使用选择的库(例如 TypeORMSequelizeMongoose等)构建用户模型和持久层。

users/users.service.ts

  1. import { Injectable } from '@nestjs/common';
  2. export type User = any;
  3. @Injectable()
  4. export class UsersService {
  5. private readonly users: User[];
  6. constructor() {
  7. this.users = [
  8. {
  9. userId: 1,
  10. username: 'john',
  11. password: 'changeme',
  12. },
  13. {
  14. userId: 2,
  15. username: 'chris',
  16. password: 'secret',
  17. },
  18. {
  19. userId: 3,
  20. username: 'maria',
  21. password: 'guess',
  22. },
  23. ];
  24. }
  25. async findOne(username: string): Promise<User | undefined> {
  26. return this.users.find(user => user.username === username);
  27. }
  28. }

UsersModule 中,惟一需要做的更改是将 UsersService 添加到 @Module 装饰器的 exports 数组中,以便提供给其他模块外部可见(我们很快将在 AuthService 中使用它)。

users/users.module.ts

  1. import { Module } from '@nestjs/common';
  2. import { UsersService } from './users.service';
  3. @Module({
  4. providers: [UsersService],
  5. exports: [UsersService],
  6. })
  7. export class UsersModule {}

我们的 AuthService 的任务是检索用户并验证密码。为此,我们创建了 validateUser() 方法。在下面的代码中,我们使用 ES6 扩展操作符从 user 对象中提取 password 属性,然后再返回它。稍后,我们将从 Passport 本地策略中调用 validateUser() 方法。

auth/auth.service.ts

  1. import { Injectable } from '@nestjs/common';
  2. import { UsersService } from '../users/users.service';
  3. @Injectable()
  4. export class AuthService {
  5. constructor(private readonly usersService: UsersService) {}
  6. async validateUser(username: string, pass: string): Promise<any> {
  7. const user = await this.usersService.findOne(username);
  8. if (user && user.password === pass) {
  9. const { password, ...result } = user;
  10. return result;
  11. }
  12. return null;
  13. }
  14. }

当然,在实际的应用程序中,您不会以纯文本形式存储密码。 取而代之的是使用带有加密单向哈希算法的 bcrypt 之类的库。使用这种方法,您只需存储散列密码,然后将存储的密码与输入密码的散列版本进行比较,这样就不会以纯文本的形式存储或暴露用户密码。为了保持我们的示例应用程序的简单性,我们违反了这个绝对命令并使用纯文本。不要在真正的应用程序中这样做!

现在,我们更新 AuthModule 来导入 UsersModule

auth/auth.module.ts

  1. import { Module } from '@nestjs/common';
  2. import { AuthService } from './auth.service';
  3. import { UsersModule } from '../users/users.module';
  4. @Module({
  5. imports: [UsersModule],
  6. providers: [AuthService],
  7. })
  8. export class AuthModule {}

现在我们可以实现 Passport 本地身份验证策略。在auth文件夹中创建一个名为 local.strategy.ts 文件,并添加以下代码:

auth/local.strategy.ts

  1. import { Strategy } from 'passport-local';
  2. import { PassportStrategy } from '@nestjs/passport';
  3. import { Injectable, UnauthorizedException } from '@nestjs/common';
  4. import { AuthService } from './auth.service';
  5. @Injectable()
  6. export class LocalStrategy extends PassportStrategy(Strategy) {
  7. constructor(private readonly authService: AuthService) {
  8. super();
  9. }
  10. async validate(username: string, password: string): Promise<any> {
  11. const user = await this.authService.validateUser(username, password);
  12. if (!user) {
  13. throw new UnauthorizedException();
  14. }
  15. return user;
  16. }
  17. }

我们遵循了前面描述的所有护照策略。在我们的 passport-local 用例中,没有配置选项,因此我们的构造函数只是调用 super() ,没有 options 对象。

我们还实现了 validate() 方法。对于每个策略,Passport 将使用适当的特定于策略的一组参数调用 verify 函数(使用 @nestjs/Passport 中的 validate() 方法实现)。对于本地策略,Passport 需要一个具有以下签名的 validate() 方法: validate(username: string, password: string): any

大多数验证工作是在我们的 AuthService 中完成的(在 UserService 的帮助下),所以这个方法非常简单。任何 Passport 策略的 validate() 方法都将遵循类似的模式,只是表示凭证的细节方面有所不同。如果找到了用户并且凭据有效,则返回该用户,以便 Passport 能够完成其任务(例如,在请求对象上创建user 属性),并且请求处理管道可以继续。如果没有找到,我们抛出一个异常,让异常层处理它。

通常,每种策略的 validate() 方法的惟一显著差异是如何确定用户是否存在和是否有效。例如,在 JWT 策略中,根据需求,我们可以评估解码令牌中携带的 userId 是否与用户数据库中的记录匹配,或者是否与已撤销的令牌列表匹配。因此,这种子类化和实现特定于策略验证的模式是一致的、优雅的和可扩展的。

我们需要配置 AuthModule 来使用刚才定义的 Passport 特性。更新 auth.module。看起来像这样:

auth/auth.module.ts

  1. import { Module } from '@nestjs/common';
  2. import { AuthService } from './auth.service';
  3. import { UsersModule } from '../users/users.module';
  4. import { PassportModule } from '@nestjs/passport';
  5. import { LocalStrategy } from './local.strategy';
  6. @Module({
  7. imports: [UsersModule, PassportModule],
  8. providers: [AuthService, LocalStrategy],
  9. })
  10. export class AuthModule {}

内置 Passport 守卫

守卫章节描述了守卫的主要功能:确定请求是否由路由处理程序。这仍然是正确的,我们将很快使用这个标准功能。但是,在使用 @nestjs/passport 模块的情况下,我们还将引入一个新的小问题,这个问题一开始可能会让人感到困惑,现在让我们来讨论一下。从身份验证的角度来看,您的应用程序可以以两种状态存在:

  1. 用户/客户端未登录(未通过身份验证)
  2. 用户/客户端已登录(已通过身份验证)

在第一种情况下(用户没有登录),我们需要执行两个不同的功能:

  • 限制未经身份验证的用户可以访问的路由(即拒绝访问受限制的路由)。 我们将使用熟悉的警卫来处理这个功能,方法是在受保护的路由上放置一个警卫。我们将在这个守卫中检查是否存在有效的 JWT ,所以我们稍后将在成功发出 JWT 之后处理这个守卫。

  • 当以前未经身份验证的用户尝试登录时,启动身份验证步骤。这时我们向有效用户发出 JWT 的步骤。考虑一下这个问题,我们知道需要 POST 用户名/密码凭证来启动身份验证,所以我们将设置 POST /auth/login 路径来处理这个问题。这就提出了一个问题:在这条路由上,我们究竟如何实施“护照-本地”战略?

答案很简单:使用另一种稍微不同类型的守卫。@nestjs/passport 模块为我们提供了一个内置的守卫,可以完成这一任务。这个保护调用 Passport 策略并启动上面描述的步骤(检索凭证、运行verify 函数、创建用户属性等)。

上面列举的第二种情况(登录用户)仅仅依赖于我们已经讨论过的标准类型的守卫,以便为登录用户启用对受保护路由的访问。

登录路由

有了这个策略,我们现在就可以实现一个简单的 /auth/login 路由,并应用内置的守卫来启动护照本地流。打开 app.controller.ts 文件,并将其内容替换为以下内容:

app.controller.ts

  1. import { Controller, Request, Post, UseGuards } from '@nestjs/common';
  2. import { AuthGuard } from '@nestjs/passport';
  3. @Controller()
  4. export class AppController {
  5. @UseGuards(AuthGuard('local'))
  6. @Post('auth/login')
  7. async login(@Request() req) {
  8. return req.user;
  9. }
  10. }

对于 @useguard(AuthGuard('local')),我们使用的是一个 AuthGuard ,它是在我们扩展护照-本地策略时 @nestjs/passportautomatic 为我们准备的。我们来分析一下。我们的 Passport 本地策略默认名为"local" 。我们在 @UseGuards() 装饰器中引用这个名称,以便将它与护照本地包提供的代码关联起来。这用于消除在应用程序中有多个 Passport 策略时调用哪个策略的歧义(每个策略可能提供一个特定于策略的 AuthGuard )。虽然到目前为止我们只有一个这样的策略,但我们很快就会添加第二个,所以这是消除歧义所需要的。

为了测试我们的路由,我们将 /auth/login 路由简单地返回用户。这还允许我们演示另一个 Passport 特性: Passport 根据从 validate() 方法返回的值自动创建一个 user 对象,并将其作为 req.user 分配给请求对象。稍后,我们将用创建并返回 JWT 的代码替换它。

因为这些是 API 路由,所以我们将使用常用的cURL库来测试它们。您可以使用 UsersService 中硬编码的任何用户对象进行测试。

  1. $ # POST to /auth/login
  2. $ curl -X POST http://localhost:3000/auth/login -d '{"username": "john", "password": "changeme"}' -H "Content-Type: application/json"
  3. $ # result -> {"userId":1,"username":"john"}

JWT功能

我们已经准备好进入JWT部分的认证系统。让我们回顾并完善我们的需求:

  • 允许用户使用用户名/密码进行身份验证,返回 JWT 以便在后续调用受保护的 API 端点时使用。我们正在努力满足这一要求。为了完成它,我们需要编写发出 JWT 的代码。

  • 创建基于token 的有效JWT 的存在而受保护的API路由。

我们需要安装更多的包来支持我们的 JWT 需求:

  1. $ npm install @nestjs/jwt passport-jwt
  2. $ npm install @types/passport-jwt --save-dev

@nest/jwt 包是一个实用程序包,可以帮助 jwt 操作。passport-jwt 包是实现 JWT 策略的 Passport包,@types/passport-jwt 提供 TypeScript 类型定义。

让我们仔细看看如何处理 POST /auth/login 请求。我们使用护照本地策略提供的内置AuthGuard 来装饰路由。这意味着:

  1. 只有在验证了用户之后,才会调用路由处理程序

  2. req参数将包含一个用户属性(在passport-local 身份验证流期间由 Passport 填充)

考虑到这一点,我们现在终于可以生成一个真正的 JWT ,并以这种方式返回它。为了使我们的服务保持干净的模块化,我们将在 authService 中生成 JWT 。在auth文件夹中添加 auth.service.ts 文件,并添加 login() 方法,导入JwtService ,如下图所示:

auth/auth.service.ts

  1. import { Injectable } from '@nestjs/common';
  2. import { UsersService } from '../users/users.service';
  3. import { JwtService } from '@nestjs/jwt';
  4. @Injectable()
  5. export class AuthService {
  6. constructor(
  7. private readonly usersService: UsersService,
  8. private readonly jwtService: JwtService
  9. ) {}
  10. async validateUser(username: string, pass: string): Promise<any> {
  11. const user = await this.usersService.findOne(username);
  12. if (user && user.password === pass) {
  13. const { password, ...result } = user;
  14. return result;
  15. }
  16. return null;
  17. }
  18. async login(user: any) {
  19. const payload = { username: user.username, sub: user.userId };
  20. return {
  21. access_token: this.jwtService.sign(payload),
  22. };
  23. }
  24. }

我们使用 @nestjs/jwt 库,该库提供了一个 sign() 函数,用于从用户对象属性的子集生成 jwt,然后以简单对象的形式返回一个 access_token 属性。注意:我们选择 sub 的属性名来保持我们的 userId 值与JWT 标准一致。不要忘记将 JwtService 提供者注入到 AuthService中。

现在,我们需要更新 AuthModule 来导入新的依赖项并配置 JwtModule

首先,在auth文件夹下创建 auth/constants.ts,并添加以下代码:

auth/constants.ts

  1. export const jwtConstants = {
  2. secret: 'secretKey',
  3. };

我们将使用它在 JWT 签名和验证步骤之间共享密钥。

不要公开公开此密钥。我们在这里这样做是为了清楚地说明代码在做什么,但是在生产系统中,您必须使用适当的措施来保护这个密钥,比如机密库、环境变量或配置服务。

现在,在auth 文件夹下 auth.module.ts,并更新它看起来像这样:

  1. auth/auth.module.tsJS
  2. import { Module } from '@nestjs/common';
  3. import { AuthService } from './auth.service';
  4. import { LocalStrategy } from './local.strategy';
  5. import { UsersModule } from '../users/users.module';
  6. import { PassportModule } from '@nestjs/passport';
  7. import { JwtModule } from '@nestjs/jwt';
  8. import { jwtConstants } from './constants';
  9. @Module({
  10. imports: [
  11. UsersModule,
  12. PassportModule,
  13. JwtModule.register({
  14. secret: jwtConstants.secret,
  15. signOptions: { expiresIn: '60s' },
  16. }),
  17. ],
  18. providers: [AuthService, LocalStrategy],
  19. exports: [AuthService],
  20. })
  21. export class AuthModule {}

我们使用 register() 配置 JwtModule ,并传入一个配置对象。有关 Nest JwtModule 的更多信息请参见此处,有关可用配置选项的更多信息请参见此处

现在我们可以更新 /auth/login 路径来返回 JWT

app.controller.ts

  1. import { Controller, Request, Post, UseGuards } from '@nestjs/common';
  2. import { AuthGuard } from '@nestjs/passport';
  3. import { AuthService } from './auth/auth.service';
  4. @Controller()
  5. export class AppController {
  6. constructor(private readonly authService: AuthService) {}
  7. @UseGuards(AuthGuard('local'))
  8. @Post('auth/login')
  9. async login(@Request() req) {
  10. return this.authService.login(req.user);
  11. }
  12. }

让我们继续使用 cURL 测试我们的路由。您可以使用 UsersService 中硬编码的任何用户对象进行测试。

  1. $ # POST to /auth/login
  2. $ curl -X POST http://localhost:3000/auth/login -d '{"username": "john", "password": "changeme"}' -H "Content-Type: application/json"
  3. $ # result -> {"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}
  4. $ # Note: above JWT truncated

Implementing Passport JWT

我们现在可以处理我们的最终需求:通过要求在请求时提供有效的 JWT 来保护端点。护照对我们也有帮助。它提供了用于用 JSON Web 标记保护 RESTful 端点的 passport-jwt 策略。在 auth 文件夹中 jwt.strategy.ts,并添加以下代码:

auth/jwt.strategy.ts

  1. import { ExtractJwt, Strategy } from 'passport-jwt';
  2. import { PassportStrategy } from '@nestjs/passport';
  3. import { Injectable } from '@nestjs/common';
  4. import { jwtConstants } from './constants';
  5. @Injectable()
  6. export class JwtStrategy extends PassportStrategy(Strategy) {
  7. constructor() {
  8. super({
  9. jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
  10. ignoreExpiration: false,
  11. secretOrKey: jwtConstants.secret,
  12. });
  13. }
  14. async validate(payload: any) {
  15. return { userId: payload.sub, username: payload.username };
  16. }
  17. }

对于我们的 JwtStrategy ,我们遵循了前面描述的所有 Passport 策略的相同配方。这个策略需要一些初始化,因此我们通过在 super() 调用中传递一个 options 对象来实现。您可以在这里阅读关于可用选项的更多信息。在我们的例子中,这些选项是:

  • jwtFromRequest:提供从请求中提取 JWT 的方法。我们将使用在 API 请求的授权头中提供tokeen的标准方法。这里描述了其他选项。

ignoreExpiration:为了明确起见,我们选择默认的 false 设置,它将确保 JWT 没有过期的责任委托给 Passport 模块。这意味着,如果我们的路由提供了一个过期的 JWT ,请求将被拒绝,并发送 401 未经授权的响应。护照会自动为我们办理。

secret orkey:我们使用权宜的选项来提供对称的秘密来签署令牌。其他选项,如 pemo 编码的公钥,可能更适合于生产应用程序(有关更多信息,请参见此处)。如前所述,无论如何,不要把这个秘密公开。

validate() 方法值得讨论一下。对于 JWT 策略,Passport 首先验证 JWT 的签名并解码 JSON。然后调用我们的 validate() 方法,该方法将解码后的 JSON 作为其单个参数传递。根据 JWT 签名的工作方式,我们可以保证接收到之前已签名并发给有效用户的有效 token 令牌。

因此,我们对 validate() 回调的响应很简单:我们只是返回一个包含 userIdusername 属性的对象。再次回忆一下,Passport 将基于 validate() 方法的返回值构建一个user 对象,并将其作为属性附加到请求对象上。

同样值得指出的是,这种方法为我们留出了将其他业务逻辑注入流程的空间(就像”挂钩”一样)。例如,我们可以在 validate() 方法中执行数据库查询,以提取关于用户的更多信息,从而在请求中提供更丰富的用户对象。这也是我们决定进行进一步令牌验证的地方,例如在已撤销的令牌列表中查找 userId ,使我们能够执行令牌撤销。我们在示例代码中实现的模型是一个快速的 "无状态JWT" 模型,其中根据有效 JWT 的存在立即对每个 API 调用进行授权,并在请求管道中提供关于请求者(其 useridusername)的少量信息。

AuthModule 中添加新的 JwtStrategy 作为提供者:

auth/auth.module.ts

  1. import { Module } from '@nestjs/common';
  2. import { AuthService } from './auth.service';
  3. import { LocalStrategy } from './local.strategy';
  4. import { JwtStrategy } from './jwt.strategy';
  5. import { UsersModule } from '../users/users.module';
  6. import { PassportModule } from '@nestjs/passport';
  7. import { JwtModule } from '@nestjs/jwt';
  8. import { jwtConstants } from './constants';
  9. @Module({
  10. imports: [
  11. UsersModule,
  12. PassportModule,
  13. JwtModule.register({
  14. secret: jwtConstants.secret,
  15. signOptions: { expiresIn: '60s' },
  16. }),
  17. ],
  18. providers: [AuthService, LocalStrategy, JwtStrategy],
  19. exports: [AuthService],
  20. })
  21. export class AuthModule {}

通过导入 JWT 签名时使用的相同密钥,我们可以确保 Passport 执行的验证阶段和 AuthService 执行的签名阶段使用公共密钥。

实现受保护的路由和 JWT 策略保护,我们现在可以实现受保护的路由及其相关的保护。

打开 app.controller.ts 文件,更新如下:

app.controller.ts

  1. import { Controller, Get, Request, Post, UseGuards } from '@nestjs/common';
  2. import { AuthGuard } from '@nestjs/passport';
  3. import { AuthService } from './auth/auth.service';
  4. @Controller()
  5. export class AppController {
  6. constructor(private readonly authService: AuthService) {}
  7. @UseGuards(AuthGuard('local'))
  8. @Post('auth/login')
  9. async login(@Request() req) {
  10. return this.authService.login(req.user);
  11. }
  12. @UseGuards(AuthGuard('jwt'))
  13. @Get('profile')
  14. getProfile(@Request() req) {
  15. return req.user;
  16. }
  17. }

同样,我们将应用在配置 passport-jwt 模块时 @nestjs/passport 模块自动为我们提供的 AuthGuard 。这个保护由它的默认名称 jwt 引用。当我们请求GET /profile 路由时,保护程序将自动调用我们的 passport-jwt 自定义配置逻辑,验证 JWT ,并将用户属性分配给请求对象。

确保应用程序正在运行,并使用 cURL 测试路由。

  1. $ # GET /profile
  2. $ curl http://localhost:3000/profile
  3. $ # result -> {"statusCode":401,"error":"Unauthorized"}
  4. $ # POST /auth/login
  5. $ curl -X POST http://localhost:3000/auth/login -d '{"username": "john", "password": "changeme"}' -H "Content-Type: application/json"
  6. $ # result -> {"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm... }
  7. $ # GET /profile using access_token returned from previous step as bearer code
  8. $ curl http://localhost:3000/profile -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm..."
  9. $ # result -> {"userId":1,"username":"john"}

注意,在 AuthModule 中,我们将 JWT 配置为 60 秒过期。这个过期时间可能太短了,而处理令牌过期和刷新的细节超出了本文的范围。然而,我们选择它来展示JWT 的一个重要品质和 jwt 护照战略。如果您在验证之后等待 60 秒再尝试 GET /profile 请求,您将收到 401 未授权响应。这是因为 Passport 会自动检查 JWT 的过期时间,从而省去了在应用程序中这样做的麻烦。

我们现在已经完成了 JWT 身份验证实现。JavaScript 客户端(如 Angular/React/Vue )和其他 JavaScript 应用程序现在可以安全地与我们的 API 服务器进行身份验证和通信。

默认策略

在我们的 AppController 中,我们在 @AuthGuard() 装饰器中传递策略的名称。我们需要这样做,因为我们已经介绍了两种 Passport 策略(护照本地策略和护照 jwt 策略),这两种策略都提供了各种 Passport 组件的实现。传递名称可以消除我们链接到的实现的歧义。当应用程序中包含多个策略时,我们可以声明一个默认策略,这样如果使用该默认策略,我们就不必在 @AuthGuard 装饰器中传递名称。下面介绍如何在导入 PassportModule 时注册默认策略。这段代码将进入 AuthModule :

要确定默认策略行为,您可以注册 PassportModule

auth.module.ts

  1. import { Module } from '@nestjs/common';
  2. import { AuthService } from './auth.service';
  3. import { LocalStrategy } from './local.strategy';
  4. import { UsersModule } from '../users/users.module';
  5. import { PassportModule } from '@nestjs/passport';
  6. import { JwtModule } from '@nestjs/jwt';
  7. import { jwtConstants } from './constants';
  8. import { JwtStrategy } from './jwt.strategy';
  9. @Module({
  10. imports: [
  11. PassportModule.register({ defaultStrategy: 'jwt' }),
  12. JwtModule.register({
  13. secret: jwtConstants.secret,
  14. signOptions: { expiresIn: '60s' },
  15. }),
  16. UsersModule
  17. ],
  18. providers: [AuthService, LocalStrategy, JwtStrategy],
  19. exports: [AuthService],
  20. })
  21. export class AuthModule {}

自定义护照

根据所使用的策略,护照会采用一系列影响库行为的属性。使用 register() 方法将选项对象直接传递给护照实例。

  1. PassportModule.register({ session: true });

您还可以在策略的构造函数中传递一个 options 对象来配置它们。至于本地策略,你可以通过例如:

  1. constructor(private readonly authService: AuthService) {
  2. super({
  3. usernameField: 'email',
  4. passwordField: 'password',
  5. });
  6. }

看看Passport Website官方文档吧。

命名策略

在实现策略时,可以通过向 PassportStrategy 函数传递第二个参数来为其提供名称。如果你不这样做,每个战略将有一个默认的名称(例如,”jwt”的 jwt策略 ):

  1. export class JwtStrategy extends PassportStrategy(Strategy, 'myjwt')

然后,通过一个像 @AuthGuard('myjwt') 这样的装饰器来引用它。

GraphQL

为了使用带有 GraphQLAuthGuard ,扩展内置的 AuthGuard 类并覆盖 getRequest() 方法。

  1. @Injectable()
  2. export class GqlAuthGuard extends AuthGuard('jwt') {
  3. getRequest(context: ExecutionContext) {
  4. const ctx = GqlExecutionContext.create(context);
  5. return ctx.getContext().req;
  6. }
  7. }

要使用上述结构,请确保在 GraphQL 模块设置中将 request (req)对象作为上下文值的一部分传递:

  1. GraphQLModule.forRoot({
  2. context: ({ req }) => ({ req }),
  3. });

要在 graphql 解析器中获得当前经过身份验证的用户,可以定义一个用户装饰器:

  1. import { createParamDecorator } from '@nestjs/common';
  2. export const CurrentUser = createParamDecorator(
  3. (data, [root, args, ctx, info]) => ctx.req.user,
  4. );

要在解析器中使用上述装饰器,请确保将其作为查询的参数:

  1. @Query(returns => User)
  2. @UseGuards(GqlAuthGuard)
  3. whoAmI(@CurrentUser() user: User) {
  4. return this.userService.findById(user.id);
  5. }

数据库

Nest 与数据库无关,允许您轻松地与任何 SQLNoSQL 数据库集成。根据您的偏好,您有许多可用的选项。一般来说,将 Nest 连接到数据库只需为数据库加载一个适当的 Node.js 驱动程序,就像使用 ExpressFastify 一样。

您还可以直接使用任何通用的 Node.js 数据库集成库或 ORM ,例如 Sequelize (recipe)knexjs (tutorial)`和 TypeORM ,以在更高的抽象级别上进行操作。

为了方便起见,Nest 还提供了与现成的 TypeORM@nestjs/typeorm 的紧密集成,我们将在本章中对此进行介绍,而与 @nestjs/mongoose 的紧密集成将在本章中介绍。这些集成提供了附加的特定于 nestjs 的特性,比如模型/存储库注入、可测试性和异步配置,从而使访问您选择的数据库更加容易。

TypeORM 集成

为了与 SQLNoSQL 数据库集成,Nest 提供了 @nestjs/typeorm 包。Nest 使用TypeORM是因为它是 TypeScript 中最成熟的对象关系映射器( ORM )。因为它是用 TypeScript 编写的,所以可以很好地与 Nest 框架集成。

为了开始使用它,我们首先安装所需的依赖项。在本章中,我们将演示如何使用流行的 MysqlTypeORM 提供了对许多关系数据库的支持,比如 PostgreSQLOracleMicrosoft SQL ServerSQLite,甚至像 MongoDB这样的 NoSQL 数据库。我们在本章中介绍的过程对于 TypeORM 支持的任何数据库都是相同的。您只需为所选数据库安装相关的客户端 API 库。

  1. $ npm install --save @nestjs/typeorm typeorm mysql

安装过程完成后,我们可以将 TypeOrmModule 导入AppModule

app.module.ts

  1. import { Module } from '@nestjs/common';
  2. import { TypeOrmModule } from '@nestjs/typeorm';
  3. @Module({
  4. imports: [
  5. TypeOrmModule.forRoot({
  6. type: 'mysql',
  7. host: 'localhost',
  8. port: 3306,
  9. username: 'root',
  10. password: 'root',
  11. database: 'test',
  12. entities: [],
  13. synchronize: true,
  14. }),
  15. ],
  16. })
  17. export class AppModule {}

forRoot() 方法接受与来自 TypeORM包的 createConnection() 相同的配置对象。另外,我们可以创建 ormconfig.json ,而不是将配置对象传递给 forRoot()

  1. {
  2. "type": "mysql",
  3. "host": "localhost",
  4. "port": 3306,
  5. "username": "root",
  6. "password": "root",
  7. "database": "test",
  8. "entities": ["dist/**/*.entity{.ts,.js}"],
  9. "synchronize": true
  10. }

静态全局路径(例如 dist/**/*.entity{ .ts,.js} )不适用于webpack。

然后,我们可以调用 forRoot() 没有任何选项:

app.module.ts

  1. import { Module } from '@nestjs/common';
  2. import { TypeOrmModule } from '@nestjs/typeorm';
  3. @Module({
  4. imports: [TypeOrmModule.forRoot()],
  5. })
  6. export class AppModule {}

一旦完成,TypeORM 连接和 EntityManager 对象就可以在整个项目中注入(不需要导入任何模块),例如:

app.module.ts

  1. import { Connection } from 'typeorm';
  2. @Module({
  3. imports: [TypeOrmModule.forRoot(), PhotoModule],
  4. })
  5. export class AppModule {
  6. constructor(private readonly connection: Connection) {}
  7. }

存储库模式

TypeORM 支持存储库设计模式,因此每个实体都有自己的存储库。可以从数据库连接获得这些存储库。

为了继续这个示例,我们需要至少一个实体。我们将使用官方TypeORM文档中的 Photo 实体。

photo/photo.entity.ts

  1. import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
  2. @Entity()
  3. export class Photo {
  4. @PrimaryGeneratedColumn()
  5. id: number;
  6. @Column({ length: 500 })
  7. name: string;
  8. @Column('text')
  9. description: string;
  10. @Column()
  11. filename: string;
  12. @Column('int')
  13. views: number;
  14. @Column()
  15. isPublished: boolean;
  16. }

Photo 实体属于该 photo 目录。这个目录代表了 PhotoModule。这是你决定在哪里保留你的模型文件。从我的观点来看,最好的方法是将它们放在他们的域中, 放在相应的模块目录中。

开始使用 photo 实体,我们需要让 TypeORM 知道它插入实体数组:

app.module.ts

  1. app.module.tsJS
  2. import { Module } from '@nestjs/common';
  3. import { TypeOrmModule } from '@nestjs/typeorm';
  4. import { Photo } from './photo/photo.entity';
  5. @Module({
  6. imports: [
  7. TypeOrmModule.forRoot({
  8. type: 'mysql',
  9. host: 'localhost',
  10. port: 3306,
  11. username: 'root',
  12. password: 'root',
  13. database: 'test',
  14. entities: [Photo],
  15. synchronize: true,
  16. }),
  17. ],
  18. })
  19. export class AppModule {}

现在让我们看一下 PhotoModule

photo.module.ts

  1. import { Module } from '@nestjs/common';
  2. import { TypeOrmModule } from '@nestjs/typeorm';
  3. import { PhotoService } from './photo.service';
  4. import { PhotoController } from './photo.controller';
  5. import { Photo } from './photo.entity';
  6. @Module({
  7. imports: [TypeOrmModule.forFeature([Photo])],
  8. providers: [PhotoService],
  9. controllers: [PhotoController],
  10. })
  11. export class PhotoModule {}

此模块使用 forFeature() 方法定义在当前范围中注册哪些存储库。这样,我们就可以使用 @InjectRepository()装饰器将 PhotoRepository 注入到 PhotoService 中:

photo.service.ts

  1. import { Injectable } from '@nestjs/common';
  2. import { InjectRepository } from '@nestjs/typeorm';
  3. import { Repository } from 'typeorm';
  4. import { Photo } from './photo.entity';
  5. @Injectable()
  6. export class PhotoService {
  7. constructor(
  8. @InjectRepository(Photo)
  9. private readonly photoRepository: Repository<Photo>,
  10. ) {}
  11. findAll(): Promise<Photo[]> {
  12. return this.photoRepository.find();
  13. }
  14. }

不要忘记将 PhotoModule 导入根 ApplicationModule

如果要在导入TypeOrmModule.forFeature 的模块之外使用存储库,则需要重新导出由其生成的提供程序。 您可以通过导出整个模块来做到这一点,如下所示:

photo.module.ts

  1. import { Module } from '@nestjs/common';
  2. import { TypeOrmModule } from '@nestjs/typeorm';
  3. import { Photo } from './photo.entity';
  4. @Module({
  5. imports: [TypeOrmModule.forFeature([Photo])],
  6. exports: [TypeOrmModule]
  7. })
  8. export class PhotoModule {}

现在,如果我们在 PhotoHttpModule 中导入 PhotoModule ,我们可以在后一个模块的提供者中使用 @InjectRepository(Photo)

photo-http.module.ts

  1. import { Module } from '@nestjs/common';
  2. import { PhotoModule } from './photo.module';
  3. import { PhotoService } from './photo.service';
  4. import { PhotoController } from './photo.controller';
  5. @Module({
  6. imports: [PhotoModule],
  7. providers: [PhotoService],
  8. controllers: [PhotoController]
  9. })
  10. export class PhotoHttpModule {}

多个数据库

某些项目可能需要多个数据库连接。幸运的是,这也可以通过本模块实现。要使用多个连接,首先要做的是创建这些连接。在这种情况下,连接命名成为必填项。

假设你有一个 Person 实体和一个 Album 实体,每个实体都存储在他们自己的数据库中。

  1. const defaultOptions = {
  2. type: 'postgres',
  3. port: 5432,
  4. username: 'user',
  5. password: 'password',
  6. database: 'db',
  7. synchronize: true,
  8. };
  9. @Module({
  10. imports: [
  11. TypeOrmModule.forRoot({
  12. ...defaultOptions,
  13. host: 'photo_db_host',
  14. entities: [Photo],
  15. }),
  16. TypeOrmModule.forRoot({
  17. ...defaultOptions,
  18. name: 'personsConnection',
  19. host: 'person_db_host',
  20. entities: [Person],
  21. }),
  22. TypeOrmModule.forRoot({
  23. ...defaultOptions,
  24. name: 'albumsConnection',
  25. host: 'album_db_host',
  26. entities: [Album],
  27. }),
  28. ],
  29. })
  30. export class AppModule {}

如果未为连接设置任何 name ,则该连接的名称将设置为 default。请注意,不应该有多个没有名称或同名的连接,否则它们会被覆盖。

此时,您的 PhotoPersonAlbum 实体中的每一个都已在各自的连接中注册。通过此设置,您必须告诉 TypeOrmModule.forFeature() 函数和 @InjectRepository() 装饰器应该使用哪种连接。如果不传递任何连接名称,则使用 default 连接。

  1. @Module({
  2. imports: [
  3. TypeOrmModule.forFeature([Photo]),
  4. TypeOrmModule.forFeature([Person], 'personsConnection'),
  5. TypeOrmModule.forFeature([Album], 'albumsConnection'),
  6. ],
  7. })
  8. export class AppModule {}

您也可以为给定的连接注入 ConnectionEntityManager

  1. @Injectable()
  2. export class PersonService {
  3. constructor(
  4. @InjectConnection('personsConnection')
  5. private readonly connection: Connection,
  6. @InjectEntityManager('personsConnection')
  7. private readonly entityManager: EntityManager,
  8. ) {}
  9. }

测试

在单元测试我们的应用程序时,我们通常希望避免任何数据库连接,从而使我们的测试适合于独立,并使它们的执行过程尽可能快。但是我们的类可能依赖于从连接实例中提取的存储库。那是什么?解决方案是创建假存储库。为了实现这一点,我们设置了自定义提供者。事实上,每个注册的存储库都由 entitynamereposition 标记表示,其中 EntityName 是实体类的名称。

@nestjs/typeorm 包提供了基于给定实体返回准备好 tokengetRepositoryToken() 函数。

  1. @Module({
  2. providers: [
  3. PhotoService,
  4. {
  5. provide: getRepositoryToken(Photo),
  6. useValue: mockRepository,
  7. },
  8. ],
  9. })
  10. export class PhotoModule {}

现在, 将使用硬编码 mockRepository 作为 PhotoRepository。每当任何提供程序使用 @InjectRepository() 装饰器请求 PhotoRepository 时, Nest 会使用注册的 mockRepository 对象。

定制存储库

TypeORM 提供称为自定义存储库的功能。要了解有关它的更多信息,请访问此页面。基本上,自定义存储库允许您扩展基本存储库类,并使用几种特殊方法对其进行丰富。

要创建自定义存储库,请使用 @EntityRepository() 装饰器和扩展 Repository 类。

  1. @EntityRepository(Author)
  2. export class AuthorRepository extends Repository<Author> {}

@EntityRepository()Repository 来自 typeorm 包。

创建类后,下一步是将实例化责任移交给 Nest。为此,我们必须将 AuthorRepository 类传递给 TypeOrm.forFeature() 函数。

  1. @Module({
  2. imports: [TypeOrmModule.forFeature([AuthorRepository])],
  3. controller: [AuthorController],
  4. providers: [AuthorService],
  5. })
  6. export class AuthorModule {}

之后,只需使用以下构造注入存储库:

  1. @Injectable()
  2. export class AuthorService {
  3. constructor(private readonly authorRepository: AuthorRepository) {}
  4. }

异步配置

通常,您可能希望异步传递模块选项,而不是事先传递它们。在这种情况下,使用 forRootAsync() 函数,提供了几种处理异步数据的方法。

第一种可能的方法是使用工厂函数:

  1. TypeOrmModule.forRootAsync({
  2. useFactory: () => ({
  3. type: 'mysql',
  4. host: 'localhost',
  5. port: 3306,
  6. username: 'root',
  7. password: 'root',
  8. database: 'test',
  9. entities: [__dirname + '/**/*.entity{.ts,.js}'],
  10. synchronize: true,
  11. }),
  12. });

我们的工厂的行为与任何其他异步提供者一样(例如,它可以是异步的,并且它能够通过注入注入依赖)。

  1. TypeOrmModule.forRootAsync({
  2. imports: [ConfigModule],
  3. useFactory: async (configService: ConfigService) => ({
  4. type: 'mysql',
  5. host: configService.getString('HOST'),
  6. port: configService.getString('PORT'),
  7. username: configService.getString('USERNAME'),
  8. password: configService.getString('PASSWORD'),
  9. database: configService.getString('DATABASE'),
  10. entities: [__dirname + '/**/*.entity{.ts,.js}'],
  11. synchronize: true,
  12. }),
  13. inject: [ConfigService],
  14. });

或者,您可以使用类而不是工厂。

  1. TypeOrmModule.forRootAsync({
  2. useClass: TypeOrmConfigService,
  3. });

上面的构造将 TypeOrmConfigService 在内部进行实例化 TypeOrmModule,并将利用它来创建选项对象。在 TypeOrmConfigService 必须实现 TypeOrmOptionsFactory 的接口。

  1. @Injectable()
  2. class TypeOrmConfigService implements TypeOrmOptionsFactory {
  3. createTypeOrmOptions(): TypeOrmModuleOptions {
  4. return {
  5. type: 'mysql',
  6. host: 'localhost',
  7. port: 3306,
  8. username: 'root',
  9. password: 'root',
  10. database: 'test',
  11. entities: [__dirname + '/**/*.entity{.ts,.js}'],
  12. synchronize: true,
  13. };
  14. }
  15. }

为了防止在 TypeOrmModule 中创建 TypeOrmConfigService 并使用从不同模块导入的提供程序,可以使用 useExisting 语法。

  1. TypeOrmModule.forRootAsync({
  2. imports: [ConfigModule],
  3. useExisting: ConfigService,
  4. });

这个构造与 useClass 的工作原理相同,但有一个关键的区别 — TypeOrmModule 将查找导入的模块来重用现有的 ConfigService,而不是实例化一个新的 ConfigService

示例

这儿有一个可用的例子。

Mongo

Nest支持两种与 MongoDB 数据库集成的方式。既使用ORM 提供的 MongoDB 支撑或对象建模工具 Mongoose。选择 ORM 的话你可以按照以前的步骤使用 typeorm 。否则请使用我们 Nest 专用包: @nestjs/mongoose

首先,我们需要安装所有必需的依赖项:

  1. $ npm install --save @nestjs/mongoose mongoose

安装过程完成后,我们可以将其 MongooseModule 导入到根目录中 ApplicationModule

app.module.ts

  1. import { Module } from '@nestjs/common';
  2. import { MongooseModule } from '@nestjs/mongoose';
  3. @Module({
  4. imports: [MongooseModule.forRoot('mongodb://localhost/nest')],
  5. })
  6. export class AppModule {}

forRoot()mongoose 包中的 mongoose.connect() 一样的参数对象。如下所述。

模型注入

cat.schema.ts 文件驻在 cats 目录中的一个文件夹中,我们还在其中定义了 CatsModule。虽然您可以将模式文件存储在您喜欢的任何地方,但是我们建议将它们存储在相关的域对象附近的适当模块目录中。

让我们来看看:

cats.module.ts

  1. import { Module } from '@nestjs/common';
  2. import { MongooseModule } from '@nestjs/mongoose';
  3. import { CatsController } from './cats.controller';
  4. import { CatsService } from './cats.service';
  5. import { CatSchema } from './schemas/cat.schema';
  6. @Module({
  7. imports: [MongooseModule.forFeature([{ name: 'Cat', schema: CatSchema }])],
  8. controllers: [CatsController],
  9. providers: [CatsService],
  10. })
  11. export class CatsModule {}

MongooseModule 提供了 forFeature() 方法来配置模块,包括定义应该在当前范围内注册哪些模型。如果您还想在另一个模块中使用模型,请将 MongooseModule 添加到 CatsModule 的导出部分,并在另一个模块中导入CatsModule

注册模式后,可以使用 @InjectModel() 装饰器将 Cat 模型注入到 CatsService 中:

cats.service.ts

  1. import { Model } from 'mongoose';
  2. import { Injectable } from '@nestjs/common';
  3. import { InjectModel } from '@nestjs/mongoose';
  4. import { Cat } from './interfaces/cat.interface';
  5. import { CreateCatDto } from './dto/create-cat.dto';
  6. @Injectable()
  7. export class CatsService {
  8. constructor(@InjectModel('Cat') private readonly catModel: Model<Cat>) {}
  9. async create(createCatDto: CreateCatDto): Promise<Cat> {
  10. const createdCat = new this.catModel(createCatDto);
  11. return await createdCat.save();
  12. }
  13. async findAll(): Promise<Cat[]> {
  14. return await this.catModel.find().exec();
  15. }
  16. }

测试’

在单元测试我们的应用程序时,我们通常希望避免任何数据库连接,使我们的测试套件独立并尽可能快地执行它们。但是我们的类可能依赖于从连接实例中提取的模型。

为了简化这一过程,@nestjs/mongoose 包公开了一个 getModelToken() 函数,该函数根据一个 token 名称返回一个准备好的注入token。使用此 token,您可以轻松地使用任何标准自定义提供程序技术(包括 useClassuseValueuseFactory)提供模拟实现。例如:

  1. @@Module({
  2. providers: [
  3. CatsService,
  4. {
  5. provide: getModelToken('Cat'),
  6. useValue: catModel,
  7. },
  8. ],
  9. })
  10. export class CatsModule {}

在本例中,每当任何使用者使用 @InjectModel() 装饰器注入模型时,都会提供一个硬编码的 Model<Cat> (对象实例)。

异步配置

通常,您可能希望异步传递模块选项,而不是事先传递它们。在这种情况下,使用 forRootAsync() 方法,提供了几种处理异步数据的方法。

第一种可能的方法是使用工厂函数:

  1. MongooseModule.forRootAsync({
  2. useFactory: () => ({
  3. uri: 'mongodb://localhost/nest',
  4. }),
  5. });

与其他工厂提供程序一样,我们的工厂函数可以是异步的,并且可以通过注入注入依赖。

  1. MongooseModule.forRootAsync({
  2. imports: [ConfigModule],
  3. useFactory: async (configService: ConfigService) => ({
  4. uri: configService.getString('MONGODB_URI'),
  5. }),
  6. inject: [ConfigService],
  7. });

或者,您可以使用类而不是工厂来配置 MongooseModule,如下所示:

  1. MongooseModule.forRootAsync({
  2. useClass: MongooseConfigService,
  3. });

上面的构造在 MongooseModule中实例化了 MongooseConfigService,使用它来创建所需的 options 对象。注意,在本例中,MongooseConfigService 必须实现 MongooseOptionsFactory 接口,如下所示。 MongooseModule 将在提供的类的实例化对象上调用 createMongooseOptions() 方法。

  1. @Injectable()
  2. class MongooseConfigService implements MongooseOptionsFactory {
  3. createMongooseOptions(): MongooseModuleOptions {
  4. return {
  5. uri: 'mongodb://localhost/nest',
  6. };
  7. }
  8. }

为了防止 MongooseConfigService 内部创建 MongooseModule 并使用从不同模块导入的提供程序,您可以使用 useExisting 语法。

  1. MongooseModule.forRootAsync({
  2. imports: [ConfigModule],
  3. useExisting: ConfigService,
  4. });

文件上传

为了处理文件上传,Nest 提供了一个内置的基于multer中间件包的 Express模块。Multer 处理以 multipart/form-data 格式发送的数据,该格式主要用于通过 HTTP POST 请求上传文件。这个模块是完全可配置的,您可以根据您的应用程序需求调整它的行为。

Multer无法处理不是受支持的多部分格式(multipart/form-data)的数据。 另外,请注意此程序包与 FastifyAdapter不兼容。

基本实例

当我们要上传单个文件时, 我们只需将 FileInterceptor () 与处理程序绑定在一起, 然后使用 @UploadedFile () 装饰器从 request 中取出 file

  1. @Post('upload')
  2. @UseInterceptors(FileInterceptor('file'))
  3. uploadFile(@UploadedFile() file) {
  4. console.log(file);
  5. }

FileInterceptor() 装饰器是 @nestjs/platform-express 包提供的, @UploadedFile() 装饰是 @nestjs/common 包提供的。

FileInterceptor() 接收两个参数:

  • 一个 fieldName (指向包含文件的 HTML 表单的字段)

  • 可选 options 对象。这些 MulterOptions 等效于传入 multer 构造函数 (此处有更多详细信息)

文件数组

为了上传文件数组,我们使用 FilesInterceptor()。请使用 FilesInterceptor() 装饰器(注意装饰器名称中的复数文件)。这个装饰器有三个参数:

  • fieldName:(保持不变)

  • maxCount:可选的数字,定义要接受的最大文件数

  • options:可选的 MulterOptions 对象 ,如上所述

使用 FilesInterceptor() 时,使用 @UploadedFiles() 装饰器从请求中提取文件。

  1. @Post('upload')
  2. @UseInterceptors(FilesInterceptor('files'))
  3. uploadFile(@UploadedFiles() files) {
  4. console.log(files);
  5. }

FilesInterceptor() 装饰器需要导入 @nestjs/platform-express,而 @UploadedFiles() 导入 @nestjs/common

多个文件

要上传多个文件(全部使用不同的键),请使用 FileFieldsInterceptor() 装饰器。这个装饰器有两个参数:

  • uploadedFields:对象数组,其中每个对象指定一个必需的 name 属性和一个指定字段名的字符串值(如上所述),以及一个可选的 maxCount 属性(如上所述)

  • options:可选的 MulterOptions 对象,如上所述

使用 FileFieldsInterceptor() 时,使用 @UploadedFiles() 装饰器从 request 中提取文件。

  1. @Post('upload')
  2. @UseInterceptors(FileFieldsInterceptor([
  3. { name: 'avatar', maxCount: 1 },
  4. { name: 'background', maxCount: 1 },
  5. ]))
  6. uploadFile(@UploadedFiles() files) {
  7. console.log(files);
  8. }

任何文件

要使用任意字段名称键上载所有字段,请使用 AnyFilesInterceptor() 装饰器。该装饰器可以接受如上所述的可选选项对象。

使用 FileFieldsInterceptor() 时,使用 @UploadedFiles() 装饰器从 request 中提取文件。

  1. @Post('upload')
  2. @UseInterceptors(AnyFilesInterceptor())
  3. uploadFile(@UploadedFiles() files) {
  4. console.log(files);
  5. }

默认选项

您可以像上面描述的那样在文件拦截器中指定 multer 选项。要设置默认选项,可以在导入 MulterModule 时调用静态 register() 方法,传入受支持的选项。您可以使用这里列出的所有选项。

  1. MulterModule.register({
  2. dest: '/upload',
  3. });

异步配置

当需要异步而不是静态地设置 MulterModule 选项时,请使用 registerAsync() 方法。与大多数动态模块一样,Nest 提供了一些处理异步配置的技术。

第一种可能的方法是使用工厂函数:

  1. MulterModule.registerAsync({
  2. useFactory: () => ({
  3. dest: '/upload',
  4. }),
  5. });

与其他工厂提供程序一样,我们的工厂函数可以是异步的,并且可以通过注入注入依赖。

  1. MulterModule.registerAsync({
  2. imports: [ConfigModule],
  3. useFactory: async (configService: ConfigService) => ({
  4. dest: configService.getString('MULTER_DEST'),
  5. }),
  6. inject: [ConfigService],
  7. });

或者,您可以使用类而不是工厂来配置 MulterModule,如下所示:

  1. MulterModule.registerAsync({
  2. useClass: MulterConfigService,
  3. });

上面的构造在 MulterModule 中实例化 MulterConfigService ,使用它来创建所需的 options 对象。注意,在本例中,MulterConfigService 必须实现 MulterOptionsFactory 接口,如下所示。MulterModule 将在提供的类的实例化对象上调用 createMulterOptions() 方法。

  1. @Injectable()
  2. class MulterConfigService implements MulterOptionsFactory {
  3. createMulterOptions(): MulterModuleOptions {
  4. return {
  5. dest: '/upload',
  6. };
  7. }
  8. }

为了防止创建 MulterConfigService 内部 MulterModule 并使用从不同模块导入的提供程序,您可以使用 useExisting 语法。

  1. MulterModule.registerAsync({
  2. imports: [ConfigModule],
  3. useExisting: ConfigService,
  4. });

验证

验证是任何现有 Web 应用程序的基本功能。为了自动验证传入请求,Nest 提供了一个内置的 ValidationPipe ,它使用了功能强大的class-validator包及其声明性验证装饰器。 ValidationPipe 提供了一种对所有传入的客户端有效负载强制执行验证规则的便捷方法,其中在每个模块的本地类/ DTO 声明中使用简单的注释声明特定的规则。

概览

Pipes 一章中,我们完成了构建简化验证管道的过程。为了更好地了解我们在幕后所做的工作,我们强烈建议您阅读本文。在这里,我们将重点讨论 ValidationPipe 的各种实际用例,并使用它的一些高级定制特性。

自动验证

为了本教程的目的,我们将绑定 ValidationPipe 到整个应用程序,因此,将自动保护所有接口免受不正确的数据的影响。

  1. async function bootstrap() {
  2. const app = await NestFactory.create(ApplicationModule);
  3. app.useGlobalPipes(new ValidationPipe());
  4. await app.listen(3000);
  5. }
  6. bootstrap();

要测试我们的管道,让我们创建一个基本接口。

  1. @Post()
  2. create(@Body() createUserDto: CreateUserDto) {
  3. return 'This action adds a new user';
  4. }

现在我们可以在 CreateUserDto 中添加一些验证规则。我们使用 class-validator 包提供的装饰器来实现这一点,这里有详细的描述。以这种方式,任何使用 CreateUserDto 的路由都将自动执行这些验证规则。

  1. import { IsEmail, IsNotEmpty } from 'class-validator';
  2. export class CreateUserDto {
  3. @IsEmail()
  4. email: string;
  5. @IsNotEmpty()
  6. password: string;
  7. }

有了这些规则,当某人使用无效 email 执行对我们的接口的请求时,则应用程序将自动以 400 Bad Request 代码以及以下响应正文进行响应:

  1. {
  2. "statusCode": 400,
  3. "error": "Bad Request",
  4. "message": [
  5. {
  6. "target": {},
  7. "property": "email",
  8. "children": [],
  9. "constraints": {
  10. "isEmail": "email must be an email"
  11. }
  12. }
  13. ]
  14. }

除了验证请求主体之外,ValidationPipe 还可以与其他请求对象属性一起使用。假设我们希望接受端点路径中的 id 。为了确保此请求参数只接受数字,我们可以使用以下结构:

  1. @Get(':id')
  2. findOne(@Param() params: FindOneParams) {
  3. return 'This action returns a user';
  4. }

DTO 一样,FindOneParams 只是一个使用 class-validator 定义验证规则的类。它是这样的:

  1. import { IsNumberString } from 'class-validator';
  2. export class FindOneParams {
  3. @IsNumberString()
  4. id: number;
  5. }

禁用详细错误

错误消息有助于解释请求中的错误。然而,一些生产环境倾向于禁用详细的错误。通过向 ValidationPipe 传递一个 options 对象来做到这一点:

  1. app.useGlobalPipes(
  2. new ValidationPipe({
  3. disableErrorMessages: true,
  4. })
  5. );

现在,不会将错误消息返回给最终用户。

剥离属性

我们的 ValidationPipe 还可以过滤掉方法处理程序不应该接收的属性。在这种情况下,我们可以对可接受的属性进行白名单,白名单中不包含的任何属性都会自动从结果对象中删除。例如,如果我们的处理程序需要 emailpassword,但是一个请求还包含一个 age 属性,那么这个属性可以从结果 DTO 中自动删除。要启用这种行为,请将白名单设置为 true

  1. app.useGlobalPipes(
  2. new ValidationPipe({
  3. whitelist: true,
  4. })
  5. );

当设置为 true 时,这将自动删除非白名单属性(在验证类中没有任何修饰符的属性)。

或者,您可以在出现非白名单属性时停止处理请求,并向用户返回错误响应。要启用此选项,请将 forbidNonWhitelisted 选项属性设置为 true ,并将白名单设置为 true

自动有效负载转换

来自网络的有效负载是普通的 JavaScript 对象。ValidationPipe 可以根据对象的 DTO 类自动将有效负载转换为对象类型。若要启用自动转换,请将转换设置为 true

  1. app.useGlobalPipes(
  2. new ValidationPipe({
  3. transform: true,
  4. })
  5. );

Websockets和微服务

尽管本章展示了使用 HTTP 风格的应用程序的例子(例如,ExpressFastify ), ValidationPipe 对于 WebSockets 和微服务是一样的,不管使用什么传输方法。

学到更多

要阅读有关自定义验证器,错误消息和可用装饰器的更多信息,请访问此页面

高速缓存(Caching)

缓存是一项伟大而简单的技术,可以帮助提高应用程序的性能。它充当临时数据存储,提供高性能的数据访问。

安装

我们首先需要安装所需的包:

  1. $ npm install --save cache-manager

内存缓存

译者注:查看相关使用方法

Nest为各种缓存存储提供程序提供了统一的 API。内置的是内存中的数据存储。但是,您可以轻松地切换到更全面的解决方案,比如 Redis 。为了启用缓存,首先导入 CacheModule 并调用它的 register() 方法。

  1. import { CacheModule, Module } from '@nestjs/common';
  2. import { AppController } from './app.controller';
  3. @Module({
  4. imports: [CacheModule.register()],
  5. controllers: [AppController],
  6. })
  7. export class ApplicationModule {}

然后将 CacheInterceptor 绑定到需要缓存数据的地方。

  1. @Controller()
  2. @UseInterceptors(CacheInterceptor)
  3. export class AppController {
  4. @Get()
  5. findAll(): string[] {
  6. return [];
  7. }
  8. }

警告: 只有使用 @Get() 方式声明的节点会被缓存。此外,注入本机响应对象( @Res() )的 HTTP 服务器路由不能使用缓存拦截器。有关详细信息,请参见响应映射。

全局缓存

为了减少重复代码量,可以一次绑定 CacheInterceptor 到每个现有节点:

  1. import { CacheModule, Module, CacheInterceptor } from '@nestjs/common';
  2. import { AppController } from './app.controller';
  3. import { APP_INTERCEPTOR } from '@nestjs/core';
  4. @Module({
  5. imports: [CacheModule.register()],
  6. controllers: [AppController],
  7. providers: [
  8. {
  9. provide: APP_INTERCEPTOR,
  10. useClass: CacheInterceptor,
  11. },
  12. ],
  13. })
  14. export class ApplicationModule {}

WebSockets和微服务

显然,您可以毫不费力地使用 CacheInterceptor WebSocket 订阅者模式以及 Microservice 的模式(无论使用何种服务间的传输方法)。

译者注: 微服务架构中服务之间的调用需要依赖某种通讯协议介质,在 nest 中不限制你是用消息队列中间价,RPC/gRPC 协议或者对外公开 APIHTTP 协议。

  1. @CacheKey('events')
  2. @UseInterceptors(CacheInterceptor)
  3. @SubscribeMessage('events')
  4. handleEvent(client: Client, data: string[]): Observable<string[]> {
  5. return [];
  6. }

提示: @CacheKey() 装饰器来源于 @nestjs/common 包。

但是, @CacheKey() 需要附加装饰器以指定用于随后存储和检索缓存数据的密钥。此外,请注意,开发者不应该缓存所有内容。缓存数据是用来执行某些业务操作,而一些简单数据查询是不应该被缓存的。

自定义缓存

所有缓存数据都有自己的到期时间(TTL)。要自定义默认值,请将配置选项填写在 register()方法中。

  1. CacheModule.register({
  2. ttl: 5, // seconds
  3. max: 10, // maximum number of items in cache
  4. });

不同的缓存库

我们充分利用了缓存管理器。该软件包支持各种实用的商店,例如Redis商店(此处列出完整列表)。要设置 Redis 存储,只需将包与 correspoding 选项一起传递给 register() 方法即可。

译者注: 缓存方案库目前可选的有 redis, fs, mongodb, memcached 等。

  1. import * as redisStore from 'cache-manager-redis-store';
  2. import { CacheModule, Module } from '@nestjs/common';
  3. import { AppController } from './app.controller';
  4. @Module({
  5. imports: [
  6. CacheModule.register({
  7. store: redisStore,
  8. host: 'localhost',
  9. port: 6379,
  10. }),
  11. ],
  12. controllers: [AppController],
  13. })
  14. export class ApplicationModule {}

调整跟踪

默认情况下, Nest 通过 @CacheKey() 装饰器设置的请求路径(在 HTTP 应用程序中)或缓存中的 key(在 websockets 和微服务中)来缓存记录与您的节点数据相关联。然而有时您可能希望根据不同因素设置跟踪,例如,使用 HTTP 头部字段(例如 Authorization 字段关联身份鉴别节点服务)。

为此,创建 CacheInterceptor 的子类并覆盖 trackBy() 方法。

  1. @Injectable()
  2. class HttpCacheInterceptor extends CacheInterceptor {
  3. trackBy(context: ExecutionContext): string | undefined {
  4. return 'key';
  5. }
  6. }

异步配置

通常,您可能希望异步传递模块选项,而不是事先传递它们。在这种情况下,使用 registerAsync() 方法,提供了几种处理异步数据的方法。

第一种可能的方法是使用工厂函数:

  1. CacheModule.registerAsync({
  2. useFactory: () => ({
  3. ttl: 5,
  4. }),
  5. });

显然,我们的工厂要看起来能让每一个调用用使用。(可以变成顺序执行的同步代码,并且能够通过注入依赖使用)。

  1. CacheModule.registerAsync({
  2. imports: [ConfigModule],
  3. useFactory: async (configService: ConfigService) => ({
  4. ttl: configService.getString('CACHE_TTL'),
  5. }),
  6. inject: [ConfigService],
  7. });

或者,您可以使用类而不是工厂:

  1. CacheModule.registerAsync({
  2. useClass: CacheConfigService,
  3. });

上面的构造将 CacheConfigService 在内部实例化为 CacheModule ,并将利用它来创建选项对象。在 CacheConfigService 中必须实现 CacheOptionsFactory 的接口。

  1. @Injectable()
  2. class CacheConfigService implements CacheOptionsFactory {
  3. createCacheOptions(): CacheModuleOptions {
  4. return {
  5. ttl: 5,
  6. };
  7. }
  8. }

为了防止 CacheConfigService 内部创建 CacheModule 并使用从不同模块导入的提供程序,您可以使用 useExisting 语法。

  1. CacheModule.registerAsync({
  2. imports: [ConfigModule],
  3. useExisting: ConfigService,
  4. });

它和 useClass 的用法有一个关键的相同点: CacheModule 将查找导入的模块以重新使用已创建的 ConfigService 实例,而不是重复实例化。

序列化(Serialization)

在发送实际响应之前, Serializers 为数据操作提供了干净的抽象层。例如,应始终从最终响应中排除敏感数据(如用户密码)。此外,某些属性可能需要额外的转换,比方说,我们不想发送整个数据库实体。相反,我们只想选择 id 和 name 。其余部分应自动剥离。不幸的是,手动映射所有实体可能会带来很多麻烦。

译者注: Serialization 实现可类比 composer 库中 fractal ,响应给用户的数据不仅仅要剔除设计安全的属性,还需要剔除一些无用字段如 create_time, delete_time,update_time 和其他属性。在 JAVA 的实体类中定义N 个属性的话就会返回 N 个字段,解决方法可以使用范型编程,否则操作实体类回影响数据库映射字段。

概要

为了提供一种直接的方式来执行这些操作, Nest 附带了这个 ClassSerializerInterceptor 类。它使用类转换器来提供转换对象的声明性和可扩展方式。基于此类基础下,可以从类转换器 ClassSerializerInterceptor 中获取方法和调用 classToPlain() 函数返回的值。

排除属性

让我们假设一下,如何从一个含有多属性的实体中剔除 password 属性?

  1. import { Exclude } from 'class-transformer';
  2. export class UserEntity {
  3. id: number;
  4. firstName: string;
  5. lastName: string;
  6. @Exclude()
  7. password: string;
  8. constructor(partial: Partial<UserEntity>) {
  9. Object.assign(this, partial);
  10. }
  11. }

然后,直接在控制器的方法中调用就能获得此类 UserEntity 的实例。

  1. @UseInterceptors(ClassSerializerInterceptor)
  2. @Get()
  3. findOne(): UserEntity {
  4. return new UserEntity({
  5. id: 1,
  6. firstName: 'Kamil',
  7. lastName: 'Mysliwiec',
  8. password: 'password',
  9. });
  10. }

提示: @SerializeOptions() 装饰器来源于 @nestjs/common 包。

现在当你调用此服务时,将收到以下响应结果:

  1. {
  2. "id": 1,
  3. "firstName": "Kamil",
  4. "lastName": "Mysliwiec"
  5. }

注意,拦截器可以应用于整个应用程序。拦截器和实体类声明的组合确保返回 UserEntity 的任何方法都将确保删除 password 属性。

公开属性

您可以使用 @Expose() 装饰器来为属性提供别名,或者执行一个函数来计算属性值(类似于 getter 函数),如下所示。

  1. @Expose()
  2. get fullName(): string {
  3. return `${this.firstName} ${this.lastName}`;
  4. }

变换

您可以使用 @Transform() 装饰器执行其他数据转换。例如,您要选择一个名称 RoleEntity 而不是返回整个对象。

  1. @Transform(role => role.name)
  2. role: RoleEntity;

通过属性

可变选项可能因某些因素而异。要覆盖默认设置,请使用 @SerializeOptions() 装饰器。

  1. @SerializeOptions({
  2. excludePrefixes: ['_'],
  3. })
  4. @Get()
  5. findOne(): UserEntity {
  6. return {};
  7. }

提示: @SerializeOptions() 装饰器来源于 @nestjs/common 包。

通过 @SerializeOptions() 传递的选项作为底层 classToPlain() 函数的第二个参数传递。在本例中,我们自动排除了所有以_前缀开头的属性。

Websockets 和微服务

虽然本章展示了使用 HTTP 风格的应用程序的例子(例如,ExpressFastify ),但是 ClassSerializerInterceptor对于 WebSockets 和微服务的工作方式是一样的,不管使用的是哪种传输方法。

更多

想了解有关装饰器选项的更多信息,请访问此页面

日志

Nest附带一个默认的内部日志记录器实现,它在实例化过程中以及在一些不同的情况下使用,比如发生异常等等。但是,有时您可能希望完全禁用日志记录,或者提供自定义实现并自己处理消息。为了关闭记录器,我们使用 Nes t应用程序选项对象。

  1. const app = await NestFactory.create(ApplicationModule, {
  2. logger: false,
  3. });
  4. await app.listen(3000);

你也可以只启用某些类型的日志:

  1. const app = await NestFactory.create(ApplicationModule, {
  2. logger: ['error', 'warn'],
  3. });
  4. await app.listen(3000);

在某些场景中,我们可能希望在底层使用不同的日志记录器。为此,我们必须传递一个实现 LoggerService 接口的对象。例如,一个内置的控制台。

  1. const app = await NestFactory.create(ApplicationModule, {
  2. logger: console,
  3. });
  4. await app.listen(3000);

但这不是一个最好的办法,我们也可以选择创建自定义的记录器:

  1. import { LoggerService } from '@nestjs/common';
  2. export class MyLogger implements LoggerService {
  3. log(message: string) {}
  4. error(message: string, trace: string) {}
  5. warn(message: string) {}
  6. debug(message: string) {}
  7. verbose(message: string) {}
  8. }

然后,我们可以 MyLogger 直接应用实例:

  1. const app = await NestFactory.create(ApplicationModule, {
  2. logger: new MyLogger(),
  3. });
  4. await app.listen(3000);

扩展内置的日志类

很多实例操作需要创建自己的日志。你不必完全重新发明轮子。只需扩展内置 Logger 类以部分覆盖默认实现,并使用 super 将调用委托给父类。

  1. import { Logger } from '@nestjs/common';
  2. export class MyLogger extends Logger {
  3. error(message: string, trace: string) {
  4. // add your tailored logic here
  5. super.error(message, trace);
  6. }
  7. }

依赖注入

如果要在 Logger 类中启用依赖项注入,则必须使 MyLogger 该类成为实际应用程序的一部分。例如,您可以创建一个 LoggerModule:

  1. import { Module } from '@nestjs/common';
  2. import { MyLogger } from './my-logger.service.ts';
  3. @Module({
  4. providers: [MyLogger],
  5. exports: [MyLogger],
  6. })
  7. export class LoggerModule {}

一旦 LoggerModule 在其他地方导入,框架将负责创建 Logger 类的实例。现在,要在整个应用程序中使用相同的 Logger 实例,包括引导和错误处理的东西,请使用以下方式:

  1. const app = await NestFactory.create(ApplicationModule, {
  2. logger: false,
  3. });
  4. app.useLogger(app.get(MyLogger));
  5. await app.listen(3000);

此解决方案的唯一缺点是您的第一个初始化消息将不会由您的 Logger 实例处理,但此时这点并不重要。

安全

在本章中,您将学习一些可以提高应用程序安全性的技术。

Helmet

通过适当地设置 HTTP 头,Helmet 可以帮助保护您的应用免受一些众所周知的 Web 漏洞的影响。通常,Helmet 只是12个较小的中间件函数的集合,它们设置与安全相关的 HTTP 头(阅读更多)。首先,安装所需的包:

  1. $ npm i --save helmet

安装完成后,将其应用为全局中间件。

  1. import * as helmet from 'helmet';
  2. // somewhere in your initialization file
  3. app.use(helmet());

CORS

跨源资源共享(CORS)是一种允许从另一个域请求资源的机制。在引擎盖下,Nest 使用了 cors 包,它提供了一系列选项,您可以根据自己的要求进行自定义。为了启用 CORS,您必须调用 enableCors() 方法。

  1. const app = await NestFactory.create(ApplicationModule);
  2. app.enableCors();
  3. await app.listen(3000);

此外,您可以将配置对象作为此函数的参数传递。可用的属性在官方 cors 存储库中详尽描述。另一种方法是使用 Nest 选项对象:

  1. const app = await NestFactory.create(ApplicationModule, { cors: true });
  2. await app.listen(3000);

您也可以使用 cors 配置对象(更多信息),而不是传递布尔值。

CSRF

跨站点请求伪造(称为 CSRFXSRF)是一种恶意利用网站,其中未经授权的命令从 Web 应用程序信任的用户传输。要减轻此类攻击,您可以使用 csurf 软件包。首先,安装所需的包:

  1. $ npm i --save csurf

正如 csurf 中间件页面所解释的,csurf 模块需要首先初始化会话中间件或 cookie 解析器。有关进一步说明,请参阅该文档

安装完成后,将其应用为全局中间件。

  1. import * as csurf from 'csurf';
  2. // somewhere in your initialization file
  3. app.use(csurf());

限速

为了保护您的应用程序免受暴力攻击,您必须实现某种速率限制。幸运的是,NPM上已经有很多各种中间件可用。其中之一是express-rate-limit

  1. $ npm i --save express-rate-limit

安装完成后,将其应用为全局中间件。

  1. import * as rateLimit from 'express-rate-limit';
  2. // somewhere in your initialization file
  3. app.use(
  4. rateLimit({
  5. windowMs: 15 * 60 * 1000, // 15 minutes
  6. max: 100, // limit each IP to 100 requests per windowMs
  7. }),
  8. );

提示: 如果您在 FastifyAdapter 下开发,请考虑使用 fastify-rate-limit

配置

应用程序通常在不同的环境中运行。根据环境的不同,应该使用不同的配置设置。例如,通常本地环境依赖于特定的数据库凭据,仅对本地DB实例有效。生产环境将使用一组单独的DB凭据。由于配置变量会更改,所以最佳实践是将配置变量存储在环境中。

通过 process.env 全局,xternal 定义的环境变量在Node.js 内部可见。 我们可以尝试通过在每个环境中分别设置环境变量来解决多个环境的问题。 这会很快变得难以处理,尤其是在需要轻松模拟或更改这些值的开发和测试环境中。

Node.js 应用程序中,通常使用 .env 文件,其中包含键值对,其中每个键代表一个特定的值,以代表每个环境。 在不同的环境中运行应用程序仅是交换正确的.env 文件的问题。

Nest 中使用这种技术的一个好方法是创建一个 ConfigModule ,它公开一个 ConfigService ,根据 $NODE_ENV 环境变量加载适当的 .env 文件。

安装

为了解析我们的环境文件,我们将使用dotenv包。

  1. $ npm i --save dotenv
  2. $ npm i --save-dev @types/dotenv

服务

首先,我们创建一个 ConfigService 类,它将执行必要的 .env 文件解析并提供读取配置变量的接口。

config/config.service.ts

  1. import * as dotenv from 'dotenv';
  2. import * as fs from 'fs';
  3. export class ConfigService {
  4. private readonly envConfig: Record<string, string>;
  5. constructor(filePath: string) {
  6. this.envConfig = dotenv.parse(fs.readFileSync(filePath))
  7. }
  8. get(key: string): string {
  9. return this.envConfig[key];
  10. }
  11. }

这个类只有一个参数,filePath 是你的 .env 文件的路径。提供 get() 方法以启用对私有 envConfig 对象的访问,该对象包含在环境文件中定义的每个属性。

最后一步是创建一个 ConfigModule

  1. import { Module } from '@nestjs/common';
  2. import { ConfigService } from './config.service';
  3. @Module({
  4. providers: [
  5. {
  6. provide: ConfigService,
  7. useValue: new ConfigService(`${process.env.NODE_ENV || 'development'}.env`),
  8. },
  9. ],
  10. exports: [ConfigService],
  11. })
  12. export class ConfigModule {}

ConfigModule 注册一个 ConfigService ,并将其导出为在其他消费模块中可见。此外,我们使用 useValue 语法(参见自定义提供程序)来传递到 .env 文件的路径。此路径将根据 NODE_ENV 环境变量中包含的实际执行环境而不同(例如,’开发’、’生产’等)。

现在您可以简单地在任何地方注入 ConfigService ,并根据传递的密钥检索特定的配置值。

development.env

  1. DATABASE_USER = test;
  2. DATABASE_PASSWORD = test;

使用 ConfigService

要从 ConfigService 访问环境变量,我们需要注入它。因此我们首先需要导入该模块。

app.module.ts

  1. @Module({
  2. imports: [ConfigModule],
  3. ...
  4. })

然后我们可以使用标准的构造函数注入,并在我们的类中使用它:

app.service.ts

  1. import { Injectable } from '@nestjs/common';
  2. import { ConfigService } from './config/config.service';
  3. @Injectable()
  4. export class AppService {
  5. private isAuthEnabled: boolean;
  6. constructor(config: ConfigService) {
  7. // Please take note that this check is case sensitive!
  8. this.isAuthEnabled = config.get('IS_AUTH_ENABLED') === 'true';
  9. }

您可以选择将 ConfigModule 声明为全局模块,而不是在每个模块中导入 ConfigModule

高级配置

我们刚刚实现了一个基础 ConfigService。但是,这种方法有几个缺点,我们现在将解决这些缺点:

  • 缺少环境变量的名称和类型(无智能感知)
  • 缺少提供对 .env 文件的验证
  • env文件将布尔值作为string (‘true‘),提供,因此每次都必须将它们转换为 boolean

验证

我们将从验证提供的环境变量开始。如果未提供所需的环境变量或者它们不符合您的预定义要求,则可以抛出错误。为此,我们将使用 npmJoi。通过 Joi,您可以定义一个对象模式(schema)并根据它来验证 JavaScript 对象。

安装 Joi 和它的类型(用于 TypeScript 用户):

  1. $ npm install --save @hapi/joi
  2. $ npm install --save-dev @types/hapi__joi

安装软件包后,我们就可以转到 ConfigService

config.service.ts

  1. import * as dotenv from 'dotenv';
  2. import * as Joi from '@hapi/joi';
  3. import * as fs from 'fs';
  4. export type EnvConfig = Record<string, string>;
  5. export class ConfigService {
  6. private readonly envConfig: EnvConfig;
  7. constructor(filePath: string) {
  8. const config = dotenv.parse(fs.readFileSync(filePath));
  9. this.envConfig = this.validateInput(config);
  10. }
  11. /**
  12. * Ensures all needed variables are set, and returns the validated JavaScript object
  13. * including the applied default values.
  14. */
  15. private validateInput(envConfig: EnvConfig): EnvConfig {
  16. const envVarsSchema: Joi.ObjectSchema = Joi.object({
  17. NODE_ENV: Joi.string()
  18. .valid('development', 'production', 'test', 'provision')
  19. .default('development'),
  20. PORT: Joi.number().default(3000),
  21. API_AUTH_ENABLED: Joi.boolean().required(),
  22. });
  23. const { error, value: validatedEnvConfig } = envVarsSchema.validate(
  24. envConfig,
  25. );
  26. if (error) {
  27. throw new Error(`Config validation error: ${error.message}`);
  28. }
  29. return validatedEnvConfig;
  30. }
  31. }

由于我们为 NODE_ENVPORT 设置了默认值,因此如果不在环境文件中提供这些变量,验证将不会失败。然而, 我们需要明确提供 API_AUTH_ENABLED。如果我们的 .env 文件中的变量不是模式( schema )的一部分, 则验证也会引发错误。此外,Joi 还会尝试将 env 字符串转换为正确的类型。

类属性

对于每个配置属性,我们必须添加一个getter方法。

config.service.ts

  1. get isApiAuthEnabled(): boolean {
  2. return Boolean(this.envConfig.API_AUTH_ENABLED);
  3. }

现在我们可以像下面这样使用getter函数:

app.service.ts

  1. @Injectable()
  2. export class AppService {
  3. constructor(config: ConfigService) {
  4. if (config.isApiAuthEnabled) {
  5. // Authorization is enabled
  6. }
  7. }
  8. }

压缩

压缩可以大大减小响应主体的大小,从而提高 Web 应用程序的速度。使用压缩中间件启用 gzip 压缩。

安装

首先,安装所需的包:

  1. $ npm i --save compression

安装完成后,将其应用为全局中间件。

  1. import * as compression from 'compression';
  2. // somewhere in your initialization file
  3. app.use(compression());

提示: 如果你在使用的是 FastifyAdapter,请考虑使用 fastify-compress 代替。

对于生产中的高流量网站,实施压缩的最佳方法是在反向代理级别实施。在这种情况下,您不需要使用压缩中间件。

HTTP 模块

Axios 是丰富功能的 HTTP 客户端, 广泛应用于许多应用程序中。这就是为什么 Nest 包装这个包, 并公开它默认为内置 HttpModuleHttpModule 导出 HttpService, 它只是公开了基于 axios 的方法来执行 HTTP 请求, 而且还将返回类型转换为 Observables

为了使用 httppservice,我们需要导入 HttpModule

  1. @Module({
  2. imports: [HttpModule],
  3. providers: [CatsService],
  4. })
  5. export class CatsModule {}

HttpModule@nestjs/common 包提供的

然后,你可以注入 HttpService。这个类可以从@nestjs/common 包中获取。

  1. @Injectable()
  2. export class CatsService {
  3. constructor(private readonly httpService: HttpService) {}
  4. findAll(): Observable<AxiosResponse<Cat[]>> {
  5. return this.httpService.get('http://localhost:3000/cats');
  6. }
  7. }

所有方法都返回 AxiosResponse, 并使用 Observable 对象包装。

配置

Axios 提供了许多选项,您可以利用这些选项来增加您的 HttpService 功能。在这里阅读更多相关信息。要配置底层库实例,请使用 register() 方法的 HttpModule

  1. @Module({
  2. imports: [
  3. HttpModule.register({
  4. timeout: 5000,
  5. maxRedirects: 5,
  6. }),
  7. ],
  8. providers: [CatsService],
  9. })
  10. export class CatsModule {}

所有这些属性都将传递给 axios 构造函数。

异步配置

通常,您可能希望异步传递模块属性,而不是事先传递它们。在这种情况下,使用 registerAsync() 方法,提供了几种处理异步数据的方法。

第一种可能的方法是使用工厂函数:

  1. HttpModule.registerAsync({
  2. useFactory: () => ({
  3. timeout: 5000,
  4. maxRedirects: 5,
  5. }),
  6. });

显然,我们的工厂表现得与其他工厂一样( async 能够通过 inject 注入依赖关系)。

  1. HttpModule.registerAsync({
  2. imports: [ConfigModule],
  3. useFactory: async (configService: ConfigService) => ({
  4. timeout: configService.getString('HTTP_TIMEOUT'),
  5. maxRedirects: configService.getString('HTTP_MAX_REDIRECTS'),
  6. }),
  7. inject: [ConfigService],
  8. });

或者,您可以使用类而不是工厂。

  1. HttpModule.registerAsync({
  2. useClass: HttpConfigService,
  3. });

上面的构造将在 HttpModule 中实例化 HttpConfigService,并利用它来创建 options 对象。 HttpConfigService 必须实现 HttpModuleOptionsFactory 接口。

  1. @Injectable()
  2. class HttpConfigService implements HttpModuleOptionsFactory {
  3. createHttpOptions(): HttpModuleOptions {
  4. return {
  5. timeout: 5000,
  6. maxRedirects: 5,
  7. };
  8. }
  9. }

为了防止在 HttpModule 中创建 HttpConfigService 并使用从不同模块导入的提供者,您可以使用 useExisting 语法。

  1. HttpModule.registerAsync({
  2. imports: [ConfigModule],
  3. useExisting: ConfigService,
  4. });

它的工作原理与 useClass 相同,但有一个关键的区别: HttpModule 将查找导入的模块来重用已经创建的 ConfigService,而不是自己实例化它。

MVC

Nest 默认使用 Express 库,因此有关Express 中的 MVC(模型 - 视图 - 控制器)模式的每个教程都与 Nest 相关。首先,让我们使用 CLI 工具搭建一个简单的 Nest 应用程序:

  1. $ npm i -g @nestjs/cli
  2. $ nest new project

为了创建一个简单的 MVC 应用程序,我们必须安装一个模板引擎

  1. $ npm install --save hbs

我们决定使用 hbs 引擎,但您可以使用任何符合您要求的内容。安装过程完成后,我们需要使用以下代码配置 express 实例:

main.ts

  1. import { NestFactory } from '@nestjs/core';
  2. import { NestExpressApplication } from '@nestjs/platform-express';
  3. import { join } from 'path';
  4. import { AppModule } from './app.module';
  5. async function bootstrap() {
  6. const app = await NestFactory.create<NestExpressApplication>(
  7. AppModule,
  8. );
  9. app.useStaticAssets(join(__dirname, '..', 'public'));
  10. app.setBaseViewsDir(join(__dirname, '..', 'views'));
  11. app.setViewEngine('hbs');
  12. await app.listen(3000);
  13. }
  14. bootstrap();

我们告诉 express,该 public 目录将用于存储静态文件, views 将包含模板,并且 hbs 应使用模板引擎来呈现 HTML 输出。

模板渲染

现在,让我们在该文件夹内创建一个 views 目录和一个 index.hbs 模板。在模板内部,我们将打印从控制器传递的 message

index.hbs

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8" />
  5. <title>App</title>
  6. </head>
  7. <body>
  8. {{ message }}
  9. </body>
  10. </html>

然后, 打开 app.controller 文件, 并用以下代码替换 root() 方法:

app.controller.ts

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

@Controller()
export class AppController {
  @Get()
  @Render('index')
  root() {
    return { message: 'Hello world!' };
  }
}

事实上,当 Nest 检测到 @Res() 装饰器时,它会注入 response 对象。在这里了解更多关于它的能力。

在应用程序运行时,打开浏览器访问 http://localhost:3000/ 你应该看到这个 Hello world! 消息。

动态模板渲染

如果应用程序逻辑必须动态决定要呈现哪个模板,那么我们应该使用 @Res()装饰器,并在路由处理程序中提供视图名,而不是在 @Render() 装饰器中:

Nest 检测到 @Res() 装饰器时,它将注入特定于库的响应对象。我们可以使用这个对象来动态呈现模板。在这里了解关于响应对象 API 的更多信息。

app.controller.ts


import { Get, Controller, Render } from '@nestjs/common';
import { Response } from 'express';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  root(@Res() res: Response) {
    return res.render(
      this.appService.getViewName(),
      { message: 'Hello world!' },
    );
  }
}

这里有一个可用的例子。

MVC(fastify)

如本章所述,我们可以将任何兼容的 HTTP 提供程序与 Nest 一起使用。比如 Fastify 。为了创建具有 fastifyMVC 应用程序,我们必须安装以下包:

$ npm i --save fastify point-of-view handlebars

接下来的步骤几乎涵盖了与 express 库相同的内容(差别很小)。安装过程完成后,我们需要打开 main.ts 文件并更新其内容:

main.ts

import { NestFactory } from '@nestjs/core';
import { NestFastifyApplication, FastifyAdapter } from '@nestjs/platform-fastify';
import { AppModule } from './app.module';
import { join } from 'path';

async function bootstrap() {
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter(),
  );
  app.useStaticAssets({
    root: join(__dirname, '..', 'public'),
    prefix: '/public/',
  });
  app.setViewEngine({
    engine: {
      handlebars: require('handlebars'),
    },
    templates: join(__dirname, '..', 'views'),
  });
  await app.listen(3000);
}
bootstrap();

API 略有不同,但这些方法调用背后的想法保持不变。此外,我们还必须确保传递到 @Render() 装饰器中的模板名称包含文件扩展名。

app.controller.ts

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

@Controller()
export class AppController {
  @Get()
  @Render('index.hbs')
  root() {
    return { message: 'Hello world!' };
  }
}

在应用程序运行时,打开浏览器并导航至 http://localhost:3000/ 。你应该看到这个 Hello world! 消息。

这里有 一个可用的例子。

性能(Fastify)

在底层,Nest 使用了Express,但如前所述,它提供了与各种其他库的兼容性,例如 Fastify。它是怎么工作的?事实上,Nest需要使用您最喜欢的库,它是一个兼容的适配器,它主要将相应的处理程序代理到适当的库特定的方法。此外,您的库必须至少提供与 express 类似的请求-响应周期管理。

Fastify 非常适合这里,因为它以与 express 类似的方式解决设计问题。然而,fastify 的速度要快得多,达到了几乎两倍的基准测试结果。问题是,为什么 Nest 仍然使用 express 作为默认的HTTP提供程序?因为 express 是应用广泛、广为人知的,而且拥有一套庞大的兼容中间件。

但是由于 Nest 提供了框架独立性,因此您可以轻松地在它们之间迁移。当您对快速的性能给予很高的评价时,Fastify 可能是更好的选择。要使用 Fastify,只需选择 FastifyAdapter本章所示的内置功能。

安装

首先,我们需要安装所需的软件包:

$ npm i --save @nestjs/platform-fastify

适配器(Adapter)

安装fastify后,我们可以使用 FastifyAdapter

import { NestFactory } from '@nestjs/core';
import {
  FastifyAdapter,
  NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { ApplicationModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create<NestFastifyApplication>(
    ApplicationModule,
    new FastifyAdapter()
  );
  await app.listen(3000);
}
bootstrap();

默认情况下,Fastify仅在 localhost 127.0.0.1 接口上监听(了解更多信息)。如果要接受其他主机上的连接,则应'0.0.0.0'listen() 呼叫中指定:

async function bootstrap() {
  const app = await NestFactory.create<NestFastifyApplication>(
    ApplicationModule,
    new FastifyAdapter()
  );
  await app.listen(3000, '0.0.0.0');
}

平台特定的软件包

请记住,当您使用 FastifyAdapter 时,Nest 使用 Fastify 作为 HTTP 提供程序。 这意味着依赖 Express 的每个配方都可能不再起作用。 您应该改为使用 Fastify 等效程序包。

重定向响应

Fastify 处理重定向响应的方式与 Express 有所不同。要使用 Fastify 进行正确的重定向,请同时返回状态代码和 URL,如下所示:

@Get()
index(@Res() res) {
  res.status(302).redirect('/login');
}

Fastify选项

您可以通过构造函数将选项传递给 Fastify的构造 FastifyAdapter 函数。例如:

new FastifyAdapter({ logger: true })

例子

这里有一个工作示例

热重载(Webpack)

对应用程序的引导过程影响最大的是 TypeScript 编译。但问题是,每次发生变化时,我们是否必须重新编译整个项目?一点也不。这就是为什么 webpack HMR(Hot-Module Replacement)大大减少了实例化您的应用程序所需的时间。

请注意,webpack这不会自动将(例如 graphql 文件)复制到 dist 文件夹中。类似地,webpack 与全局静态路径(例如中的 entities 属性 TypeOrmModule )不兼容。

CLI

如果使用的是 Nest CLI,则配置过程非常简单。CLI 包装 webpack,允许使用 HotModuleReplacementPlugin

安装

首先,我们安装所需的软件包:

$ npm i --save-dev webpack-node-externals

配置(Configuration)

然后,我们需要创建一个webpack.config.js,它是webpack的一个配置文件,并将其放入根目录。

const webpack = require('webpack');
const nodeExternals = require('webpack-node-externals');

module.exports = function(options) {
  return {
    ...options,
    entry: ['webpack/hot/poll?100', './src/main.ts'],
    watch: true,
    externals: [
      nodeExternals({
        whitelist: ['webpack/hot/poll?100'],
      }),
    ],
    plugins: [...options.plugins, new webpack.HotModuleReplacementPlugin()],
  };
}

此函数获取包含默认 webpack 配置的原始对象,并返回一个已修改的对象和一个已应用的 HotModuleReplacementPlugin 插件。

热模块更换

为了启用 HMR,请打开应用程序入口文件( main.ts )并添加一些与 Webpack相关的说明,如下所示:

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();

就这样。为了简化执行过程,请将这两行添加到 package.json 文件的脚本中。

"build": "nest build --watch --webpack"
"start": "node dist/main",

现在只需打开你的命令行并运行下面的命令:

$ npm run build

webpack开始监视文件后,在另一个命令行窗口中运行另一个命令:

$ npm run start

没有使用 CLI

如果您没有使用 Nest CLI ,配置将稍微复杂一些(需要更多的手动步骤)。

安装

首先安装所需的软件包:

$ npm i --save-dev webpack webpack-cli webpack-node-externals ts-loader

配置

然后,我们需要创建一个webpack.config.js,它是 webpack 的一个配置文件,并将其放入根目录。

const webpack = require('webpack');
const path = require('path');
const nodeExternals = require('webpack-node-externals');

module.exports = {
  entry: ['webpack/hot/poll?100', './src/main.ts'],
  watch: true,
  target: 'node',
  externals: [
    nodeExternals({
      whitelist: ['webpack/hot/poll?100'],
    }),
  ],
  module: {
    rules: [
      {
        test: /.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  mode: 'development',
  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
  },
  plugins: [new webpack.HotModuleReplacementPlugin()],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'server.js',
  },
};

这个配置告诉 webpack 关于我们的应用程序的一些基本信息。入口文件位于何处,应使用哪个目录保存已编译的文件,以及我们要使用哪种装载程序来编译源文件。基本上,您不必担心太多,根本不需要了解该文件的内容。

热模块更换

为了启用 HMR ,我们必须打开应用程序入口文件( main.ts ),并添加一些与 Webpack 相关的说明。

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();

为了简化执行过程,请将两个脚本添加到 package.json 文件中。

"webpack": "webpack --config webpack.config.js"
"start": "node dist/server",

现在,只需打开命令行并运行以下命令:

$ npm run webpack

一旦 webpack 开始监视文件,请在单独的命令行窗口中运行以下命令:

$ npm run start

这里有一个可用的例子

译者署名

用户名 头像 职能 签名
@zuohuadong 技术 - 图1 翻译 专注于 caddy 和 nest,@zuohuadong at Github
@Drixn 技术 - 图2 翻译 专注于 nginx 和 C++,@Drixn

@Armor | 技术 - 图3 | 翻译 | 专注于 Java 和 Nest,@Armor || @Erchoc | 技术 - 图4 | 翻译 | 学习更优雅的架构方式,做更贴切用户的产品。@Erchoc at Github || @havef | 技术 - 图5 | 校正 | 数据分析、机器学习、TS/JS技术栈 @havef |