Skip to content

当然!我们来探讨一个在构建前后端分离应用时,几乎 100% 会遇到的“经典难题”:CORS (跨源资源共享)。这就像是为你的应用设置一套“涉外接待准则”,决定了来自“外国”的访客(其他网站)是否有权访问你的资源。

为你的应用设定“涉外接待准 T 则”:NestJS CORS 完全指南

想象你的 NestJS 应用是一家运营在 api.my-bank.com 上的银行 API 服务中心

与此同时,你们公司还有几个不同的“营业网点”:

  • 官方网站www.my-bank.com (一个 React 或 Vue 开发的前端应用)
  • 合作伙伴的网站partner.com
  • 一个恶意的钓鱼网站evil-hacker.com

出于安全考虑,浏览器执行一个非常重要的同源策略 (Same-Origin Policy)。这个策略就像是银行内部的一条铁律:“我们只和自己人说话!

  • 如果一个请求的发起方(源 Origin)和接收方(你的 API)的协议、域名、端口完全相同,那么就是“同源”的,浏览器会允许这次通信。
  • 如果三者有任何一个不同,就是“跨源 (Cross-Origin)”的。比如,www.my-bank.com 想请求 api.my-bank.com 的数据,虽然域名相似,但子域名不同,所以这也是一次跨源请求。

在没有额外配置的情况下,当 www.my-bank.com 的前端代码试图用 fetchaxios 去请求 api.my-bank.com/user/profile 时,浏览器会先发送一个特殊的“预检请求 (Preflight Request)”(一个 OPTIONS 方法的请求),就像是派一个信使去问银行:

“你好,我是来自 www.my-bank.com 的,我接下来想用 POST 方法,带着一个 Content-Type 的头,来访问你们的 /user/profile 接口,可以吗?”

如果你的银行 API 服务中心(NestJS 应用)没有配置任何“涉外接待准则”,它就会沉默不语。浏览器看到服务器没反应,就会认为“对方拒绝了这次跨国会谈”,然后直接在控制台报错,阻止真正的 POST 请求发出。这就是你经常在前端开发中看到的 CORS 错误

CORS (Cross-Origin Resource Sharing) 就是一套由服务器来声明的规则,它通过设置一系列特殊的 HTTP 响应头(如 Access-Control-Allow-Origin),来明确地告诉浏览器:“没问题,我信任来自 www.my-bank.com 的请求,请放行!

1. 第一步:制定“接待准则” (在 main.ts 中开启 CORS)

NestJS 提供了一个极其简单的方法来开启和配置 CORS。

src/main.ts

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

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

  // 开启 CORS 功能
  app.enableCors();

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

app.enableCors() 这行代码做了什么?

在不传递任何参数的情况下,它启用了一套最宽松的默认配置。它会在响应头中加入 Access-Control-Allow-Origin: *,这相当于对浏览器说:“我允许来自任何源 (any origin) 的请求!

对于开发和一些完全公开的 API 来说,这很方便。但对于生产环境,特别是涉及用户数据的应用,这通常是不安全的。因为它意味着那个恶意的 evil-hacker.com 也能从他们的网站上请求你的 API。

2. 第二步:精细化你的“接待名单” (配置 CORS 选项)

我们需要一份更精确的“白名单”,只允许我们信任的源进行访问。我们可以向 enableCors() 传递一个配置对象。

src/main.ts (生产环境推荐配置)

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

  app.enableCors({
    // 1. 明确指定允许的源 (origin)
    origin: [
      'http://localhost:8080', // 本地开发的前端地址
      'https://www.my-bank.com', // 官方网站
      'https://partner.com', // 合作伙伴网站
    ],

    // 2. 允许的 HTTP 方法
    methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',

    // 3. 允许的请求头
    allowedHeaders: 'Content-Type, Accept, Authorization',

    // 4. 是否允许携带凭证 (如 Cookies)
    credentials: true,
  });

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

现在,我们的“涉外接待准则”变得非常明确:

  • 只有来自白名单中的这三个源的跨域请求才会被允许。
  • 任何来自 evil-hacker.com 的请求,在预检阶段就会被服务器拒绝。

动态配置 origin: 有时,你的白名单可能非常长,或者需要动态变化。你可以提供一个函数。

typescript
app.enableCors({
origin: (origin, callback) => {
// 从数据库或配置中心读取白名单
const whitelist = ['http://localhost:8080', 'https://www.my-bank.com'];
if (whitelist.indexOf(origin) !== -1 || !origin) {
// 如果来源在白名单中,或者是非浏览器环境的同源请求 (origin 为 undefined)
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
// ... 其他选项
});

3. 特殊情况:只为某个 Controller 开启 CORS

如果你不希望全局开启 CORS,只想为某个特定的 Controller(比如一个公开的 /public 接口)设置,你可以在该 Controller 上使用 @Header() 装饰器或直接操作 @Res()。但这种方式比较繁琐,通常不推荐。全局配置 enableCors 是管理 CORS 策略最清晰、最集中的方式。

4. 中国企业级的方案思考

CORS 本身是一个全球通用的 Web 标准,app.enableCors() 提供的功能已经完全足够满足任何企业的需求,它不存在所谓的“水土不服”。企业级的考量通常不在于如何“实现”CORS,而在于如何“管理”CORS 策略,以及在 CORS 之外如何构建更坚固的安全壁垒。

企业级实践的关注点:

  1. 统一的 API 网关 (API Gateway):

    • 架构: 在大型微服务架构中,前端应用通常不会直接请求各个独立的微服务。所有的请求都会先经过一个统一的入口——API 网关(如基于 Nginx+Lua, Kong, 或自研的网关服务)。
    • CORS 策略集中管理: 所有的 CORS 策略都在 API 网关层进行统一配置和管理。这意味着,后端的各个 NestJS 微服务本身甚至可以不开启 CORS (app.enableCors() 都可以省略),因为从网关转发到内部服务的请求,在网络层面通常被视为“同源”或内部调用。
    • 好处:
      • 策略统一:无需在几十个微服务中重复配置和同步 CORS 规则。
      • 安全加固:网关还可以执行统一的认证、授权、限流、日志记录等横切关注点。
      • 后端简化:后端开发者可以更专注于业务逻辑。

    Nginx 配置 CORS 的伪代码:

    nginx
    location /api/ {
        # 允许的源
        if ($http_origin ~* (https?://www\.my-bank\.com|https?://partner\.com)) {
            add_header 'Access-Control-Allow-Origin' "$http_origin";
            add_header 'Access-Control-Allow-Credentials' 'true';
        }
    
        # 处理预检请求
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type';
            add_header 'Access-Control-Max-Age' 1728000;
            add_header 'Content-Type' 'text/plain charset=UTF-8';
            add_header 'Content-Length' 0;
            return 204;
        }
    
        # 将真正的请求代理到后端的 NestJS 服务
        proxy_pass http://nestjs_backend_cluster;
    }
  2. CSRF (跨站请求伪造) 防护:

    • 仅仅配置了 CORS 还不足以完全防止跨站攻击。CORS 主要防止的是恶意网站读取你的 API 响应,但对于不读取响应的“写入”操作(如 POST 一个转账请求),还需要 CSRF Token 来防护。
    • 企业级实践: 在 API 网关或 NestJS 的全局中间件中,会集成 CSRF 防护逻辑(如使用 csurf 中间件),要求所有“写入”操作都必须携带一个与用户会话绑定的、不可预测的 CSRF Token。

总结

CORS 是处理跨源 HTTP 请求的安全基石,是现代前后端分离应用开发必须掌握的知识。

  • 它解决了什么问题? 允许服务器安全地、可控地向来自不同源的客户端开放其资源。
  • 如何在 NestJS 中实现?main.ts 中调用 app.enableCors()
  • 最佳实践是什么?
    • 不要使用 origin: '*' 在生产环境中。
    • 明确地配置一个可信源的白名单
  • 企业级的做法是什么?CORS 策略上移到 API 网关层进行统一管理,让后端微服务保持纯净。同时,配合 CSRF Token 等其他安全措施,构建纵深防御体系。

理解并正确配置 CORS,是确保你的“银行 API”只为它所信任的“营业网点”服务,而将不法之徒拒之门外的第一步。