技术

认证(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 的策略,它实现了一种用户名/密码身份验证机制,这符合我们在这一部分用例中的需求。

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

对于您选择的任何 Passport 策略,都需要 @nestjs/Passport 和 Passport 包。然后,需要安装特定策略的包(例如,passport-jwt 或 passport-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 :

$ nest g module auth
$ nest g service auth

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

$ nest g module users
$ nest g service users

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

users/users.service.ts

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

export type User = any;

@Injectable()
export class UsersService {
  private readonly users: User[];

  constructor() {
    this.users = [
      {
        userId: 1,
        username: 'john',
        password: 'changeme',
      },
      {
        userId: 2,
        username: 'chris',
        password: 'secret',
      },
      {
        userId: 3,
        username: 'maria',
        password: 'guess',
      },
    ];
  }

  async findOne(username: string): Promise<User | undefined> {
    return this.users.find(user => user.username === username);
  }
}

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

users/users.module.ts

import { Module } from '@nestjs/common';
import { UsersService } from './users.service';

@Module({
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

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

auth/auth.service.ts

import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';

@Injectable()
export class AuthService {
  constructor(private readonly usersService: UsersService) {}

  async validateUser(username: string, pass: string): Promise<any> {
    const user = await this.usersService.findOne(username);
    if (user && user.password === pass) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }
}

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

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

auth/auth.module.ts

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';

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

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

auth/local.strategy.ts

import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly authService: AuthService) {
    super();
  }

  async validate(username: string, password: string): Promise<any> {
    const user = await this.authService.validateUser(username, password);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}

我们遵循了前面描述的所有护照策略。在我们的 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

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './local.strategy';

@Module({
  imports: [UsersModule, PassportModule],
  providers: [AuthService, LocalStrategy],
})
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

import { Controller, Request, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Controller()
export class AppController {
  @UseGuards(AuthGuard('local'))
  @Post('auth/login')
  async login(@Request() req) {
    return req.user;
  }
}

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

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

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

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

如果上述内容可以正常工作,可以通过直接将策略名称传递给AuthGuard()来引入代码库中的魔术字符串。作为替代,我们推荐创建自己的类,如下所示:

auth/local-auth.guard.ts

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

@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
``` ```
@UseGuards(LocalAuthGuard)
@Post('auth/login')
async login(@Request() req) {
  return req.user;
}

JWT 功能

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

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

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

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

$ npm install @nestjs/jwt passport-jwt
$ 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

import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class AuthService {
  constructor(
    private readonly usersService: UsersService,
    private readonly jwtService: JwtService
  ) {}

  async validateUser(username: string, pass: string): Promise<any> {
    const user = await this.usersService.findOne(username);
    if (user && user.password === pass) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }

  async login(user: any) {
    const payload = { username: user.username, sub: user.userId };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }
}

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

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

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

auth/constants.ts

export const jwtConstants = {
  secret: 'secretKey',
};

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

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

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

auth/auth.module.tsJS

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';

@Module({
  imports: [
    UsersModule,
    PassportModule,
    JwtModule.register({
      secret: jwtConstants.secret,
      signOptions: { expiresIn: '60s' },
    }),
  ],
  providers: [AuthService, LocalStrategy],
  exports: [AuthService],
})
export class AuthModule {}

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

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

app.controller.ts

import { Controller, Request, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth/auth.service';

@Controller()
export class AppController {
  constructor(private readonly authService: AuthService) {}

  @UseGuards(AuthGuard('local'))
  @Post('auth/login')
  async login(@Request() req) {
    return this.authService.login(req.user);
  }
}

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

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

实施 Passport JWT

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

auth/jwt.strategy.ts

import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { jwtConstants } from './constants';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: jwtConstants.secret,
    });
  }

  async validate(payload: any) {
    return { userId: payload.sub, username: payload.username };
  }
}

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

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

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

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

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

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

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

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

auth/auth.module.ts

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { JwtStrategy } from './jwt.strategy';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';

@Module({
  imports: [
    UsersModule,
    PassportModule,
    JwtModule.register({
      secret: jwtConstants.secret,
      signOptions: { expiresIn: '60s' },
    }),
  ],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}

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

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

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

app.controller.ts

import { Controller, Get, Request, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth/auth.service';

@Controller()
export class AppController {
  constructor(private readonly authService: AuthService) {}

  @UseGuards(AuthGuard('local'))
  @Post('auth/login')
  async login(@Request() req) {
    return this.authService.login(req.user);
  }

  @UseGuards(AuthGuard('jwt'))
  @Get('profile')
  getProfile(@Request() req) {
    return req.user;
  }
}

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

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

$ # GET /profile
$ curl http://localhost:3000/profile
$ # result -> {"statusCode":401,"error":"Unauthorized"}

$ # POST /auth/login
$ curl -X POST http://localhost:3000/auth/login -d '{"username": "john", "password": "changeme"}' -H "Content-Type: application/json"
$ # result -> {"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm... }

$ # GET /profile using access_token returned from previous step as bearer code
$ curl http://localhost:3000/profile -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm..."
$ # 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

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';
import { JwtStrategy } from './jwt.strategy';

@Module({
  imports: [
    PassportModule.register({ defaultStrategy: 'jwt' }),
    JwtModule.register({
      secret: jwtConstants.secret,
      signOptions: { expiresIn: '60s' },
    }),
    UsersModule
  ],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}

请求范围策略

passportAPI基于将策略注册到库的全局实例。因此策略并没有设计为依赖请求的选项的或者根据每个请求动态生成实例(更多内容见请求范围提供者)。当你配置你的策略为请求范围时,Nest永远不会将其实例化,因为它并没有和任何特定路径绑定。并没有一个物理方法来决定哪个“请求范围”策略会根据每个请求执行。

然而,在策略中总有办法动态处理请求范围提供者。我们在这里利用模块参考特性。

首先,打开local.strategy.ts文件并且将ModuleRef按照正常方法注入其中:

constructor(private moduleRef: ModuleRef){
  super({
    passReqToCallback:true;
  })
}

!> 注意: ModuleRef 类需要从@nestjs/core中导入。

要保证passReqToCallback属性和上述示例中一样配置为true。

在下一步中,请求的实例将被用于获取一个当前上下文标识,而不是生成一个新的(更多关于请求上下文的内容见这里)。

现在,在LocalStrategy类的validate()方法中,使用ContextIdFactory类中的getByRequest()方法来创建一个基于请求对向的上下文id,并将其传递给resolve()调用:


async validate(
  request: Request,
  username: string,
  password: string,
) {
  const contextId = ContextIdFactory.getByRequest(request);
  // "AuthService" is a request-scoped provider
  const authService = await this.moduleRef.resolve(AuthService, contextId);
  ...
}

在上述例子中,resolve()方法会异步返回AuthService提供者的请求范围实例(我们假设AuthService被标示为一个请求范围提供者)。

扩展守卫

在大多数情况下,使用一个提供的AuthGuard类是有用的。然而,在一些用例中你可能只是希望简单地扩展默认的错误处理或者认证逻辑。在这种情况下,你可以通过一个子类来扩展内置的类并且覆盖其方法。


import {
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  canActivate(context: ExecutionContext) {
    // 在这里添加自定义的认证逻辑
    // 例如调用 super.logIn(request) 来建立一个session
    return super.canActivate(context);
  }

  handleRequest(err, user, info) {
    // 可以抛出一个基于info或者err参数的异常
    if (err || !user) {
      throw err || new UnauthorizedException();
    }
    return user;
  }
}

自定义 Passport

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

PassportModule.register({ session: true });

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

constructor(private readonly authService: AuthService) {
  super({
    usernameField: 'email',
    passwordField: 'password',
  });
}

看看Passport Website官方文档吧。

命名策略

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

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

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

GraphQL

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

@Injectable()
export class GqlAuthGuard extends AuthGuard('jwt') {
  getRequest(context: ExecutionContext) {
    const ctx = GqlExecutionContext.create(context);
    return ctx.getContext().req;
  }
}

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

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

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

import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';

export const CurrentUser = createParamDecorator(
  (data: unknown, context: ExecutionContext) => {
    const ctx = GqlExecutionContext.create(context);
    return ctx.getContext().req.user;
  },
);

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

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

数据库

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

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

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

TypeORM 集成

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

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

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

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

app.module.ts

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

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      entities: [],
      synchronize: true,
    }),
  ],
})
export class AppModule {}

forRoot() 方法支持所有TypeORM包中createConnection()函数暴露出的配置属性。其他一些额外的配置参数描述如下:

参数 说明
retryAttempts 重试连接数据库的次数(默认:10)
retryDelay 两次重试连接的间隔(ms)(默认:3000)
autoLoadEntities 如果为true,将自动加载实体(默认:false)
keepConnectionAlive 如果未true,在应用程序关闭后连接不会关闭(默认:false)

?> 更多连接选项见这里

另外,我们可以创建 ormconfig.json ,而不是将配置对象传递给 forRoot()。

{
  "type": "mysql",
  "host": "localhost",
  "port": 3306,
  "username": "root",
  "password": "root",
  "database": "test",
  "entities": ["dist/**/*.entity{.ts,.js}"],
  "synchronize": true
}

然后,我们可以不带任何选项地调用 forRoot() :

app.module.ts

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

@Module({
  imports: [TypeOrmModule.forRoot()],
})
export class AppModule {}

?> 静态全局路径(例如 dist/**/*.entity{ .ts,.js} )不适用于Webpack热重载。

!> 注意,ormconfig.json 文件由typeorm库载入,因此,任何上述参数之外的属性都不会被应用(例如由forRoot()方法内部支持的属性—例如autoLoadEntities和retryDelay())

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

app.module.ts

import { Connection } from 'typeorm';

@Module({
  imports: [TypeOrmModule.forRoot(), PhotoModule],
})
export class AppModule {
  constructor(private readonly connection: Connection) {}
}

存储库模式

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

为了继续这个示例,我们需要至少一个实体。我们来定义User 实体。

user.entity.ts

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  firstName: string;

  @Column()
  lastName: string;

  @Column({ default: true })
  isActive: boolean;
}

?> 关于实体的更多内容见TypeORM 文档。

该 User 实体在 users 目录下。这个目录包含了和 UsersModule模块有关的所有文件。你可以决定在哪里保存模型文件,但我们推荐在他们的域 中就近创建,即在相应的模块目录中。

要开始使用 user 实体,我们需要在模块的forRoot()方法的选项中(除非你使用一个静态的全局路径)将它插入entities数组中来让 TypeORM知道它的存在。

app.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Photo } from './photo/photo.entity';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      entities: [User],
      synchronize: true,
    }),
  ],
})
export class AppModule {}

现在让我们看一下 UsersModule:

user.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { User } from './user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  providers: [UsersService],
  controllers: [UsersController],
})
export class UsersModule {}

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

users.service.ts


import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  findAll(): Promise<User[]> {
    return this.usersRepository.find();
  }

  findOne(id: string): Promise<User> {
    return this.usersRepository.findOne(id);
  }

  async remove(id: string): Promise<void> {
    await this.usersRepository.delete(id);
  }
}

?> 不要忘记将 UsersModule 导入根 AppModule。

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

users.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  exports: [TypeOrmModule]
})
export class UsersModule {}

现在,如果我们在 UserHttpModule 中导入 UsersModule ,我们可以在后一个模块的提供者中使用 @InjectRepository(User)。

users-http.module.ts

import { Module } from '@nestjs/common';
import { UsersModule } from './user.module';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';

@Module({
  imports: [UsersModule],
  providers: [UsersService],
  controllers: [UsersController]
})
export class UserHttpModule {}

关系

关系是指两个或多个表之间的联系。关系基于每个表中的常规字段,通常包含主键和外键。

关系有三种:

名称 说明
一对一 主表中的每一行在外部表中有且仅有一个对应行。使用@OneToOne()装饰器来定义这种类型的关系
一对多/多对一 主表中的每一行在外部表中有一个或多的对应行。使用@OneToMany()和@ManyToOne()装饰器来定义这种类型的关系
多对多 主表中的每一行在外部表中有多个对应行,外部表中的每个记录在主表中也有多个行。使用@ManyToMany()装饰器来定义这种类型的关系

使用对应的装饰器来定义实体的关系。例如,要定义每个User可以有多个Photo,可以使用@OneToMany()装饰器。

user.entity.ts

import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
import { Photo } from '../photos/photo.entity';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  firstName: string;

  @Column()
  lastName: string;

  @Column({ default: true })
  isActive: boolean;

  @OneToMany(type => Photo, photo => photo.user)
  photos: Photo[];
}

?> 要了解TypeORM中关系的内容,可以查看TypeORM文档。

自动载入实体

手动将实体一一添加到连接选项的entities数组中的工作会很无聊。此外,在根模块中涉及实体破坏了应用的域边界,并可能将应用的细节泄露给应用的其他部分。针对这一情况,可以使用静态全局路径(例如, dist/*/.entity{.ts,.js})。

注意,webpack不支持全局路径,因此如果你要在单一仓库(Monorepo)中构建应用,可能不能使用全局路径。针对这一问题,有另外一个可选的方案。在配置对象的属性中(传递给forRoot()方法的)设置autoLoadEntities属性为true来自动载入实体,示意如下:

app.module.ts

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

@Module({
  imports: [
    TypeOrmModule.forRoot({
      ...
      autoLoadEntities: true,
    }),
  ],
})
export class AppModule {}

通过配置这一选项,每个通过forFeature()注册的实体都会自动添加到配置对象的entities数组中。

?> 注意,那些没有通过forFeature()方法注册,而仅仅是在实体中被引用(通过关系)的实体不能通过autoLoadEntities配置被包含。

事务

数据库事务代表在数据库管理系统(DBMS)中针对数据库的一组操作,这组操作是有关的、可靠的并且和其他事务相互独立的。一个事务通常可以代表数据库中的任何变更(了解更多)。

在TypeORM事务中有很多不同策略来处理事务,我们推荐使用QueryRunner类,因为它对事务是完全可控的。

首先,我们需要将Connection对象以正常方式注入:

@Injectable()
export class UsersService {
  constructor(private connection: Connection) {}
}

?> Connection类需要从typeorm包中导入

现在,我们可以使用这个对象来创建一个事务。

async createMany(users: User[]) {
  const queryRunner = this.connection.createQueryRunner();

  await queryRunner.connect();
  await queryRunner.startTransaction();
  try {
    await queryRunner.manager.save(users[0]);
    await queryRunner.manager.save(users[1]);

    await queryRunner.commitTransaction();
  } catch (err) {
    //如果遇到错误,可以回滚事务
    await queryRunner.rollbackTransaction();
  } finally {
    //你需要手动实例化并部署一个queryRunner
    await queryRunner.release();
  }
}

?> 注意connection仅用于创建QueryRunner。然而,要测试这个类,就需要模拟整个Connection对象(它暴露出来的几个方法),因此,我们推荐采用一个帮助工厂类(也就是QueryRunnerFactory)并且定义一个包含仅限于维持事务需要的方法的接口。这一技术让模拟这些方法变得非常直接。

可选地,你可以使用一个Connection对象的回调函数风格的transaction方法(阅读更多)。

async createMany(users: User[]) {
  await this.connection.transaction(async manager => {
    await manager.save(users[0]);
    await manager.save(users[1]);
  });
}

不推荐使用装饰器来控制事务(@Transaction()和@TransactionManager())。

订阅者

使用TypeORM订阅者,你可以监听特定的实体事件。

import {
  Connection,
  EntitySubscriberInterface,
  EventSubscriber,
  InsertEvent,
} from 'typeorm';
import { User } from './user.entity';

@EventSubscriber()
export class UserSubscriber implements EntitySubscriberInterface<User> {
  constructor(connection: Connection) {
    connection.subscribers.push(this);
  }

  listenTo() {
    return User;
  }

  beforeInsert(event: InsertEvent<User>) {
    console.log(`BEFORE USER INSERTED: `, event.entity);
  }
}

!> 事件订阅者不能是请求范围的。

现在,将UserSubscriber类添加到providers数组。

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { UserSubscriber } from './user.subscriber';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  providers: [UsersService, UserSubscriber],
  controllers: [UsersController],
})
export class UsersModule {}

?> 更多实体订阅者内容见这里。

迁移

迁移提供了一个在保存数据库中现有数据的同时增量升级数据库使其与应用中的数据模型保持同步的方法。TypeORM提供了一个专用CLI命令行工具用于生成、运行以及回滚迁移。

迁移类和Nest应用源码是分开的。他们的生命周期由TypeORM CLI管理,因此,你不能在迁移中使用依赖注入和其他Nest专有特性。在TypeORM文档 中查看更多关于迁移的内容。

多个数据库

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

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

const defaultOptions = {
  type: 'postgres',
  port: 5432,
  username: 'user',
  password: 'password',
  database: 'db',
  synchronize: true,
};

@Module({
  imports: [
    TypeOrmModule.forRoot({
      ...defaultOptions,
      host: 'user_db_host',
      entities: [User],
    }),
    TypeOrmModule.forRoot({
      ...defaultOptions,
      name: 'albumsConnection',
      host: 'album_db_host',
      entities: [Album],
    }),
  ],
})
export class AppModule {}

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

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

@Module({
  imports: [
    TypeOrmModule.forFeature([User]),
    TypeOrmModule.forFeature([Album], 'albumsConnection'),
  ],
})
export class AppModule {}

您也可以为给定的连接注入 Connection 或 EntityManager:

@Injectable()
export class AlbumsService {
  constructor(
    @InjectConnection('albumsConnection')
    private connection: Connection,
    @InjectEntityManager('albumsConnection')
    private entityManager: EntityManager,
  ) {}
}

测试

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

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

@Module({
  providers: [
    UsersService,
    {
      provide: getRepositoryToken(User),
      useValue: mockRepository,
    },
  ],
})
export class UsersModule {}

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

定制存储库

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

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

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

?> @EntityRepository() 和 Repository 来自 typeorm 包。

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

@Module({
  imports: [TypeOrmModule.forFeature([AuthorRepository])],
  controller: [AuthorController],
  providers: [AuthorService],
})
export class AuthorModule {}

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

@Injectable()
export class AuthorService {
  constructor(private readonly authorRepository: AuthorRepository) {}
}

异步配置

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

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

TypeOrmModule.forRootAsync({
  useFactory: () => ({
    type: 'mysql',
    host: 'localhost',
    port: 3306,
    username: 'root',
    password: 'root',
    database: 'test',
    entities: [__dirname + '/**/*.entity{.ts,.js}'],
    synchronize: true,
  }),
});

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

TypeOrmModule.forRootAsync({
  imports: [ConfigModule],
  useFactory: (configService: ConfigService) => ({
    type: 'mysql',
    host: configService.get<string>('HOST'),
    port: configService.get<string>('PORT'),
    username: configService.get<string>('USERNAME'),
    password: configService.get<string>('PASSWORD'),
    database: configService.get<string>('DATABASE'),
    entities: [__dirname + '/**/*.entity{.ts,.js}'],
    synchronize: true,
  }),
  inject: [ConfigService],
});

或者,您可以使用useClass语法。

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

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

上面的构造将在TypeOrmModule内部实例化TypeOrmConfigService,并通过调用createTypeOrmOptions()

@Injectable()
class TypeOrmConfigService implements TypeOrmOptionsFactory {
  createTypeOrmOptions(): TypeOrmModuleOptions {
    return {
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      entities: [__dirname + '/**/*.entity{.ts,.js}'],
      synchronize: true,
    };
  }
}

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

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

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

示例

这儿有一个可用的例子。

序列化集成

另一个使用TypeORM的选择是使用@nestjs/sequelize包中的Sequelize ROM。额外地,我们使用sequelize-typescript包来提供一系列额外的装饰器以声明实体。

要开始使用它,我们首先安装需要的依赖。在本章中,我们通过流行的MySQL关系数据库来进行说明。序列化支持很多种关系数据库,例如PostgreSQL,MySQL,Microsoft SQL Server,SQLite以及MariaDB。本章中的步骤也适合其他任何序列化支持的数据库。你只要简单地安装所选数据库相应的客户端API库就可以。

$ npm install --save @nestjs/sequelize sequelize sequelize-typescript mysql2
$ npm install --save-dev @types/sequelize

安装完成后,就可以将SequelizeModule导入到根AppModule中。

app.module.ts

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

@Module({
  imports: [
    SequelizeModule.forRoot({
      dialect: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      models: [],
    }),
  ],
})
export class AppModule {}

forRoot()方法支持所有序列化构造器(了解更多)暴露的配置属性。下面是一些额外的配置属性。

名称 说明
retryAttempts 尝试连接数据库的次数(默认:10)
retryDelay 两次连接之间间隔时间(ms)(默认:3000)
autoLoadModels 如果为true,模型将自动载入(默认:false)
keepConnectionAlive 如果为true,在应用关闭后连接将不会关闭(默认:false)
synchronize 如果为true,自动载入的模型将同步(默认:false)

一旦这些完成了,Sequelize对象就可以注入到整个项目中(不需要在任何模块中再引入),例如:

app.service.ts

import { Injectable } from '@nestjs/common';
import { Sequelize } from 'sequelize-typescript';

@Injectable()
export class AppService {
  constructor(private sequelize: Sequelize) {}
}

模型

序列化采用活动记录(Active Record)模式,在这一模式下,你可以使用模型类直接和数据库交互。要继续该示例,我们至少需要一个模型,让我们定义这个User模型:

user.model.ts

import { Column, Model, Table } from 'sequelize-typescript';

@Table
export class User extends Model<User> {
  @Column
  firstName: string;

  @Column
  lastName: string;

  @Column({ defaultValue: true })
  isActive: boolean;
}

?> 查看更多的可用装饰器。

User模型文件在users目录下。该目录包含了和UsersModule有关的所有文件。你可以决定在哪里保存模型文件,但我们推荐在他们的域 中就近创建,即在相应的模块目录中。

要开始使用User模型,我们需要通过将其插入到forRoot()方法选项的models数组中来让序列化知道它的存在。

app.module.ts

import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import { User } from './users/user.model';

@Module({
  imports: [
    SequelizeModule.forRoot({
      dialect: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      models: [User],
    }),
  ],
})
export class AppModule {}

接下来我们看看UsersModule:

users.module.ts

import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import { User } from './user.model';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  imports: [SequelizeModule.forFeature([User])],
  providers: [UsersService],
  controllers: [UsersController],
})
export class UsersModule {}

这个模块使用forFeature()方法来定义哪个模型被注册在当前范围中。我们可以使用@InjectModel()装饰器来把UserModel注入到UsersService中。

users.service.ts


import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { User } from './user.model';

@Injectable()
export class UsersService {
  constructor(
    @InjectModel(User)
    private userModel: typeof User,
  ) {}

  async findAll(): Promise<User[]> {
    return this.userModel.findAll();
  }

  findOne(id: string): Promise<User> {
    return this.userModel.findOne({
      where: {
        id,
      },
    });
  }

  async remove(id: string): Promise<void> {
    const user = await this.findOne(id);
    await user.destroy();
  }
}

?> 不要忘记在根AppModule中导入UsersModule。

如果你要在导入SequelizeModule.forFreature的模块之外使用存储库,你需要重新导出其生成的提供者。你可以像这样将整个模块导出:

users.module.ts

import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import { User } from './user.entity';

@Module({
  imports: [SequelizeModule.forFeature([User])],
  exports: [SequelizeModule]
})
export class UsersModule {}

现在如果我们在UserHttpModule中引入UsersModule,我们可以在后一个模块的提供者中使用@InjectModel(User)。

users-http.module.ts

import { Module } from '@nestjs/common';
import { UsersModule } from './user.module';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';

@Module({
  imports: [UsersModule],
  providers: [UsersService],
  controllers: [UsersController]
})
export class UserHttpModule {}

关系

关系是指两个或多个表之间的联系。关系基于每个表中的常规字段,通常包含主键和外键。

关系有三种:

名称 说明
一对一 主表中的每一行在外部表中有且仅有一个对应行。使用@OneToOne()装饰器来定义这种类型的关系
一对多/多对一 主表中的每一行在外部表中有一个或多的对应行。使用@OneToMany()和@ManyToOne()装饰器来定义这种类型的关系
多对多 主表中的每一行在外部表中有多个对应行,外部表中的每个记录在主表中也有多个行。使用@ManyToMany()装饰器来定义这种类型的关系

使用对应的装饰器来定义实体的关系。例如,要定义每个User可以有多个Photo,可以使用@HasMany()装饰器。

user.entity.ts

import { Column, Model, Table, HasMany } from 'sequelize-typescript';
import { Photo } from '../photos/photo.model';

@Table
export class User extends Model<User> {
  @Column
  firstName: string;

  @Column
  lastName: string;

  @Column({ defaultValue: true })
  isActive: boolean;

  @HasMany(() => Photo)
  photos: Photo[];
}

?> 阅读本章了解更多关于序列化的内容。

自动载入模型

手动将模型一一添加到连接选项的models数组中的工作会很无聊。此外,在根模块中涉及实体破坏了应用的域边界,并可能将应用的细节泄露给应用的其他部分。针对这一情况,在配置对象的属性中(传递给forRoot()方法的)设置autoLoadModels和synchronize属性来自动载入模型,示意如下:

app.module.ts

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

@Module({
  imports: [
    SequelizeModule.forRoot({
      ...
      autoLoadModels: true,
      synchronize: true,
    }),
  ],
})
export class AppModule {}

通过配置这一选项,每个通过forFeature()注册的实体都会自动添加到配置对象的models数组中。

?> 注意,这不包含那些没有通过forFeature()方法注册,而仅仅是在实体中被引用(通过关系)的模型。

事务

数据库事务代表在数据库管理系统(DBMS)中针对数据库的一组操作,这组操作是有关的、可靠的并且和其他事务相互独立的。一个事务通常可以代表数据库中的任何变更(了解更多)。

在序列化事务中有很多不同策略来处理事务,下面是一个管理事务的示例(自动回调)。

首先,我们需要将Sequelize对象以正常方式注入:

@Injectable()
export class UsersService {
  constructor(private sequelize: Sequelize) {}
}

?> Sequelize类需要从sequelize-typescript包中导入

现在,我们可以使用这个对象来创建一个事务。

async createMany() {
  try {
    await this.sequelize.transaction(async t => {
      const transactionHost = { transaction: t };

      await this.userModel.create(
          { firstName: 'Abraham', lastName: 'Lincoln' },
          transactionHost,
      );
      await this.userModel.create(
          { firstName: 'John', lastName: 'Boothe' },
          transactionHost,
      );
    });
  } catch (err) {
    // 一旦发生错误,事务会回滚
  }
}

?> 注意Sequelize仅用于开始一个事务。然而,要测试这个类,就需要模拟整个Sequelize对象(它暴露出来的几个方法),因此,我们推荐采用一个帮助工厂类(也就是TransactionRunner)并且定义一个包含仅限于维持事务需要的方法的接口。这一技术让模拟这些方法变得非常直接。

可选地,你可以使用一个Connection对象的回调函数风格的transaction方法(阅读更多)。

async createMany(users: User[]) {
  await this.connection.transaction(async manager => {
    await manager.save(users[0]);
    await manager.save(users[1]);
  });
}

不推荐使用装饰器来控制事务(@Transaction()和@TransactionManager())。

迁移

迁移提供了一个在保存数据库中现有数据的同时增量升级数据库使其与应用中的数据模型保持同步的方法。序列化提供了一个专用CLI命令行工具用于生成、运行以及回滚迁移。

迁移类和Nest应用源码是分开的。他们的生命周期由TypeORM CLI管理,因此,你不能在迁移中使用依赖注入和其他Nest专有特性。在序列化文档 中查看更多关于迁移的内容。

多个数据库

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

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

``` const defaultOptions = { dialect: 'postgres', port: 5432, username: 'user', password: 'password', database: 'db', synchronize: true, };

@Module({

(本文超出站点单文档体积上限,已截断)