首页 / NestJS 入门教程 / 安全加固

NestJS 入门教程

安全加固

本教程共 47 篇 · 第 23 篇 · 更新于 2026-08-09 · 约 11 分钟阅读

NestJS安全CORSCSRFHelmet限流加密哈希

本节目标:掌握 NestJS 中的安全加固手段,包括 CORS 跨域配置、CSRF 防护、Helmet 安全头、接口限流、密码加密与哈希,把 API 的安全防线建起来。

前面几章我们搞定了认证和权限。但安全不只是”验证身份”和”控制权限”——就像家里装了防盗门,你还得关窗户、拉窗帘、装监控。

这一章涵盖 CORS 跨域配置、CSRF 防护、Helmet 安全头、接口限流、加密和哈希。逐个来搞定。

CORS:跨域资源共享

什么是 CORS?

浏览器有个”同源策略”——http://localhost:3000 的页面,默认不能请求 http://localhost:4000 的接口。这是浏览器的安全机制,防止恶意网站偷偷访问你的 API。

但前后端分离的项目,前端和后端几乎必然不在同一个域。这时候就需要后端”主动开口”告诉浏览器:我允许这个域来访问我。

这就是 CORS(Cross-Origin Resource Sharing)。

基本配置

NestJS 开启 CORS 非常简单:

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

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // 一行搞定,允许所有来源
  app.enableCors();
  
  await app.listen(3000);
}
bootstrap();

也可以在创建应用时通过选项开启:

const app = await NestFactory.create(AppModule, {
  cors: true,
});

精细配置

生产环境中不能允许”所有来源”——那等于没设防。你需要指定具体哪些域可以访问:

app.enableCors({
  origin: ['http://localhost:5173', 'https://www.yoursite.com'],
  methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
  credentials: true,   // 允许携带 Cookie
  maxAge: 86400,       // 预检请求缓存 24 小时
});

各字段含义:

字段作用
origin允许的来源列表,可以是字符串、数组或函数
methods允许的 HTTP 方法
allowedHeaders允许的请求头
credentials是否允许携带凭证(Cookie、Authorization 头)
maxAge预检请求(OPTIONS)的缓存时间,单位秒
Warning

credentials: true 时,origin 不能设为 '*'。浏览器会直接报错。必须指定具体的域名。

Tip

origin 也可以传一个函数,方便你做动态判断。比如从数据库读取允许的域名列表,或者根据环境变量切换。

app.enableCors({
  origin: (origin, callback) => {
    const allowedOrigins = ['https://www.yoursite.com', 'http://localhost:5173'];
    // 开发环境下允许没有 origin 的请求(比如 Postman)
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('该来源不被允许'));
    }
  },
});

CSRF 防护

什么是 CSRF?

CSRF(Cross-Site Request Forgery,跨站请求伪造)是一种攻击方式。简单说:

用户登录了 bank.com,Cookie 里存着会话信息。用户又打开了一个恶意网站 evil.com,这个网站里藏了一段代码,自动向 bank.com 发起转账请求。因为浏览器会自动带上 Cookie,所以 bank.com 以为这是用户自己的操作。

这就是”借刀杀人”——利用用户的身份,干坏事。

什么时候需要 CSRF 防护?

如果你用的是 JWT + Authorization 头的方式认证,CSRF 基本不是问题——恶意网站没法自动往请求头里塞 JWT。

但如果你的应用用 Cookie 做认证(session-cookie 方案),那就必须做 CSRF 防护。

使用 csrf-csrf

安装依赖:

npm install csrf-csrf

在 NestJS 中配置:

// main.ts
import { doubleCsrf } from 'csrf-csrf';

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

  // csrf-csrf 需要 cookie-parser 或 session 中间件
  app.use(cookieParser());

  const { doubleCsrfProtection } = doubleCsrf({
    getSecret: () => 'your-csrf-secret',
    cookieName: '__csrf',
    cookieOptions: {
      sameSite: 'strict',
      path: '/',
      secure: process.env.NODE_ENV === 'production',
    },
    size: 64,
    getTokenFromRequest: (req) => req.headers['x-csrf-token'],
  });

  // GET 请求通常不需要 CSRF 保护
  app.use((req, res, next) => {
    if (req.method === 'GET') return next();
    return doubleCsrfProtection(req, res, next);
  });

  await app.listen(3000);
}

前端使用时,先从接口获取 CSRF Token,然后在请求头中带上:

// 获取 CSRF Token
const { token } = await fetch('/api/csrf-token').then(r => r.json());

// 请求时带上 Token
fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'x-csrf-token': token,
  },
  body: JSON.stringify({ amount: 100 }),
});
Note

如果你的 API 纯粹给 SPA 或移动端用,且使用 JWT Bearer Token 认证,CSRF 防护可以不加。但如果用了基于 Cookie 的认证方案,CSRF 防护是必须的。

Helmet:安全响应头

什么是 Helmet?

Helmet 是一个 Express 中间件,它帮你设置一堆 HTTP 安全响应头。这些头告诉浏览器:

  • 别加载来路不明的脚本(Content-Security-Policy)
  • 别让别人用 iframe 嵌套我的页面(X-Frame-Options)
  • 别猜我的内容类型(X-Content-Type-Options)
  • 强制使用 HTTPS(Strict-Transport-Security)

打个比方:Helmet 给你的服务器戴上了一顶安全头盔,挡住了一堆常见的 Web 攻击。

安装和使用

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

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

  // 一定要在其他中间件之前注册
  app.use(helmet());

  await app.listen(3000);
}

就这么一行,Helmet 会自动帮你设置以下响应头:

响应头作用
Content-Security-Policy控制页面可以加载哪些来源的资源
X-Content-Type-Options禁止浏览器猜测 Content-Type
X-Frame-Options防止点击劫持,禁止被 iframe 嵌套
Strict-Transport-Security强制 HTTPS
X-XSS-Protection启用浏览器 XSS 过滤
Referrer-Policy控制 Referer 头的发送策略

自定义配置

默认配置已经能覆盖大部分场景。但有时候你需要微调:

app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'", "'unsafe-inline'"],
        imgSrc: ["'self'", 'data:', 'https:'],
        styleSrc: ["'self'", "'unsafe-inline'"],
      },
    },
    crossOriginEmbedderPolicy: false, // 如果需要加载外部资源
  }),
);
Tip

如果你用了 Swagger 文档或者 GraphQL Playground,可能需要调整 CSP 策略,否则页面样式和脚本会被拦截。

接口限流

为什么需要限流?

想象一下:有人写个脚本,每秒向你登录接口发 1000 次请求,尝试不同的密码组合。这就是暴力破解。

限流(Rate Limiting)就是给每个用户设置”请求配额”。比如:每分钟最多 60 次请求。超了?返回 429 状态码(Too Many Requests),你先歇会儿。

安装 @nestjs/throttler

npm install @nestjs/throttler

基本配置

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

@Module({
  imports: [
    ThrottlerModule.forRoot({
      throttlers: [
        {
          ttl: 60000,  // 时间窗口:60 秒
          limit: 60,   // 最多 60 次请求
        },
      ],
    }),
  ],
  providers: [
    {
      provide: APP_GUARD,
      useClass: ThrottlerGuard,
    },
  ],
})
export class AppModule {}

这段配置的意思是:每个 IP,每 60 秒内最多 60 次请求。超过就返回 429。

多级限流

实际项目中,不同接口可能需要不同的限流策略:

ThrottlerModule.forRoot({
  throttlers: [
    {
      name: 'short',
      ttl: 1000,    // 1 秒
      limit: 3,     // 最多 3 次
    },
    {
      name: 'medium',
      ttl: 10000,   // 10 秒
      limit: 20,    // 最多 20 次
    },
    {
      name: 'long',
      ttl: 60000,   // 60 秒
      limit: 100,   // 最多 100 次
    },
  ],
}),

在控制器中调整限流

有些接口需要更严格的限制,比如登录接口:

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

@Controller('auth')
export class AuthController {
  // 登录接口:每分钟最多 5 次
  @Post('login')
  @Throttle({ default: { limit: 5, ttl: 60000 } })
  login(@Body() loginDto: LoginDto) {
    return this.authService.login(loginDto);
  }

  // 注册接口:每分钟最多 3 次
  @Post('register')
  @Throttle({ default: { limit: 3, ttl: 60000 } })
  register(@Body() registerDto: RegisterDto) {
    return this.authService.register(registerDto);
  }
}

有些接口则完全不需要限流,比如健康检查:

@Controller('health')
@SkipThrottle()  // 整个控制器跳过限流
export class HealthController {
  @Get()
  check() {
    return { status: 'ok' };
  }
}

代理服务器下的限流

如果你的应用跑在 Nginx 后面,限流拿到的 IP 全是 Nginx 的地址。需要配置信任代理:

// main.ts
import { NestExpressApplication } from '@nestjs/platform-express';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  
  // 信任代理,获取真实 IP
  app.set('trust proxy', 1);
  
  await app.listen(3000);
}
Tip

分布式部署时,内存里的限流计数不共享。每个实例各算各的。生产环境建议用 Redis 做统一存储——@nestjs/throttler 支持自定义 storage,社区有 throttler-storage-redis 包可以直接用。

加密与哈希

加密 vs 哈希

这两个概念经常被搞混,其实差别很大:

加密(Encryption)哈希(Hashing)
方向双向——可以解密单向——不可逆
用途保护传输中的数据验证数据(如密码)
密钥需要密钥不需要
例子AES、RSAbcrypt、argon2

打个比方:加密像保险箱,放进去还能拿出来。哈希像碎纸机,碎了就拼不回去。

密码哈希:bcrypt

存储密码的黄金法则是:永远不要存明文密码。存哈希值。

安装 bcrypt:

npm install bcrypt
npm install -D @types/bcrypt

注册时对密码做哈希:

// auth.service.ts
import * as bcrypt from 'bcrypt';

async function hashPassword(password: string): Promise<string> {
  const saltRounds = 10;
  return bcrypt.hash(password, saltRounds);
}

async function register(email: string, password: string) {
  const hashedPassword = await hashPassword(password);
  
  const user = await this.userService.create({
    email,
    password: hashedPassword,  // 存哈希值
  });
  
  return user;
}

登录时验证密码:

async function validatePassword(
  plainPassword: string,
  hashedPassword: string,
): Promise<boolean> {
  return bcrypt.compare(plainPassword, hashedPassword);
}

async function login(email: string, password: string) {
  const user = await this.userService.findByEmail(email);
  
  if (!user) {
    throw new UnauthorizedException('邮箱或密码错误');
  }
  
  const isMatch = await validatePassword(password, user.password);
  
  if (!isMatch) {
    throw new UnauthorizedException('邮箱或密码错误');
  }
  
  // 登录成功,签发 JWT...
}
Note

saltRounds 越大越安全,但计算越慢。10 是一个不错的平衡点。太低不安全,太高影响性能。

Tip

错误提示用”邮箱或密码错误”,不要说”密码错误”或”邮箱不存在”。否则攻击者可以借此枚举你的注册用户。

数据加密:Node.js crypto 模块

有些场景需要加密(不是哈希)——比如存储用户的 API Key,之后还需要读取出来用。这时候用加密。

Node.js 自带 crypto 模块,不需要额外安装:

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

// 加密
async function encrypt(text: string, password: string): Promise<string> {
  const iv = randomBytes(16);
  const salt = randomBytes(16);
  const key = (await promisify(scrypt)(password, salt, 32)) as Buffer;
  const cipher = createCipheriv('aes-256-ctr', key, iv);
  
  const encrypted = Buffer.concat([
    cipher.update(text),
    cipher.final(),
  ]);
  
  // 把 iv、salt 和密文拼在一起返回
  return `${iv.toString('hex')}:${salt.toString('hex')}:${encrypted.toString('hex')}`;
}

// 解密
async function decrypt(encryptedText: string, password: string): Promise<string> {
  const [ivHex, saltHex, dataHex] = encryptedText.split(':');
  const iv = Buffer.from(ivHex, 'hex');
  const salt = Buffer.from(saltHex, 'hex');
  const encrypted = Buffer.from(dataHex, 'hex');
  
  const key = (await promisify(scrypt)(password, salt, 32)) as Buffer;
  const decipher = createDecipheriv('aes-256-ctr', key, iv);
  
  const decrypted = Buffer.concat([
    decipher.update(encrypted),
    decipher.final(),
  ]);
  
  return decrypted.toString();
}
Warning

加密密钥(上面的 password 参数)一定要通过环境变量管理,绝对不能硬编码在代码里。

安全加固清单

整理一份安全检查清单,上线前对着过一遍:

检查项怎么做优先级
CORS 配置只允许已知域名
Helmet 安全头app.use(helmet())
接口限流@nestjs/throttler
密码存储bcrypt 哈希,不存明文
敏感数据加密使用 crypto 模块
CSRF 防护Cookie 认证时必须
环境变量密钥、密码不硬编码
HTTPS生产环境必须启用
输入验证第 25 章详细讲
SQL 注入使用 ORM 或参数化查询
Tip

安全是一个持续的过程,不是一次性的配置。定期更新依赖、审查代码、做渗透测试,都是必要的。

踩坑经验

1. Helmet 必须在其他中间件之前注册

Helmet 本质是一组中间件。Express 的中间件是按注册顺序执行的。如果 Helmet 放在路由之后,它就不会保护那些路由。

// 正确
app.use(helmet());
app.use(cors());
// ... 其他中间件
// ... 路由

// 错误:Helmet 放太后面了
app.use(cors());
app.get('/api/data', handler);
app.use(helmet()); // 对上面的路由无效

2. CORS 的 credentials 和 origin 不能同时用通配符

origin: '*' + credentials: true 会被浏览器拒绝。要么指定具体域名,要么不传 credentials。

3. 限流的 ttl 单位是毫秒

@nestjs/throttler v5+ 的 ttl 单位是毫秒,不是秒。ttl: 60000 才是 60 秒。可以用包提供的 seconds()minutes() 辅助函数让代码更直观:

import { seconds } from '@nestjs/throttler';

ThrottlerModule.forRoot({
  throttlers: [{
    ttl: seconds(60),  // 比写 60000 清楚多了
    limit: 60,
  }],
}),

4. bcrypt 的 saltRounds 不是越大越好

saltRounds = 10 时,哈希一次大约 100ms。saltRounds = 15 时可能需要 3 秒。注册接口会变慢。10 到 12 是推荐范围。

小结

关键知识点回顾:

  • CORS 配置要注意 credentials 和 origin 不能同时用通配符
  • CSRF 防护用 csurf 中间件,配合双重 Cookie 或 Token
  • Helmet 设置安全响应头,必须在其他中间件之前注册
  • @nestjs/throttler 做接口限流,支持多维度限流策略
  • 密码存储用 bcrypt,saltRounds 推荐 10-12

下一章我们聊聊配置管理,让应用在不同环境下灵活切换。