第十二课:安全加固 — 防御纵深

4 阅读12分钟

覆盖文档:Encryption and Hashing, Helmet, CORS, CSRF Protection, Rate Limiting 前置知识:第11课(认证与授权) 源码重点:Node.js crypto 模块, @nestjs/throttler 限流机制


一、Helmet 与 CORS

[基础] 本节面向首次配置生产安全的开发者。

1.1 Helmet — 安全响应头

Helmet 是一个 Express 中间件,通过设置 15+ HTTP 响应头来防御常见的 Web 攻击。

npm install helmet
// main.ts
import { NestFactory } from '@nestjs/core';
import helmet from 'helmet';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Helmet 必须在其他中间件之前注册
  app.use(helmet());

  await app.listen(3000);
}
bootstrap();

Helmet 设置的主要安全头:

响应头作用防御
Content-Security-Policy限制资源加载来源XSS、数据注入
X-Content-Type-Options: nosniff禁止 MIME 类型嗅探MIME 混淆攻击
X-Frame-Options: SAMEORIGIN禁止被嵌入 iframe点击劫持
X-XSS-Protection启用浏览器 XSS 过滤器反射型 XSS
Strict-Transport-Security强制 HTTPS中间人攻击
X-DNS-Prefetch-Control: off禁止 DNS 预解析DNS 信息泄露
X-Download-Options: noopen禁止 IE 直接打开下载文件执行攻击
X-Permitted-Cross-Domain-Policies限制 Flash/PDF 跨域跨域数据读取
Referrer-Policy控制 Referer 头发送隐私泄露

关键规则app.use(helmet()) 必须在其他 app.use() 之前调用,否则后续中间件设置的 header 可能被 Helmet 覆盖。

1.2 Fastify 平台使用 Helmet

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

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

  // Fastify 使用 register 而不是 use
  await app.register(helmet);

  await app.listen(3000, '0.0.0.0');
}
bootstrap();

1.3 自定义 Helmet 配置

app.use(
  helmet({
    // 自定义 CSP 指令
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        styleSrc: ["'self'", "'unsafe-inline'"],  // 允许内联样式
        imgSrc: ["'self'", 'data:', 'https:'],    // 允许 HTTPS 图片
        scriptSrc: ["'self'"],                      // 只允许同源脚本
      },
    },
    // 开发环境可能需要关闭某些头
    crossOriginEmbedderPolicy: false,
  }),
);

1.4 CORS — 跨域资源共享

CORS(Cross-Origin Resource Sharing)控制哪些域名可以访问你的 API。

浏览器安全策略:
  http://frontend.com  →  http://api.com/users
       (源 A)                   (源 B)

  同源策略默认阻止跨域请求,CORS 是"放行白名单"

快速启用

// 方式 1:创建时启用
const app = await NestFactory.create(AppModule, { cors: true });

// 方式 2:调用 enableCors
const app = await NestFactory.create(AppModule);
app.enableCors();

生产级配置

app.enableCors({
  // 允许的来源(白名单)
  origin: ['https://frontend.example.com', 'https://admin.example.com'],

  // 允许的 HTTP 方法
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],

  // 允许的请求头
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],

  // 暴露给前端的响应头
  exposedHeaders: ['X-Total-Count', 'X-Page-Size'],

  // 允许携带 Cookie/凭证
  credentials: true,

  // 预检请求缓存时间(秒)
  maxAge: 3600,
});

动态 Origin 配置

app.enableCors({
  origin: (origin, callback) => {
    // 允许无 Origin 的请求(如服务端请求、Postman)
    if (!origin) {
      return callback(null, true);
    }

    const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [];
    if (allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('CORS 策略不允许该来源'));
    }
  },
});

1.5 CORS 配置要点

配置项开发环境生产环境
origintrue(允许所有)明确的域名白名单
credentialstruetrue(若需要 Cookie)
methods全部按需配置
maxAge不限3600(1小时)

安全提醒:生产环境绝不能使用 origin: trueorigin: '*'。这会允许任何网站调用你的 API,等同于关闭跨域保护。


二、CSRF 防护与限流

[中阶] 本节面向需要防御 CSRF 和暴力攻击的开发者。

2.1 CSRF 攻击原理

CSRF(Cross-Site Request Forgery)跨站请求伪造:

  1. 用户登录 bank.com,浏览器存储了 Cookie
  2. 用户访问 evil.com
  3. evil.com 包含:<img src="https://bank.com/transfer?to=hacker&amount=10000">
  4. 浏览器自动携带 bank.com 的 Cookie 发送请求
  5. bank.com 收到"合法"请求,执行转账

  防御:服务端验证请求确实来自自己的前端,而非第三方页面

2.2 csrf-csrf 双重提交 Cookie

npm install csrf-csrf cookie-parser
// main.ts
import { NestFactory } from '@nestjs/core';
import * as cookieParser from 'cookie-parser';
import { doubleCsrf } from 'csrf-csrf';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // 1. Cookie 解析器(csrf-csrf 依赖)
  app.use(cookieParser());

  // 2. 配置 CSRF 保护
  const {
    doubleCsrfProtection,     // 中间件
    generateToken,             // Token 生成函数
  } = doubleCsrf({
    getSecret: () => process.env.CSRF_SECRET,  // 用于签名的密钥
    cookieName: '__csrf',                       // Cookie 名称
    cookieOptions: {
      httpOnly: true,
      sameSite: 'strict',
      secure: process.env.NODE_ENV === 'production',
    },
    getTokenFromRequest: (req) => req.headers['x-csrf-token'],  // 从请求头提取 token
  });

  // 3. 全局启用 CSRF 保护
  app.use(doubleCsrfProtection);

  await app.listen(3000);
}
bootstrap();

双重提交 Cookie 的工作原理:

  1. 服务端生成 CSRF Token,同时存入 Cookie 和响应中
  2. 前端将 Token 放入请求头 X-CSRF-Token
  3. 服务端验证:Cookie 中的 Token === 请求头中的 Token
  4. 攻击者无法读取其他域的 Cookie,因此无法构造合法的请求头

2.3 @nestjs/throttler — 请求限流

限流(Rate Limiting)防止暴力攻击和滥用。

npm install @nestjs/throttler

基本配置

// app.module.ts
import { Module } from '@nestjs/common';
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
import { APP_GUARD } from '@nestjs/core';

@Module({
  imports: [
    ThrottlerModule.forRoot([
      {
        name: 'short',
        ttl: 1000,      // 1 秒内
        limit: 3,        // 最多 3 次请求
      },
      {
        name: 'medium',
        ttl: 10000,      // 10 秒内
        limit: 20,       // 最多 20 次请求
      },
      {
        name: 'long',
        ttl: 60000,      // 1 分钟内
        limit: 100,      // 最多 100 次请求
      },
    ]),
  ],
  providers: [
    {
      provide: APP_GUARD,
      useClass: ThrottlerGuard,  // 全局限流守卫
    },
  ],
})
export class AppModule {}

多策略同时生效:请求必须满足所有限流规则。如上配置中,一个请求必须同时满足 1秒3次、10秒20次、1分钟100次的限制。

时间助手

import { seconds, minutes, hours } from '@nestjs/throttler';

ThrottlerModule.forRoot([
  {
    name: 'short',
    ttl: seconds(1),      // 更语义化
    limit: 3,
  },
  {
    name: 'long',
    ttl: minutes(1),
    limit: 100,
  },
]);

2.4 跳过与自定义限流

import { SkipThrottle, Throttle } from '@nestjs/throttler';

@Controller('cats')
export class CatsController {
  // 跳过该路由的限流
  @SkipThrottle()
  @Get('health')
  healthCheck() {
    return { status: 'ok' };
  }

  // 跳过特定策略的限流
  @SkipThrottle({ short: true })  // 跳过 short 策略,medium 和 long 仍生效
  @Get()
  findAll() {
    return this.catsService.findAll();
  }

  // 自定义该路由的限流规则(覆盖全局配置)
  @Throttle({ short: { ttl: 1000, limit: 1 } })  // 更严格:1秒1次
  @Post()
  create(@Body() dto: CreateCatDto) {
    return this.catsService.create(dto);
  }
}

// 类级跳过:整个控制器不限流
@SkipThrottle()
@Controller('internal')
export class InternalController {}

2.5 分布式限流 — Redis 存储

默认 ThrottlerGuard 使用内存存储,多实例部署时每个实例独立计数。要实现全局统一限流,需要 Redis 作为存储后端。

npm install @nestjs/throttler-storage-redis ioredis
import { ThrottlerModule } from '@nestjs/throttler';
import { ThrottlerStorageRedisService } from '@nestjs/throttler-storage-redis';
import Redis from 'ioredis';

@Module({
  imports: [
    ThrottlerModule.forRoot({
      throttlers: [
        { name: 'short', ttl: seconds(1), limit: 3 },
        { name: 'long', ttl: minutes(1), limit: 100 },
      ],
      storage: new ThrottlerStorageRedisService(
        new Redis({
          host: 'localhost',
          port: 6379,
        }),
      ),
    }),
  ],
})
export class AppModule {}

2.6 限流响应

当请求超出限流,ThrottlerGuard 自动返回:

{
  "statusCode": 429,
  "message": "ThrottlerException: Too Many Requests"
}

响应头中包含限流信息:

X-RateLimit-Limit: 3
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1620000000
Retry-After: 1

2.7 自定义限流逻辑

// 基于用户身份的差异化限流
@Injectable()
export class CustomThrottlerGuard extends ThrottlerGuard {
  // 自定义追踪键(默认按 IP)
  protected async getTracker(req: Record<string, any>): Promise<string> {
    // 已认证用户:按用户 ID 限流(每用户独立计数)
    if (req.user) {
      return req.user.userId.toString();
    }
    // 未认证用户:按 IP 限流
    return req.ip;
  }

  // 跳过特定条件
  protected async shouldSkip(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    // 内部服务调用不限流
    return request.headers['x-internal-service'] === 'true';
  }
}

三、加密与哈希

[高阶] 本节面向需要实现数据加密方案的开发者。

3.1 加密 vs 哈希

┌───────────────────────────────────────────────────────┐
│  加密(Encryption)                                    │
│  ├─ 可逆:密文 + 密钥 → 原文                           │
│  ├─ 用途:保护传输/存储中的数据                          │
│  ├─ 示例:信用卡号、身份证号                             │
│  └─ 算法:AES-256-CTR, AES-256-GCM                    │
│                                                       │
│  哈希(Hashing)                                       │
│  ├─ 不可逆:原文 → 哈希值(无法还原)                    │
│  ├─ 用途:验证数据完整性、存储密码                       │
│  ├─ 示例:用户密码                                     │
│  └─ 算法:bcrypt, argon2, scrypt                      │
└───────────────────────────────────────────────────────┘

选择原则:
  - 需要还原原始数据 → 加密(AES)
  - 只需要验证是否匹配 → 哈希(bcrypt)
  - 密码存储 → 永远用哈希,永远不用加密

3.2 AES-256-CTR 对称加密

import {
  createCipheriv,
  createDecipheriv,
  randomBytes,
  scrypt,
} from 'crypto';
import { promisify } from 'util';

const scryptAsync = promisify(scrypt);

export class EncryptionService {
  private readonly algorithm = 'aes-256-ctr';

  // 加密
  async encrypt(text: string): Promise<string> {
    // 1. 从密码派生密钥(32 字节 = 256 位)
    const key = (await scryptAsync(
      process.env.ENCRYPTION_KEY,
      'salt',       // 生产环境应使用随机 salt 并存储
      32,
    )) as Buffer;

    // 2. 生成随机初始化向量(16 字节)
    const iv = randomBytes(16);

    // 3. 创建加密器并加密
    const cipher = createCipheriv(this.algorithm, key, iv);
    const encrypted = Buffer.concat([
      cipher.update(text, 'utf8'),
      cipher.final(),
    ]);

    // 4. 返回 iv:密文(iv 不是秘密,但每次必须不同)
    return `${iv.toString('hex')}:${encrypted.toString('hex')}`;
  }

  // 解密
  async decrypt(encryptedText: string): Promise<string> {
    const [ivHex, encryptedHex] = encryptedText.split(':');
    const iv = Buffer.from(ivHex, 'hex');
    const encrypted = Buffer.from(encryptedHex, 'hex');

    const key = (await scryptAsync(
      process.env.ENCRYPTION_KEY,
      'salt',
      32,
    )) as Buffer;

    const decipher = createDecipheriv(this.algorithm, key, iv);
    const decrypted = Buffer.concat([
      decipher.update(encrypted),
      decipher.final(),
    ]);

    return decrypted.toString('utf8');
  }
}

关键安全要点:

  • 密钥:从环境变量读取,绝不硬编码
  • IV(初始化向量):每次加密必须使用新的随机 IV,确保相同明文产生不同密文
  • scrypt:密钥派生函数,将可记忆的密码转换为固定长度的密钥
  • AES-256-CTR:CTR 模式不需要 padding,适合流式加密

3.3 密码哈希 — bcrypt

import * as bcrypt from 'bcrypt';

export class PasswordService {
  private readonly saltRounds = 10;

  // 哈希密码
  async hash(plainPassword: string): Promise<string> {
    return bcrypt.hash(plainPassword, this.saltRounds);
  }

  // 验证密码
  async compare(plainPassword: string, hashedPassword: string): Promise<boolean> {
    return bcrypt.compare(plainPassword, hashedPassword);
  }
}

bcrypt 的内部结构:

$2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
 │  │  │                                                    │
 │  │  └── salt(22 字符)                                    │
 │  └── cost factor(2^10 = 1024 轮)                        │
 └── 算法版本                               哈希值(31 字符) ─┘

bcrypt 自动将 salt 嵌入哈希结果中,compare() 时能从哈希中提取 salt 重新计算。

3.4 Argon2 — 更现代的密码哈希

npm install argon2
import * as argon2 from 'argon2';

// 哈希
const hash = await argon2.hash(password, {
  type: argon2.argon2id,      // 混合模式(推荐)
  memoryCost: 2 ** 16,         // 64 MB 内存
  timeCost: 3,                 // 3 轮迭代
  parallelism: 1,              // 并行度
});

// 验证
const isValid = await argon2.verify(hash, password);

bcrypt vs argon2:

特性bcryptargon2
年代19992015(PHC 获胜者)
内存抵抗固定 4KB可配置(MB 级别)
GPU 抵抗中等强(内存密集型)
密码长度最长 72 字节无限制
推荐度成熟稳定更安全,新项目首选
生态广泛支持逐渐普及

四、Node.js crypto 深入

[资深] 本节面向需要深入理解密码学基础的开发者。

4.1 密钥派生:scrypt vs pbkdf2

两者都是将密码转换为加密密钥的函数,但设计目标不同:

import { scrypt, pbkdf2, randomBytes } from 'crypto';
import { promisify } from 'util';

const scryptAsync = promisify(scrypt);
const pbkdf2Async = promisify(pbkdf2);

// scrypt:内存密集型,抗 GPU/ASIC 暴力破解
const keyScrypt = await scryptAsync(
  'password',           // 输入密码
  randomBytes(16),      // salt
  32,                   // 输出长度(字节)
  // 可选参数:{ N: 16384, r: 8, p: 1 }
  // N: CPU/内存开销,r: 块大小,p: 并行度
);

// pbkdf2:CPU 密集型,NIST 标准推荐
const keyPbkdf2 = await pbkdf2Async(
  'password',           // 输入密码
  randomBytes(16),      // salt
  100000,               // 迭代次数(OWASP 推荐 ≥ 600,000 for SHA-256)
  32,                   // 输出长度
  'sha256',             // 哈希算法
);
特性scryptpbkdf2
内存消耗高(可配置)低(固定)
GPU 抵抗力
标准非 NIST 标准NIST SP 800-132
适用密码哈希、密钥派生合规要求场景
Node.jscrypto.scryptcrypto.pbkdf2

4.2 安全随机数

import { randomBytes, randomUUID, randomInt } from 'crypto';

// 密码学安全的随机字节
const secureRandom = randomBytes(32);
// → <Buffer 7a 2c f1 ... > (不可预测)

// UUID v4
const id = randomUUID();
// → '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed'

// 安全随机整数(范围内)
const num = randomInt(0, 100);
// → 42(密码学安全的随机数)

crypto.randomBytes vs Math.random

// Math.random — 不安全,可预测
// 使用 PRNG(伪随机数生成器),有周期性
// 绝不能用于安全场景
const insecure = Math.random();  // 0.7295310407131368

// crypto.randomBytes — 安全,不可预测
// 使用操作系统的 CSPRNG(密码学安全伪随机数生成器)
// /dev/urandom (Linux) 或 CryptGenRandom (Windows)
const secure = randomBytes(16);  // 密码学安全

规则:所有与安全相关的随机值(Token、Salt、IV、密钥、验证码)都必须使用 crypto.randomBytes,永远不要使用 Math.random

4.3 JWT Secret 安全性

// 不安全的 secret
const weak = 'my-secret';           // 太短,可暴力破解
const dict = 'password123';          // 字典词,可猜测

// 安全的 secret(至少 256 位 = 32 字节)
// 生成方式:
//   node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
// 结果类似:a3f7c2b1...(128 个十六进制字符)

JWT Secret 安全清单:

  • 长度 >= 256 位(32 字节,推荐 64 字节)
  • 使用 crypto.randomBytes 生成
  • 存储在环境变量中,绝不硬编码
  • 定期轮换(配合 Token 黑名单或双密钥切换)
  • 不同环境使用不同 Secret

4.4 加密模式选择

模式特点适用
AES-CTR流式加密,不需 padding,可并行通用加密
AES-GCMCTR + 认证标签(AEAD),防篡改需要完整性保证
AES-CBC块加密,需 padding,串行遗留系统兼容

生产环境推荐 AES-256-GCM(同时提供加密和完整性保证):

import { createCipheriv, createDecipheriv, randomBytes, scrypt } from 'crypto';
import { promisify } from 'util';

const scryptAsync = promisify(scrypt);

async function encryptGCM(text: string): Promise<string> {
  const key = (await scryptAsync(process.env.ENCRYPTION_KEY, 'salt', 32)) as Buffer;
  const iv = randomBytes(12);  // GCM 推荐 12 字节 IV

  const cipher = createCipheriv('aes-256-gcm', key, iv);
  const encrypted = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);
  const authTag = cipher.getAuthTag();  // 16 字节认证标签

  // iv:authTag:密文
  return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted.toString('hex')}`;
}

async function decryptGCM(encryptedText: string): Promise<string> {
  const [ivHex, authTagHex, encryptedHex] = encryptedText.split(':');

  const key = (await scryptAsync(process.env.ENCRYPTION_KEY, 'salt', 32)) as Buffer;
  const iv = Buffer.from(ivHex, 'hex');
  const authTag = Buffer.from(authTagHex, 'hex');
  const encrypted = Buffer.from(encryptedHex, 'hex');

  const decipher = createDecipheriv('aes-256-gcm', key, iv);
  decipher.setAuthTag(authTag);  // 设置认证标签,自动验证完整性

  const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
  return decrypted.toString('utf8');
}

GCM 模式在解密时会自动验证认证标签,如果密文被篡改,decipher.final() 会抛出异常。这比 CTR 模式更安全。


五、安全架构设计

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

5.1 安全防护清单

每个生产环境 NestJS 应用必须满足的最低安全要求:

┌─────────────────────────────────────────────────────┐
│  NestJS 安全清单                                     │
│                                                     │
│  □ HTTPS(全站强制)                                  │
│  □ Helmet(安全响应头)                               │
│  □ CORS(跨域白名单)                                │
│  □ CSRF 防护(状态变更操作)                          │
│  □ Rate Limiting(请求限流)                         │
│  □ 输入验证(ValidationPipe + class-validator)      │
│  □ 认证(JWT / Session)                             │
│  □ 授权(RBAC / ABAC)                               │
│  □ 密码哈希(bcrypt / argon2)                       │
│  □ 敏感数据加密(AES-256-GCM)                       │
│  □ SQL 注入防御(参数化查询 / ORM)                   │
│  □ XSS 防御(输出转义 + CSP)                        │
│  □ 依赖漏洞扫描(npm audit)                         │
│  □ 日志脱敏(不记录密码、Token、密钥)                 │
│  □ 错误信息脱敏(不暴露堆栈和内部细节)                │
└─────────────────────────────────────────────────────┘

5.2 分层防御策略

┌────────────────────────────────────────────────────────┐
│  第 1 层:网络层(基础设施团队)                          │
│  ├─ WAF(Web 应用防火墙)                               │
│  ├─ DDoS 防护(CDN / CloudFlare)                      │
│  └─ IP 黑白名单                                        │
├────────────────────────────────────────────────────────┤
│  第 2 层:传输层(运维团队)                              │
│  ├─ TLS 1.3(HTTPS 强制)                               │
│  ├─ HSTS(Strict-Transport-Security)                  │
│  └─ 证书管理与自动续签                                   │
├────────────────────────────────────────────────────────┤
│  第 3 层:应用层(开发团队)— NestJS 职责               │
│  ├─ Helmet(安全响应头)                                │
│  ├─ CORS(跨域控制)                                   │
│  ├─ CSRF(防伪造)                                     │
│  ├─ Rate Limiting(防暴力)                             │
│  ├─ AuthGuard(认证)                                  │
│  ├─ RolesGuard(授权)                                 │
│  ├─ ValidationPipe(输入验证)                          │
│  └─ ExceptionFilter(错误脱敏)                         │
├────────────────────────────────────────────────────────┤
│  第 4 层:数据层(开发 + DBA 团队)                      │
│  ├─ 参数化查询(ORM / 预编译语句)                      │
│  ├─ 密码哈希(bcrypt / argon2)                        │
│  ├─ 敏感字段加密(AES-256-GCM)                        │
│  ├─ 数据库访问控制(最小权限原则)                       │
│  └─ 备份加密                                           │
└────────────────────────────────────────────────────────┘

纵深防御的核心思想:每一层都假设上一层可能失败。即使 WAF 被绕过,应用层的 Guard 和 Pipe 仍然能阻止攻击。

5.3 main.ts 安全配置模板

import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import helmet from 'helmet';
import * as cookieParser from 'cookie-parser';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    logger: ['error', 'warn', 'log'],  // 生产环境不输出 debug/verbose
  });

  // 1. 安全响应头(最先注册)
  app.use(helmet());

  // 2. Cookie 解析(CSRF 依赖)
  app.use(cookieParser());

  // 3. CORS 白名单
  app.enableCors({
    origin: process.env.ALLOWED_ORIGINS?.split(','),
    credentials: true,
    methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
  });

  // 4. 全局输入验证
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,              // 剥离未装饰的属性
      forbidNonWhitelisted: true,   // 未知属性报错
      transform: true,              // 自动类型转换
      disableErrorMessages:         // 生产环境隐藏验证细节
        process.env.NODE_ENV === 'production',
    }),
  );

  // 5. 全局前缀
  app.setGlobalPrefix('api/v1');

  // 6. 优雅关闭
  app.enableShutdownHooks();

  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

对应的 app.module.ts

@Module({
  imports: [
    ThrottlerModule.forRoot([
      { name: 'short', ttl: seconds(1), limit: 3 },
      { name: 'long', ttl: minutes(1), limit: 100 },
    ]),
    // ... 其他模块
  ],
  providers: [
    { provide: APP_GUARD, useClass: ThrottlerGuard },   // 限流
    { provide: APP_GUARD, useClass: AuthGuard },         // 认证
    { provide: APP_GUARD, useClass: RolesGuard },        // 授权
  ],
})
export class AppModule {}

5.4 安全审计与合规

# 检查依赖漏洞
npm audit

# 自动修复可安全升级的漏洞
npm audit fix

# 查看详细报告
npm audit --json

# 只显示高危和严重漏洞
npm audit --audit-level=high

安全审计流程:

频率审计内容工具
每次提交代码中无硬编码密钥ESLint + pre-commit hook
每日依赖漏洞扫描npm audit(CI/CD 集成)
每周安全日志审查日志聚合系统
每月全面安全评估OWASP 清单
每季度渗透测试安全团队 / 第三方

5.5 常见安全错误

错误风险正确做法
生产环境 origin: '*'任何网站都能调用 API使用域名白名单
synchronize: true(数据库)生产环境自动修改表结构使用 migration
JWT Secret 硬编码源码泄露即全线崩溃环境变量 + 定期轮换
明文存储密码数据库泄露即全部暴露bcrypt / argon2 哈希
错误信息含堆栈暴露内部实现细节ExceptionFilter 脱敏
无限流暴力破解、DDoSThrottlerGuard
日志记录敏感数据日志泄露即数据泄露日志脱敏中间件
disableErrorMessages: false(生产)暴露验证规则生产环境设为 true

六、课后实践

练习 1:完整安全组合(基础)

为应用配置 Helmet + CORS + ThrottlerGuard 三件套:

// main.ts
import helmet from 'helmet';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.use(helmet());
  app.enableCors({
    origin: ['http://localhost:3001'],
    credentials: true,
  });

  await app.listen(3000);
}
// app.module.ts — 添加 ThrottlerModule
ThrottlerModule.forRoot([
  { name: 'short', ttl: seconds(1), limit: 3 },
  { name: 'long', ttl: minutes(1), limit: 100 },
]),

验证:

# 快速发送 4 次请求,第 4 次应返回 429
for i in {1..4}; do
  curl -w "%{http_code}\n" http://localhost:3000/cats
done

练习 2:密码哈希注册/登录(中阶)

  1. 添加 POST /auth/register 端点,使用 bcrypt.hash() 存储密码
  2. 修改 POST /auth/login,使用 bcrypt.compare() 验证密码
  3. 确认数据库中存储的是哈希值,而非明文
// 注册
const hashedPassword = await bcrypt.hash(dto.password, 10);
await this.usersRepository.save({ ...dto, password: hashedPassword });

// 登录
const isMatch = await bcrypt.compare(dto.password, user.password);

练习 3:AES 加密服务(高阶)

  1. 实现 EncryptionService,提供 encrypt()decrypt() 方法
  2. 使用 AES-256-GCM 模式
  3. 密钥从环境变量读取
  4. 编写单元测试:加密后解密应得到原文
# .env
ENCRYPTION_KEY=your-secure-random-key-here

练习 4:CSRF 防护配置(中阶)

  1. 安装 csrf-csrfcookie-parser
  2. 配置双重提交 Cookie 模式
  3. 测试:不携带 CSRF Token 的 POST 请求应被拒绝

练习 5:安全审计(基础)

# 1. 运行依赖漏洞扫描
npm audit

# 2. 检查项目中是否有硬编码的密钥
grep -rn "secret\|password\|api_key\|token" src/ --include="*.ts" \
  | grep -v "node_modules" \
  | grep -v ".spec.ts"

# 3. 检查是否有 console.log 输出敏感信息
grep -rn "console.log" src/ --include="*.ts" | grep -v "node_modules"

七、本课知识点总结

知识点要点
Helmetapp.use(helmet()),15+ 安全头,必须最先注册
CORSapp.enableCors(),生产环境必须白名单,禁止 origin: '*'
CSRFcsrf-csrf 双重提交 Cookie,需要 cookie-parser
ThrottlerGuardThrottlerModule.forRoot([{name,ttl,limit}]),多策略同时生效
限流定制@SkipThrottle() 跳过,@Throttle() 自定义,Redis 分布式存储
加密(AES)createCipheriv / createDecipheriv,随机 IV,密钥从环境变量读取
哈希(bcrypt)bcrypt.hash() + bcrypt.compare(),saltRounds 推荐 10-12
argon2更现代,内存密集型抗 GPU,新项目首选
安全随机数crypto.randomBytes,永远不用 Math.random
分层防御WAF → TLS → NestJS Guards → 数据加密,每层独立防护
npm audit定期扫描依赖漏洞,CI/CD 集成自动检查

下一课预告:第十三课将学习配置管理、日志系统与事件驱动,包括 @nestjs/config 多环境配置、自定义 Logger、@nestjs/event-emitter 事件发布/订阅等应用基础设施。