当然!我们来探讨一个在构建前后端分离应用时,几乎 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
的前端代码试图用 fetch
或 axios
去请求 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
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
(生产环境推荐配置)
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
: 有时,你的白名单可能非常长,或者需要动态变化。你可以提供一个函数。
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 之外如何构建更坚固的安全壁垒。
企业级实践的关注点:
统一的 API 网关 (API Gateway):
- 架构: 在大型微服务架构中,前端应用通常不会直接请求各个独立的微服务。所有的请求都会先经过一个统一的入口——API 网关(如基于 Nginx+Lua, Kong, 或自研的网关服务)。
- CORS 策略集中管理: 所有的 CORS 策略都在 API 网关层进行统一配置和管理。这意味着,后端的各个 NestJS 微服务本身甚至可以不开启 CORS (
app.enableCors()
都可以省略),因为从网关转发到内部服务的请求,在网络层面通常被视为“同源”或内部调用。 - 好处:
- 策略统一:无需在几十个微服务中重复配置和同步 CORS 规则。
- 安全加固:网关还可以执行统一的认证、授权、限流、日志记录等横切关注点。
- 后端简化:后端开发者可以更专注于业务逻辑。
Nginx 配置 CORS 的伪代码:
nginxlocation /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; }
CSRF (跨站请求伪造) 防护:
- 仅仅配置了 CORS 还不足以完全防止跨站攻击。CORS 主要防止的是恶意网站读取你的 API 响应,但对于不读取响应的“写入”操作(如
POST
一个转账请求),还需要 CSRF Token 来防护。 - 企业级实践: 在 API 网关或 NestJS 的全局中间件中,会集成 CSRF 防护逻辑(如使用
csurf
中间件),要求所有“写入”操作都必须携带一个与用户会话绑定的、不可预测的 CSRF Token。
- 仅仅配置了 CORS 还不足以完全防止跨站攻击。CORS 主要防止的是恶意网站读取你的 API 响应,但对于不读取响应的“写入”操作(如
总结
CORS 是处理跨源 HTTP 请求的安全基石,是现代前后端分离应用开发必须掌握的知识。
- 它解决了什么问题? 允许服务器安全地、可控地向来自不同源的客户端开放其资源。
- 如何在 NestJS 中实现? 在
main.ts
中调用app.enableCors()
。 - 最佳实践是什么?
- 不要使用
origin: '*'
在生产环境中。 - 明确地配置一个可信源的白名单。
- 不要使用
- 企业级的做法是什么? 将 CORS 策略上移到 API 网关层进行统一管理,让后端微服务保持纯净。同时,配合 CSRF Token 等其他安全措施,构建纵深防御体系。
理解并正确配置 CORS,是确保你的“银行 API”只为它所信任的“营业网点”服务,而将不法之徒拒之门外的第一步。