覆盖文档: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 配置要点
| 配置项 | 开发环境 | 生产环境 |
|---|---|---|
origin | true(允许所有) | 明确的域名白名单 |
credentials | true | true(若需要 Cookie) |
methods | 全部 | 按需配置 |
maxAge | 不限 | 3600(1小时) |
安全提醒:生产环境绝不能使用
origin: true或origin: '*'。这会允许任何网站调用你的 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 的工作原理:
- 服务端生成 CSRF Token,同时存入 Cookie 和响应中
- 前端将 Token 放入请求头
X-CSRF-Token - 服务端验证:Cookie 中的 Token === 请求头中的 Token
- 攻击者无法读取其他域的 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:
| 特性 | bcrypt | argon2 |
|---|---|---|
| 年代 | 1999 | 2015(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', // 哈希算法
);
| 特性 | scrypt | pbkdf2 |
|---|---|---|
| 内存消耗 | 高(可配置) | 低(固定) |
| GPU 抵抗力 | 强 | 弱 |
| 标准 | 非 NIST 标准 | NIST SP 800-132 |
| 适用 | 密码哈希、密钥派生 | 合规要求场景 |
| Node.js | crypto.scrypt | crypto.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-GCM | CTR + 认证标签(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 脱敏 |
| 无限流 | 暴力破解、DDoS | ThrottlerGuard |
| 日志记录敏感数据 | 日志泄露即数据泄露 | 日志脱敏中间件 |
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:密码哈希注册/登录(中阶)
- 添加
POST /auth/register端点,使用bcrypt.hash()存储密码 - 修改
POST /auth/login,使用bcrypt.compare()验证密码 - 确认数据库中存储的是哈希值,而非明文
// 注册
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 加密服务(高阶)
- 实现
EncryptionService,提供encrypt()和decrypt()方法 - 使用 AES-256-GCM 模式
- 密钥从环境变量读取
- 编写单元测试:加密后解密应得到原文
# .env
ENCRYPTION_KEY=your-secure-random-key-here
练习 4:CSRF 防护配置(中阶)
- 安装
csrf-csrf和cookie-parser - 配置双重提交 Cookie 模式
- 测试:不携带 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"
七、本课知识点总结
| 知识点 | 要点 |
|---|---|
| Helmet | app.use(helmet()),15+ 安全头,必须最先注册 |
| CORS | app.enableCors(),生产环境必须白名单,禁止 origin: '*' |
| CSRF | csrf-csrf 双重提交 Cookie,需要 cookie-parser |
| ThrottlerGuard | ThrottlerModule.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 事件发布/订阅等应用基础设施。