覆盖文档: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 对比
| 特性 | 手动 AuthGuard | Passport 集成 |
|---|---|---|
| 依赖 | 仅 @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/jwt 的 JwtModule 是 ConfigurableModuleBuilder 模式的典型应用:
// 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() 的内部流程:
- 接收配置项(
secret,signOptions等) - 创建一个
DynamicModule,注册JwtService为 Provider 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 / OAuth | RBAC / ABAC / CASL |
| 失败响应 | 401 Unauthorized | 403 Forbidden |
| NestJS 组件 | AuthGuard | RolesGuard / 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 API | JWT(Access + Refresh Token) |
| 传统 Web 应用(SSR) | Session + Cookie |
| 移动端 App | JWT + Refresh Token(长期) |
| 第三方登录 | OAuth 2.0 + Passport |
| 微服务内部通信 | 短期 JWT 或 mTLS |
| 企业 SSO | SAML / OpenID Connect |
七、课后实践
练习 1:完整 JWT 认证流程(基础)
实现以下功能:
- 创建
AuthModule、UsersModule POST /auth/login:验证用户名/密码,签发 JWTGET /auth/profile:携带 Bearer Token 返回用户信息- 全局注册
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 基础上:
- 定义
Role枚举:User、Admin、Editor - 创建
@Roles()装饰器和RolesGuard - 在
CatsController上实现:GET /cats:所有角色可访问POST /cats:仅Admin和EditorDELETE /cats/:id:仅Admin
练习 3:Passport 双策略认证(中阶)
- 安装
@nestjs/passport、passport-local、passport-jwt - 实现
LocalStrategy和JwtStrategy POST /auth/login使用AuthGuard('local')GET /auth/profile使用AuthGuard('jwt')
练习 4:bcrypt 密码安全(中阶)
- 添加
POST /auth/register端点 - 注册时使用
bcrypt.hash()存储密码 - 登录时使用
bcrypt.compare()验证密码 - 确认数据库中不存在明文密码
练习 5:阅读 Guard 源码(资深)
打开以下源码文件,回答问题:
packages/core/guards/guards-consumer.ts:Guard 链如何短路?packages/core/services/reflector.service.ts:getAllAndOverride和getAllAndMerge的区别在源码中如何体现?Reflector.createDecorator()内部使用了什么作为 metadata key?
八、本课知识点总结
| 知识点 | 要点 |
|---|---|
| JWT 认证流程 | 登录 → signAsync → Bearer Token → verifyAsync → request.user |
| JwtModule | register({ global, secret, signOptions }),secret 必须环境变量 |
| AuthGuard | 提取 Bearer → verifyAsync → 挂载 user;@Public() 跳过认证 |
| APP_GUARD | 全局注册 Guard,默认所有路由受保护 |
| Passport | PassportStrategy(Strategy) + validate() 方法 |
| 密码安全 | bcrypt.hash() 存储,bcrypt.compare() 验证,永不明文存储 |
| RBAC | @Roles() + RolesGuard + Reflector.getAllAndOverride |
| CASL | CaslAbilityFactory + PoliciesGuard + @CheckPolicies(),支持属性/字段级权限 |
| Token 刷新 | Access Token(短期) + Refresh Token(长期,服务端存储),轮换防重放 |
| 源码入口 | guards-consumer.ts(Guard 链)、reflector.service.ts(元数据读取) |
下一课预告:第十二课将学习安全加固,包括 Helmet 安全头、CORS 跨域、CSRF 防护、限流策略、加密与哈希算法,构建完整的纵深防御体系。