logologo
文档仓库
文档仓库
logologo
开始

概述

第一步
控制器
提供者
模块
中间件
异常过滤器
管道
守卫
拦截器
自定义装饰器

基础

自定义提供程序
异步提供者
动态模块
注入作用域
循环依赖
模块引用
懒加载模块
执行上下文
生命周期事件
发现服务
平台无关
单元测试

技术

配置
SQL
Mongo
验证
缓存
序列化
版本控制
任务调度
队列
日志
Cookies
事件
压缩
文件上传
文件流
HTTP 模块
Session
MVC
性能(Fastify)
SSE

安全

认证
授权
加密与哈希
Helmet
CORS
CSRF
速率限制

GraphQL

快速开始
解析器
变更
订阅
标量
指令
接口
联合与枚举
字段中间件
类型映射
插件
复杂度
扩展
CLI 插件
生成SDL
共享模型
其他功能
联邦

WebSocket

网关
异常过滤器
管道
守卫
拦截器
适配器

微服务

基础
Redis
MQTT
NATS
RabbitMQ
Kafka
gRPC
自定义传输
异常过滤器
管道
守卫
拦截器
部署
独立应用程序

CLI

概述
工作区
库
用法
脚本

OpenAPI

介绍
装饰器
类型映射
操作
其他特性
安全
类型与参数
CLI 插件

实用示例

REPL
CRUD生成器
SWC
Passport(认证)
热重载
MikroORM
TypeORM
Mongoose
Sequelize
路由模块
Swagger
健康检查
CQRS
Compodoc
Prisma
Sentry
静态资源
Commander
异步本地存储
Necord
套件(原Automock)

常见问题

Serverless
HTTP 适配器
长连接
全局前缀
原始请求体
混合应用
HTTPS & 多服务器
请求生命周期
错误

开发工具

概述
CI/CD
迁移指南
API参考(官方)

生态与案例

谁在用
精彩资源

支持

支持

社区

贡献者

最后更新于: 2025/11/18 02:11:37

上一页SSE
下一页授权

#认证

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

让我们明确一下需求。对于这个用例,客户端将首先使用用户名和密码进行认证。一旦认证成功,服务器将发出一个 JWT,该 JWT 可以在后续请求中作为承载令牌在授权头中发送,以证明认证。我们还将创建一个受保护的路由,该路由只能被包含有效 JWT 的请求访问。

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

#创建认证模块

我们将首先生成一个 AuthModule,并在其中生成 AuthService 和 AuthController。我们将使用 AuthService 来实现认证逻辑,使用 AuthController 来暴露认证端点。

$ nest g module auth
$ nest g controller 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 = [
   {
     userId: 1,
     username: 'john',
     password: 'changeme',
   },
   {
     userId: 2,
     username: 'maria',
     password: 'guess',
   },
 ];

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

现在,更新 UsersModule 以导出 UsersService,以便在模块外部可用(我们很快就会在 AuthService 中使用它):

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

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

我们的 AuthService 的工作是检索用户并验证密码。我们为此创建一个 signIn() 方法。在下面的代码中,我们使用方便的 ES6 展开运算符从 user 对象中剥离密码属性,然后返回它。这是一种常见做法,当从用户对象返回时,您希望避免包含敏感字段,如密码。

auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { UsersService } from '../users/users.service';

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

 async signIn(username: string, pass: string): Promise<any> {
   const user = await this.usersService.findOne(username);
   if (user?.password !== pass) {
     throw new UnauthorizedException();
   }
   const { password, ...result } = user;
   // TODO: 在这里生成 JWT 并返回它
   //       而不是用户对象
   return result;
 }
}
警告

当然,在真实的应用程序中,您不会以明文形式存储密码。您会使用加了盐的单向哈希算法,如 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 {}

#JWT 令牌

我们准备转向我们认证系统的 JWT 部分。让我们回顾并完善我们的要求:

  • 用户使用用户名/密码进行认证,并返回 JWT 以便在后续调用受保护 API 端点时使用
  • 创建基于有效 JWT 存在的 API 路由保护

我们需要安装额外的包来支持我们的 JWT 要求:

$ npm install --save @nestjs/jwt
提示

@nestjs/jwt 包(见这里)是一个实用程序包,有助于 JWT 操作。

为了保持我们的服务清洁和模块化,我们将在 authService 中处理 JWT 生成。打开 auth/auth.service.ts 文件,注入 JwtService,并更新 signIn 方法以生成 JWT 令牌,如下所示:

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

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

 async signIn(username: string, pass: string): Promise<any> {
   const user = await this.usersService.findOne(username);
   if (user?.password !== pass) {
     throw new UnauthorizedException();
   }
   const payload = { sub: user.userId, username: user.username };
   return {
     access_token: await this.jwtService.signAsync(payload),
   };
 }
}

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

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

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

auth/constants.ts
export const jwtConstants = {
 secret: 'DO NOT USE THIS VALUE. INSTEAD, CREATE A COMPLEX SECRET AND KEEP IT SAFE OUTSIDE OF THE SOURCE CODE.',
};

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

警告

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

现在,打开 auth 文件夹中的 auth.module.ts 并更新它,如下所示:

auth/auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';

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

我们使用 global: true 注册 JwtModule 以便简化。这意味着我们不需要在任何其他地方导入 JwtModule。

我们使用 register() 配置 JwtModule,传入一个配置对象。查看这里了解更多关于 Nest JwtModule 的信息,这里了解更多关于可用配置选项的信息。

#实现认证端点

现在我们可以实现一个简单的 /auth/login 路由,该路由会 POST 用户的凭据以获取 JWT。打开 auth/auth.controller.ts 文件并添加以下代码:

auth/auth.controller.ts
import {
 Body,
 Controller,
 Post,
 HttpCode,
 HttpStatus,
} from '@nestjs/common';
import { AuthService } from './auth.service';

@Controller('auth')
export class AuthController {
 constructor(private authService: AuthService) {}

 @HttpCode(HttpStatus.OK)
 @Post('login')
 signIn(@Body() signInDto: Record<string, any>) {
   return this.authService.signIn(signInDto.username, signInDto.password);
 }
}
提示

理想情况下,不要使用 Record<string, any> 类型。相反,应该创建一个 DTO 类来定义 body 的形状。查看验证章节了解更多信息。

不要忘记将 AuthController 添加到 AuthModule:

auth/auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
import { JwtModule } from '@nestjs/jwt';
import { AuthController } from './auth.controller';
import { jwtConstants } from './constants';

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

让我们使用 cURL 测试我们的路由。使用 nest start 启动应用程序,然后测试路由。

$ # 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

#实现认证守卫

我们现在可以处理最后的要求:通过要求请求中存在有效的 JWT 来保护端点。我们将通过创建一个 AuthGuard 来实现,该守卫可用于保护我们的路由。

auth/auth.guard.ts
import {
 CanActivate,
 ExecutionContext,
 Injectable,
 UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { jwtConstants } from './constants';
import { Request } from 'express';

@Injectable()
export class AuthGuard implements CanActivate {
 constructor(private jwtService: JwtService) {}

 async canActivate(context: ExecutionContext): Promise<boolean> {
   const request = context.switchToHttp().getRequest();
   const token = this.extractTokenFromHeader(request);
   if (!token) {
     throw new UnauthorizedException();
   }
   try {
     const payload = await this.jwtService.verifyAsync(
       token,
       {
         secret: jwtConstants.secret
       }
     );
     // 💡 我们在这里将 payload 分配给 request 对象
     // 以便我们可以在路由处理程序中访问它
     request['user'] = payload;
   } catch {
     throw new UnauthorizedException();
   }
   return true;
 }

 private extractTokenFromHeader(request: Request): string | undefined {
   const [type, token] = request.headers.authorization?.split(' ') ?? [];
   return type === 'Bearer' ? token : undefined;
 }
}

我们现在可以实现一个受保护的路由和一个用于测试我们守卫的注册装饰器。打开 auth/auth.controller.ts 文件并更新它,如下所示:

auth/auth.controller.ts
import {
 Body,
 Controller,
 Get,
 HttpCode,
 HttpStatus,
 Post,
 Request,
 UseGuards
} from '@nestjs/common';
import { AuthGuard } from './auth.guard';
import { AuthService } from './auth.service';

@Controller('auth')
export class AuthController {
 constructor(private authService: AuthService) {}

 @HttpCode(HttpStatus.OK)
 @Post('login')
 signIn(@Body() signInDto: Record<string, any>) {
   return this.authService.signIn(signInDto.username, signInDto.password);
 }

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

将 AuthGuard 应用到 /auth/profile GET 路由,以便它受到保护。

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

$ # GET /auth/profile
$ curl http://localhost:3000/auth/profile
$ # result -> {"statusCode":401,"message":"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.eyJzdWIiOjEsInVzZXJuYW1lIjoiam9obiIsImlhdCI6MTY0MzcyNTUxMywiZXhwIjoxNjQzNzI1NTczfQ.cLLIDjvl_l8OaIsTlqOUVbekX0jZdNEMoDi4tWlisME"}

$ # GET /auth/profile using access_token returned from previous step as bearer code
$ curl http://localhost:3000/auth/profile -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEsInVzZXJuYW1lIjoiam9obiIsImlhdCI6MTY0MzcyNTUxMywiZXhwIjoxNjQzNzI1NTczfQ.cLLIDjvl_l8OaIsTlqOUVbekX0jZdNEMoDi4tWlisME"
$ # result -> {"sub":1,"username":"john","iat":1643725513,"exp":1643725573}

请注意,在 AuthModule 中,我们将 JWT 配置为 60 秒的过期时间。这太短了,处理令牌过期和刷新的细节超出了本文的范围。但是,我们选择它来展示 JWT 的一个重要品质和 @nestjs/jwt 包的行为。如果您在认证后等待 60 秒然后尝试 GET /auth/profile 请求,您将收到 401 Unauthorized 响应。这是因为 @nestjs/jwt 自动检查 JWT 的过期时间,为您省去在应用程序中这样做的麻烦。

我们现在已经完成了我们的 JWT 认证实现。JavaScript 客户端(如 Angular/React/Vue)和其他 JavaScript 应用程序现在可以与我们的 API 服务器进行认证和安全通信。您可以在这里找到本章完整代码。

#启用全局认证

如果绝大多数端点都应该受到保护,您可以将认证守卫注册为全局守卫,而不是在每个控制器顶部使用 @UseGuards() 装饰器,您可以简单地标记哪些路由应该是公共的。

首先,使用以下结构在任何模块中注册 AuthGuard 为全局守卫(例如,在 AuthModule 中):

providers: [
  {
    provide: APP_GUARD,
    useClass: AuthGuard,
  },
],

有了这个,Nest 将自动将 AuthGuard 绑定到所有端点。

现在我们必须提供一种机制来声明路由为公共的。为此,我们可以使用 SetMetadata 装饰器工厂函数创建自定义装饰器。

auth/decorators/public.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

在上面的文件中,我们导出了两个常量。第一个是我们的元数据键,名为 IS_PUBLIC_KEY,第二个是我们的新装饰器本身,我们将称之为 Public(您也可以将其命名为 SkipAuth 或 AllowAnon,任何适合您项目的名称)。

现在我们有了自定义的 @Public() 装饰器,我们可以使用它来装饰任何方法,如下所示:

@Public()
@Get()
findAll() {
  return [];
}

最后,我们需要修改 AuthGuard 以在找到 "isPublic" 元数据时返回 true。为此,我们将使用 Reflector 类(在这里阅读更多)。

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private jwtService: JwtService, private reflector: Reflector) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (isPublic) {
      // 💡 查看此条件
      return true;
    }

    const request = context.switchToHttp().getRequest();
    const token = this.extractTokenFromHeader(request);
    if (!token) {
      throw new UnauthorizedException();
    }
    try {
      const payload = await this.jwtService.verifyAsync(token, {
        secret: jwtConstants.secret,
      });
      // 💡 我们在这里将 payload 分配给 request 对象
      // 以便我们可以在路由处理程序中访问它
      request['user'] = payload;
    } catch {
      throw new UnauthorizedException();
    }
    return true;
  }

  private extractTokenFromHeader(request: Request): string | undefined {
    const [type, token] = request.headers.authorization?.split(' ') ?? [];
    return type === 'Bearer' ? token : undefined;
  }
}

#Passport 集成

Passport 是最流行的 node.js 认证库,被社区广泛了解并成功用于许多生产应用程序。使用 @nestjs/passport 模块将此库与 Nest 应用程序集成很简单。

要了解如何将 Passport 与 NestJS 集成,请查看这里。

#示例

您可以在这里找到本章的完整示例。