第十一课:认证与授权 — 身份验证体系

3 阅读15分钟

覆盖文档:Authentication, Authorization, Passport(Recipe) 前置知识:第7课(Guards、Reflector)、第10课(数据持久化) 源码重点:packages/core/guards/guards-consumer.ts, packages/core/services/reflector.service.ts, packages/common/module-utils/configurable-module.builder.ts


一、JWT 认证

[基础] 本节面向首次实现认证系统的开发者。

1.1 认证流程全景

客户端                                    服务端
  │                                        │
  │  1. POST /auth/login                   │
  │     { username, password }             │
  │  ─────────────────────────────────────→ │
  │                                        │  2. 验证用户名/密码
  │                                        │  3. 签发 JWT(jwtService.signAsync)
  │  4. 返回 { access_token }              │
  │  ←───────────────────────────────────── │
  │                                        │
  │  5. GET /profile                       │
  │     Authorization: Bearer <token>      │
  │  ─────────────────────────────────────→ │
  │                                        │  6. AuthGuard 提取 Bearer token
  │                                        │  7. jwtService.verifyAsync(token)
  │                                        │  8. 将 payload 挂载到 request.user
  │  9. 返回用户信息                        │
  │  ←───────────────────────────────────── │

JWT(JSON Web Token)由三部分组成:Header.Payload.Signature,服务端签发后无需存储,客户端每次请求携带即可。

1.2 安装依赖

npm install @nestjs/jwt

1.3 用户模块(数据准备)

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

// 实际项目中用户数据来自数据库(第10课)
const users = [
  { userId: 1, username: 'john', password: 'changeme' },
  { userId: 2, username: 'maria', password: 'guess' },
];

@Injectable()
export class UsersService {
  async findOne(username: string) {
    return users.find((user) => user.username === username);
  }
}
// users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';

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

1.4 JWT 模块注册

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

@Module({
  imports: [
    UsersModule,
    JwtModule.register({
      global: true,                          // 全局可用,无需每个模块 import
      secret: process.env.JWT_SECRET,        // 从环境变量读取密钥
      signOptions: { expiresIn: '60s' },     // 过期时间
    }),
  ],
  providers: [AuthService],
  controllers: [AuthController],
})
export class AuthModule {}

关键配置说明:

  • global: true:注册为全局模块,JwtService 在任何地方都可注入
  • secret:签名密钥,必须从环境变量读取,绝不能硬编码
  • signOptions.expiresIn:Token 过期时间,支持 '60s''7d''24h' 等格式

1.5 AuthService — 登录签发

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

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

  async signIn(username: string, pass: string) {
    // 1. 查找用户
    const user = await this.usersService.findOne(username);

    // 2. 验证密码(此处为明文对比,实际项目必须用 bcrypt)
    if (user?.password !== pass) {
      throw new UnauthorizedException('用户名或密码错误');
    }

    // 3. 构造 JWT payload(不要放敏感信息)
    const payload = { sub: user.userId, username: user.username };

    // 4. 签发 Token
    return {
      access_token: await this.jwtService.signAsync(payload),
    };
  }
}

安全提醒:JWT payload 是 Base64 编码(不是加密),任何人都能解码读取。绝不要在 payload 中放密码、密钥等敏感数据。

1.6 AuthController — 登录端点

// auth/auth.controller.ts
import { Body, Controller, Post, Get, Request } from '@nestjs/common';
import { AuthService } from './auth.service';
import { Public } from './decorators/public.decorator';

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

  @Public()
  @Post('login')
  signIn(@Body() signInDto: { username: string; password: string }) {
    return this.authService.signIn(signInDto.username, signInDto.password);
  }

  @Get('profile')
  getProfile(@Request() req) {
    return req.user;  // AuthGuard 解析后挂载的用户信息
  }
}

1.7 AuthGuard — Token 验证守卫

// auth/auth.guard.ts
import {
  CanActivate,
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Reflector } from '@nestjs/core';
import { Request } from 'express';
import { IS_PUBLIC_KEY } from './decorators/public.decorator';

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

  async canActivate(context: ExecutionContext): Promise<boolean> {
    // 1. 检查是否标记了 @Public()
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (isPublic) {
      return true;
    }

    // 2. 从 Authorization header 提取 Bearer token
    const request = context.switchToHttp().getRequest();
    const token = this.extractTokenFromHeader(request);
    if (!token) {
      throw new UnauthorizedException('缺少认证令牌');
    }

    // 3. 验证 Token
    try {
      const payload = await this.jwtService.verifyAsync(token, {
        secret: process.env.JWT_SECRET,
      });
      // 4. 将解析后的用户信息挂载到 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;
  }
}

1.8 @Public() 装饰器

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

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

1.9 全局注册 AuthGuard

// app.module.ts
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { AuthModule } from './auth/auth.module';
import { AuthGuard } from './auth/auth.guard';

@Module({
  imports: [AuthModule],
  providers: [
    {
      provide: APP_GUARD,
      useClass: AuthGuard,
    },
  ],
})
export class AppModule {}

通过 APP_GUARD 注册后,所有路由默认需要认证,只有标记了 @Public() 的路由才允许匿名访问。这是"默认安全"的设计理念。

1.10 验证流程

# 1. 登录获取 Token
curl -X POST http://localhost:3000/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"john","password":"changeme"}'
# 返回:{ "access_token": "eyJhbGciOiJIUzI1NiIs..." }

# 2. 携带 Token 访问受保护路由
curl http://localhost:3000/auth/profile \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
# 返回:{ "sub": 1, "username": "john", "iat": ..., "exp": ... }

# 3. 不带 Token 访问
curl http://localhost:3000/auth/profile
# 返回:401 Unauthorized

二、Passport 集成与密码安全

[中阶] 本节面向需要集成多种认证策略的开发者。

2.1 Passport 概述

Passport 是 Node.js 最流行的认证库,提供 500+ 策略(本地、JWT、OAuth、SAML 等)。NestJS 通过 @nestjs/passport 将 Passport 策略封装为 NestJS 风格。

npm install @nestjs/passport passport passport-local passport-jwt
npm install -D @types/passport-local @types/passport-jwt

2.2 Passport 策略模式

┌──────────────────────────────────────────────────────┐
│  NestJS 认证架构                                      │
│                                                      │
│  ┌──────────────┐    ┌──────────────────────┐        │
│  │ AuthGuard    │───→│ PassportStrategy     │        │
│  │ ('local')    │    │ (Strategy)           │        │
│  └──────────────┘    │                      │        │
│                      │  + validate(...)     │        │
│  ┌──────────────┐    │    ↓ 返回用户对象    │        │
│  │ AuthGuard    │───→│    ↓ 自动挂载到      │        │
│  │ ('jwt')      │    │    ↓ request.user    │        │
│  └──────────────┘    └──────────────────────┘        │
│                                                      │
│  每个策略只需实现 validate() 方法:                     │
│  - LocalStrategy: validate(username, password)       │
│  - JwtStrategy:   validate(payload)                  │
│  - GoogleStrategy: validate(accessToken, profile)    │
└──────────────────────────────────────────────────────┘

2.3 LocalStrategy — 用户名/密码验证

// auth/strategies/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();  // 默认期望 body 中有 username 和 password 字段
    // 自定义字段名:super({ usernameField: 'email' })
  }

  async validate(username: string, password: string): Promise<any> {
    const user = await this.authService.validateUser(username, password);
    if (!user) {
      throw new UnauthorizedException('用户名或密码错误');
    }
    return user;  // 返回值自动挂载到 request.user
  }
}

2.4 JwtStrategy — Token 自动解码

// auth/strategies/jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),  // 从 Bearer 提取
      ignoreExpiration: false,                                     // 不忽略过期
      secretOrKey: process.env.JWT_SECRET,                        // 验证密钥
    });
  }

  async validate(payload: any) {
    // payload 已经被 passport-jwt 解码验证
    // 返回值直接挂载到 request.user
    return { userId: payload.sub, username: payload.username };
  }
}

2.5 在控制器中使用 Passport Guard

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

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

  // LocalStrategy:自动从 body 提取 username/password 并调用 validate()
  @UseGuards(AuthGuard('local'))
  @Post('login')
  async login(@Request() req) {
    // req.user 已由 LocalStrategy.validate() 填充
    return this.authService.login(req.user);
  }

  // JwtStrategy:自动从 Header 提取并验证 JWT
  @UseGuards(AuthGuard('jwt'))
  @Get('profile')
  getProfile(@Request() req) {
    return req.user;
  }
}

2.6 AuthModule 整合

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

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

2.7 密码安全 — bcrypt 哈希

核心原则:永远不要明文存储密码。

npm install bcrypt
npm install -D @types/bcrypt
import * as bcrypt from 'bcrypt';

// 注册时:哈希密码
async register(username: string, plainPassword: string) {
  const saltRounds = 10;
  const hashedPassword = await bcrypt.hash(plainPassword, saltRounds);
  
  // 存储 hashedPassword 到数据库
  return this.usersRepository.save({
    username,
    password: hashedPassword,  // 存储哈希值,不是明文
  });
}

// 登录时:验证密码
async validateUser(username: string, plainPassword: string) {
  const user = await this.usersService.findOne(username);
  if (!user) {
    return null;
  }

  // bcrypt.compare 内部提取 salt 并重新计算哈希
  const isMatch = await bcrypt.compare(plainPassword, user.password);
  if (!isMatch) {
    return null;
  }

  // 返回用户信息(不含密码)
  const { password, ...result } = user;
  return result;
}

bcrypt 的 saltRounds(默认 10)决定计算强度:

  • 每增加 1,计算时间翻倍
  • 10 ≈ 10ms,12 ≈ 40ms,14 ≈ 160ms
  • 推荐 10-12,兼顾安全与性能

2.8 Passport vs 手动 AuthGuard 对比

特性手动 AuthGuardPassport 集成
依赖@nestjs/jwt@nestjs/passport + passport + 策略包
代码量较少较多(需要 Strategy 类)
灵活性完全控制遵循 Passport 约定
多策略需自行组合内置策略切换 AuthGuard('xxx')
OAuth/SAML需自行实现500+ 现成策略
推荐场景仅 JWT 认证多种认证方式混合

三、授权(RBAC)

[中阶] 本节面向需要实现角色控制的开发者。

3.1 认证 vs 授权

认证(Authentication):你是谁?— 验证身份
授权(Authorization):你能做什么?— 验证权限

请求流程:
  Request → AuthGuard(认证) → RolesGuard(授权) → Controller
                 ↑                    ↑
            验证 Token           检查角色/权限

3.2 角色枚举与装饰器

// auth/enums/role.enum.ts
export enum Role {
  User = 'user',
  Admin = 'admin',
  Editor = 'editor',
}
// auth/decorators/roles.decorator.ts
import { Reflector } from '@nestjs/core';
import { Role } from '../enums/role.enum';

// 使用 Reflector.createDecorator 创建强类型装饰器
export const Roles = Reflector.createDecorator<Role[]>();

3.3 RolesGuard 实现

// auth/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Role } from '../enums/role.enum';
import { Roles } from '../decorators/roles.decorator';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    // 1. 读取路由上的角色要求
    const requiredRoles = this.reflector.getAllAndOverride<Role[]>(Roles, [
      context.getHandler(),    // 当前方法
      context.getClass(),      // 当前控制器类
    ]);

    // 2. 没有角色要求,放行
    if (!requiredRoles) {
      return true;
    }

    // 3. 从 request.user 获取用户角色(由 AuthGuard 填充)
    const { user } = context.switchToHttp().getRequest();

    // 4. 检查用户是否拥有所需角色之一
    return requiredRoles.some((role) => user.roles?.includes(role));
  }
}

3.4 使用角色保护路由

@Controller('cats')
export class CatsController {
  @Post()
  @Roles([Role.Admin])  // 只有管理员能创建
  create(@Body() createCatDto: CreateCatDto) {
    return this.catsService.create(createCatDto);
  }

  @Get()
  @Roles([Role.User, Role.Admin])  // 普通用户和管理员都能查看
  findAll() {
    return this.catsService.findAll();
  }

  @Delete(':id')
  @Roles([Role.Admin])  // 只有管理员能删除
  remove(@Param('id') id: string) {
    return this.catsService.remove(id);
  }
}

3.5 全局注册 RolesGuard

// app.module.ts
providers: [
  { provide: APP_GUARD, useClass: AuthGuard },    // 先认证
  { provide: APP_GUARD, useClass: RolesGuard },   // 再授权
],

Guard 的执行顺序:按注册顺序依次执行。先 AuthGuard 认证用户身份,再 RolesGuard 检查权限。

3.6 getAllAndOverride vs getAllAndMerge

// 控制器级设置
@Roles([Role.User])
@Controller('cats')
export class CatsController {
  // 方法级设置
  @Roles([Role.Admin])
  @Delete(':id')
  remove() {}
}

// getAllAndOverride:方法级覆盖类级
// 结果:[Role.Admin](只要求 Admin)
const roles = this.reflector.getAllAndOverride(Roles, [handler, class]);

// getAllAndMerge:合并方法级和类级
// 结果:[Role.Admin, Role.User](Admin 或 User 都可以)
const roles = this.reflector.getAllAndMerge(Roles, [handler, class]);

选择策略:

  • getAllAndOverride:方法级可以收紧放宽权限(常用)
  • getAllAndMerge:方法级权限在类级基础上叠加

四、CASL 细粒度权限

[高阶] 本节面向需要字段级权限控制的开发者。

4.1 RBAC 的局限

RBAC(Role-Based Access Control)只能回答"这个角色能不能访问这个端点"。但实际业务中需要更细粒度的控制:

  • 用户只能编辑自己的文章
  • 管理员可以读取所有文章,但只能删除草稿状态的文章
  • 编辑器可以修改文章内容,但不能修改作者字段

这需要 ABAC(Attribute-Based Access Control),CASL 是 JavaScript 生态中最流行的实现。

npm install @casl/ability

4.2 CaslAbilityFactory

// casl/casl-ability.factory.ts
import {
  AbilityBuilder,
  createMongoAbility,
  MongoAbility,
  InferSubjects,
  ExtractSubjectType,
} from '@casl/ability';
import { Injectable } from '@nestjs/common';
import { Article } from '../articles/entities/article.entity';
import { User } from '../users/entities/user.entity';
import { Role } from '../auth/enums/role.enum';

// 定义动作
export enum Action {
  Manage = 'manage',  // 通配符,代表所有动作
  Create = 'create',
  Read = 'read',
  Update = 'update',
  Delete = 'delete',
}

type Subjects = InferSubjects<typeof Article | typeof User> | 'all';
export type AppAbility = MongoAbility<[Action, Subjects]>;

@Injectable()
export class CaslAbilityFactory {
  createForUser(user: User): AppAbility {
    const { can, cannot, build } = new AbilityBuilder<AppAbility>(
      createMongoAbility,
    );

    if (user.roles.includes(Role.Admin)) {
      // 管理员:可以管理所有资源
      can(Action.Manage, 'all');
      // 但不能删除已发布的文章
      cannot(Action.Delete, Article, { isPublished: true });
    } else {
      // 普通用户:只能读取所有文章
      can(Action.Read, 'all');
      // 可以创建文章
      can(Action.Create, Article);
      // 只能更新自己的文章
      can(Action.Update, Article, { authorId: user.userId });
      // 只能删除自己的草稿
      can(Action.Delete, Article, { authorId: user.userId, isPublished: false });
    }

    return build({
      detectSubjectType: (item) =>
        item.constructor as ExtractSubjectType<Subjects>,
    });
  }
}

4.3 Policy Handler 接口

// casl/interfaces/policy-handler.interface.ts
import { AppAbility } from '../casl-ability.factory';

// 接口式
export interface IPolicyHandler {
  handle(ability: AppAbility): boolean;
}

// 回调式
type PolicyHandlerCallback = (ability: AppAbility) => boolean;

export type PolicyHandler = IPolicyHandler | PolicyHandlerCallback;

4.4 @CheckPolicies() 装饰器

// casl/decorators/check-policies.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { PolicyHandler } from '../interfaces/policy-handler.interface';

export const CHECK_POLICIES_KEY = 'check_policy';
export const CheckPolicies = (...handlers: PolicyHandler[]) =>
  SetMetadata(CHECK_POLICIES_KEY, handlers);

4.5 PoliciesGuard

// casl/guards/policies.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { CaslAbilityFactory, AppAbility } from '../casl-ability.factory';
import { CHECK_POLICIES_KEY } from '../decorators/check-policies.decorator';
import { PolicyHandler } from '../interfaces/policy-handler.interface';

@Injectable()
export class PoliciesGuard implements CanActivate {
  constructor(
    private readonly reflector: Reflector,
    private readonly caslAbilityFactory: CaslAbilityFactory,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const policyHandlers =
      this.reflector.get<PolicyHandler[]>(
        CHECK_POLICIES_KEY,
        context.getHandler(),
      ) || [];

    const { user } = context.switchToHttp().getRequest();
    const ability = this.caslAbilityFactory.createForUser(user);

    return policyHandlers.every((handler) =>
      this.execPolicyHandler(handler, ability),
    );
  }

  private execPolicyHandler(handler: PolicyHandler, ability: AppAbility) {
    if (typeof handler === 'function') {
      return handler(ability);
    }
    return handler.handle(ability);
  }
}

4.6 在控制器中使用

// 回调函数式(简单场景)
@Get()
@CheckPolicies((ability: AppAbility) => ability.can(Action.Read, Article))
findAll() {
  return this.articlesService.findAll();
}

// 类实现式(复杂场景,可注入依赖)
export class ReadArticlePolicyHandler implements IPolicyHandler {
  handle(ability: AppAbility) {
    return ability.can(Action.Read, Article);
  }
}

@Get()
@CheckPolicies(new ReadArticlePolicyHandler())
findAll() {
  return this.articlesService.findAll();
}

4.7 字段级权限控制

// 在 Service 中根据能力过滤字段
async findOne(id: number, ability: AppAbility) {
  const article = await this.articlesRepository.findOneBy({ id });

  // 检查能否读取特定字段
  if (ability.can(Action.Read, article)) {
    // 如果不能读取 authorEmail,过滤掉
    if (!ability.can(Action.Read, article, 'authorEmail')) {
      const { authorEmail, ...safeArticle } = article;
      return safeArticle;
    }
    return article;
  }

  throw new ForbiddenException('无权访问该文章');
}

4.8 RBAC vs CASL 对比

特性RBAC(RolesGuard)CASL(PoliciesGuard)
粒度端点级(整个路由)资源级 + 字段级
条件无条件(有角色即可)支持属性条件(如 authorId
复杂度中-高
适用场景简单的管理后台多角色、资源所有权、复杂业务
维护成本中(需维护 AbilityFactory)
推荐中小型项目首选大型项目或权限复杂场景

五、认证源码解析

[资深] 本节面向希望理解框架内部机制的开发者。

5.1 AuthGuard 与 Reflector 的协作

自定义 AuthGuard 中最关键的设计是通过 Reflector 读取 @Public() 元数据:

// Reflector.getAllAndOverride 的工作原理
// 文件:packages/core/services/reflector.service.ts

getAllAndOverride<TResult>(
  decorator: ReflectableDecorator,
  targets: (Type | Function)[],   // [handler, class]
): TResult | undefined {
  // 依次从 targets 中查找元数据
  for (const target of targets) {
    const result = this.get(decorator, target);
    if (result !== undefined) {
      return result;  // 找到第一个非 undefined 的值就返回
    }
  }
  return undefined;
}

这意味着 getAllAndOverride 实现了方法级优先的查找策略:先查方法上的装饰器,再查类上的装饰器,找到即停。

5.2 GuardsConsumer — Guard 链执行

// 文件:packages/core/guards/guards-consumer.ts
export class GuardsConsumer {
  public async tryActivate(
    guards: CanActivate[],
    args: unknown[],
    instance: Controller,
    callback: (...args: unknown[]) => unknown,
    type?: TContext,
  ): Promise<boolean> {
    if (!guards || isEmpty(guards)) {
      return true;  // 无 Guard 直接放行
    }
    const context = this.createContext(args, instance, callback);
    context.setType(type);

    // 顺序执行每个 Guard
    for (const guard of guards) {
      const result = guard.canActivate(context);
      if (typeof result === 'boolean') {
        if (!result) return false;   // 同步返回 false,立即拒绝
        continue;
      }
      // 异步结果(Promise 或 Observable)
      if (await this.pickResult(result)) {
        continue;
      }
      return false;
    }
    return true;  // 所有 Guard 都通过
  }
}

关键设计:

  • Guard 链是串行执行的,任何一个返回 false 即短路
  • 支持同步、Promise、Observable 三种返回类型
  • ExecutionContextHost 为每个 Guard 提供统一的上下文

5.3 @nestjs/jwt 与 ConfigurableModuleBuilder

@nestjs/jwtJwtModuleConfigurableModuleBuilder 模式的典型应用:

// ConfigurableModuleBuilder 自动生成 register() 和 registerAsync()
// 文件:packages/common/module-utils/configurable-module.builder.ts

export class ConfigurableModuleBuilder<ModuleOptions> {
  // 自动生成的静态方法签名
  // ModuleCls.register(options)        — 同步配置
  // ModuleCls.registerAsync(options)   — 异步配置(支持 useFactory/useClass/useExisting)
}

JwtModule.register() 的内部流程:

  1. 接收配置项(secret, signOptions 等)
  2. 创建一个 DynamicModule,注册 JwtService 为 Provider
  3. JwtService 注入配置项,提供 sign() / verify() / signAsync() / verifyAsync() 方法

5.4 Passport 适配器模式

@nestjs/passport 通过适配器模式将 Passport 策略封装为 NestJS Provider:

┌─────────────────────────────────────────────────┐
│  PassportStrategy(Strategy)                      │
│                                                  │
│  1. 创建一个 mixin 类,继承传入的 Strategy        │
│  2. 在构造函数中调用 super(options)               │
│  3. 将 validate() 方法绑定到 Passport 的回调      │
│  4. passport.use() 注册策略                      │
│                                                  │
│  AuthGuard('strategy-name')                      │
│                                                  │
│  1. 返回一个实现 CanActivate 的 Guard 类          │
│  2. canActivate() 中调用 passport.authenticate()  │
│  3. 认证成功:将 user 挂载到 request               │
│  4. 认证失败:抛出 UnauthorizedException          │
└─────────────────────────────────────────────────┘

PassportStrategy 是一个 mixin 函数(高阶类工厂),它接受任何 Passport 策略类,返回一个新类。这使得 NestJS 的策略类既是 Passport 策略,又是 NestJS @Injectable() Provider。


六、认证架构设计

[架构] 本节面向技术负责人和架构师。

6.1 认证 vs 授权的边界

维度认证(Authentication)授权(Authorization)
回答问题你是谁?你能做什么?
时机登录时、每次请求时每次访问资源时
实现JWT / Session / OAuthRBAC / ABAC / CASL
失败响应401 Unauthorized403 Forbidden
NestJS 组件AuthGuardRolesGuard / PoliciesGuard

6.2 Access Token + Refresh Token 策略

┌──────────────────────────────────────────────────────────────┐
│  双 Token 架构                                                │
│                                                              │
│  Access Token(短期,15min-1h)                               │
│  ├─ 存储位置:内存 / httpOnly cookie                          │
│  ├─ 每次请求携带                                              │
│  ├─ 无状态验证(无需查数据库)                                  │
│  └─ 过期后需要用 Refresh Token 换新                            │
│                                                              │
│  Refresh Token(长期,7d-30d)                                │
│  ├─ 存储位置:httpOnly + secure cookie                        │
│  ├─ 仅用于换取新 Access Token                                 │
│  ├─ 服务端存储(数据库/Redis),可主动吊销                       │
│  └─ 一次使用后轮换(Rotation),防止重放攻击                     │
│                                                              │
│  流程:                                                       │
│  1. 登录 → 签发 Access Token + Refresh Token                 │
│  2. Access Token 过期 → POST /auth/refresh                   │
│  3. 验证 Refresh Token → 签发新 Access Token + 新 Refresh Token│
│  4. 旧 Refresh Token 立即失效                                 │
└──────────────────────────────────────────────────────────────┘
// Refresh Token 实现示例
@Post('refresh')
async refresh(@Body() body: { refreshToken: string }) {
  // 1. 验证 Refresh Token 是否存在于数据库
  const stored = await this.tokenService.findRefreshToken(body.refreshToken);
  if (!stored || stored.isRevoked || stored.expiresAt < new Date()) {
    throw new UnauthorizedException('Refresh Token 无效');
  }

  // 2. 吊销旧 Refresh Token
  await this.tokenService.revokeRefreshToken(body.refreshToken);

  // 3. 签发新的 Token 对
  const user = await this.usersService.findById(stored.userId);
  return {
    access_token: await this.jwtService.signAsync(
      { sub: user.id, username: user.username },
      { expiresIn: '15m' },
    ),
    refresh_token: await this.tokenService.createRefreshToken(user.id),
  };
}

6.3 零信任架构

在微服务场景下,每个服务独立验证 Token,不依赖网关的认证结果:

客户端 → API Gateway → Service A → Service B
           │              │            │
        验证 JWT       验证 JWT     验证 JWT
        (可选)         (必须)       (必须)

零信任原则:

  • 每个服务都有 AuthGuard,独立验证 Token
  • 服务间通信也携带 Token(或使用 Service-to-Service Token)
  • 不信任网络位置(内网也需要认证)
  • 最小权限原则:每个服务只拥有完成工作所需的最小权限

6.4 敏感操作二次验证

// 二次验证装饰器
export const RequireReauth = () => SetMetadata('requireReauth', true);

// 二次验证 Guard
@Injectable()
export class ReauthGuard implements CanActivate {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const requireReauth = this.reflector.get<boolean>(
      'requireReauth',
      context.getHandler(),
    );
    if (!requireReauth) return true;

    const request = context.switchToHttp().getRequest();
    const reauthToken = request.headers['x-reauth-token'];

    // 验证二次认证令牌(短效,如 5 分钟)
    if (!reauthToken) {
      throw new ForbiddenException('敏感操作需要二次验证');
    }

    return this.authService.verifyReauthToken(reauthToken);
  }
}

// 使用
@Delete('account')
@RequireReauth()
deleteAccount(@Request() req) {
  return this.usersService.delete(req.user.userId);
}

6.5 认证方案选型

场景推荐方案
SPA + REST APIJWT(Access + Refresh Token)
传统 Web 应用(SSR)Session + Cookie
移动端 AppJWT + Refresh Token(长期)
第三方登录OAuth 2.0 + Passport
微服务内部通信短期 JWT 或 mTLS
企业 SSOSAML / OpenID Connect

七、课后实践

练习 1:完整 JWT 认证流程(基础)

实现以下功能:

  1. 创建 AuthModuleUsersModule
  2. POST /auth/login:验证用户名/密码,签发 JWT
  3. GET /auth/profile:携带 Bearer Token 返回用户信息
  4. 全局注册 AuthGuard,标记 /auth/login@Public()
# 测试
curl -X POST http://localhost:3000/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"john","password":"changeme"}'

练习 2:RBAC 角色控制(中阶)

在练习 1 基础上:

  1. 定义 Role 枚举:UserAdminEditor
  2. 创建 @Roles() 装饰器和 RolesGuard
  3. CatsController 上实现:
    • GET /cats:所有角色可访问
    • POST /cats:仅 AdminEditor
    • DELETE /cats/:id:仅 Admin

练习 3:Passport 双策略认证(中阶)

  1. 安装 @nestjs/passportpassport-localpassport-jwt
  2. 实现 LocalStrategyJwtStrategy
  3. POST /auth/login 使用 AuthGuard('local')
  4. GET /auth/profile 使用 AuthGuard('jwt')

练习 4:bcrypt 密码安全(中阶)

  1. 添加 POST /auth/register 端点
  2. 注册时使用 bcrypt.hash() 存储密码
  3. 登录时使用 bcrypt.compare() 验证密码
  4. 确认数据库中不存在明文密码

练习 5:阅读 Guard 源码(资深)

打开以下源码文件,回答问题:

  1. packages/core/guards/guards-consumer.ts:Guard 链如何短路?
  2. packages/core/services/reflector.service.tsgetAllAndOverridegetAllAndMerge 的区别在源码中如何体现?
  3. Reflector.createDecorator() 内部使用了什么作为 metadata key?

八、本课知识点总结

知识点要点
JWT 认证流程登录 → signAsync → Bearer Token → verifyAsync → request.user
JwtModuleregister({ global, secret, signOptions }),secret 必须环境变量
AuthGuard提取 Bearer → verifyAsync → 挂载 user;@Public() 跳过认证
APP_GUARD全局注册 Guard,默认所有路由受保护
PassportPassportStrategy(Strategy) + validate() 方法
密码安全bcrypt.hash() 存储,bcrypt.compare() 验证,永不明文存储
RBAC@Roles() + RolesGuard + Reflector.getAllAndOverride
CASLCaslAbilityFactory + PoliciesGuard + @CheckPolicies(),支持属性/字段级权限
Token 刷新Access Token(短期) + Refresh Token(长期,服务端存储),轮换防重放
源码入口guards-consumer.ts(Guard 链)、reflector.service.ts(元数据读取)

下一课预告:第十二课将学习安全加固,包括 Helmet 安全头、CORS 跨域、CSRF 防护、限流策略、加密与哈希算法,构建完整的纵深防御体系。