首页 / Node.js 教程 / CORS 与安全响应头

Node.js 教程

CORS 与安全响应头

本教程共 76 篇 · 第 57 篇 · 更新于 2026-07-25 · 约 7 分钟阅读

Node.jsCORS安全helmet响应头

57. CORS 与安全响应头

本节目标:- 同源策略为什么要存在,什么算「跨域」 - CORS 的简单请求和预检(preflight)两套逻辑 - 用 cors 包在 Express 里正确放行 - 用 helmet 给响应加上 CSP、HSTS 等安全头

你前端跑在 http://localhost:5173,后端在 http://localhost:3000——浏览器立刻给你报一个错:Blocked by CORS policy。这玩意儿几乎每个全栈新手都撞过。它背后是浏览器的同源策略(Same-Origin Policy),而 CORS 是浏览器和服务器之间约定的「跨域放行协议」。

这一章先讲清楚同源策略是什么、CORS 怎么工作,再讲一组能用 helmet 一键加上的安全响应头——它们跟鉴权一样,是生产环境的基本盘。

同源策略:浏览器的「门禁」

「同源」要求协议、域名、端口三者完全一致。下面这几个都和 https://example.com 不同源:

URL是否同源原因
https://example.com/page协议、域、端口全同
http://example.com协议不同
https://api.example.com域名不同
https://example.com:8080端口不同

同源策略限制了网页里的脚本不能随便读另一个源的资源。这是道安全门:否则你登录了银行,随便一个恶意网页就能用你的身份去调银行接口。

但现代前后端分离开发,跨域请求是刚需。CORS(跨源资源共享,Cross-Origin Resource Sharing)就是给这道门加了把「白名单锁」——服务器在响应头里说「我允许来自某某源的请求」,浏览器才放行。

CORS 的两种请求

浏览器把跨域请求分两类,处理方式不一样:

简单请求:满足一定条件(比如方法只用了 GET/POST/HEAD,且没有自定义头)时,浏览器直接发,再根据响应头里的 Access-Control-Allow-Origin 决定是否让你读结果。

预检请求(preflight):如果带了自定义头(比如 Authorization: Bearer xxx)、或者用了 PUT/DELETE、或者 Content-Typeapplication/json,浏览器会发一个 OPTIONS 请求探路,问服务器「我接下来要这么干,你允许吗」,服务器同意了才发真正的请求。

服务器这边要回的关键响应头:

响应头作用
Access-Control-Allow-Origin允许哪些源,可填具体域名或 *
Access-Control-Allow-Methods允许哪些 HTTP 方法
Access-Control-Allow-Headers允许哪些请求头
Access-Control-Allow-Credentials是否允许带凭证(Cookie)
Access-Control-Max-Age预检结果缓存多久,省得每次都问

用 cors 包落地

手写这些头很烦,cors 包(2.8.x)一行就搞定:

npm install cors
import express from 'express';
import cors from 'cors';

const app = express();

// 默认:允许所有源(相当于 Access-Control-Allow-Origin: *)
app.use(cors());

app.get('/api/data', (req, res) => {
  res.json({ ok: true });
});

生产环境别用 * 裸放行,要指定可信的前端域名:

app.use(cors({
  origin: 'https://my-frontend.com',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
}));

带凭证的跨域

如果你的接口依赖 Cookie(比如上一章的 refresh token 种在 Cookie 里),必须两端配合:

app.use(cors({
  origin: 'https://my-frontend.com',
  credentials: true, // 允许浏览器带上 Cookie
}));
Warning

一旦 credentials: trueAccess-Control-Allow-Origin 不能再写 *,必须是一个明确的源,否则浏览器直接拒绝。这是 CORS 规范硬性规定的,很多人卡在这儿。前端那边发请求时也要 fetch(url, { credentials: 'include' })

CORS 防的不是服务器,是浏览器

得说清楚一个容易误解的点:CORS 不是服务器的安全防线。它本质上是浏览器在「收到跨域响应后,决定要不要把数据交给网页脚本」的规则。如果你用 curl 或者自己的后端去调那个接口,根本不受 CORS 限制——CORS 拦不住一个恶意的服务端程序,它只拦「浏览器里的网页脚本」。

所以别指望靠 CORS 来保密数据。真正该做的授权(谁有权读这个接口)还得靠第 56 章的鉴权中间件。CORS 只是用来声明「我的资源允许哪些网页来读」,是个协作约定,不是防火墙。

支持多个前端域名、正确处理预检

如果你的接口要同时给 app.example.comadmin.example.com 用,origin 就不能写死一个字符串。它可以是个函数,按请求动态判断:

app.use(cors({
  origin: (origin, callback) => {
    const allowed = ['https://app.example.com', 'https://admin.example.com'];
    // 允许白名单内的源,以及没有 origin 的非浏览器请求(如 curl、服务端调用)
    if (!origin || allowed.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('CORS 不允许该源'));
    }
  },
  credentials: true,
}));

// 显式处理预检 OPTIONS 请求(多数 cors 配置会自动处理,这里便于理解)
app.options('/api/data', cors());
Tip

如果你手动处理预检(没用 cors 包),记得对 OPTIONS 请求直接返回 204,并且把 Access-Control-Allow-* 头设全,否则浏览器不会发真正的请求。用 cors 包的话这些它都替你做好了。

helmet:一键安全响应头

CORS 解决的是「谁能调我」,但还有一类风险是「浏览器拿到我的响应后会干傻事」——比如执行了响应里的恶意脚本、被塞进钓鱼 iframe。这些靠一组安全响应头来防,helmet(7.x / 8.x 皆可)能帮你一键加上一整套。

npm install helmet
import helmet from 'helmet';

app.use(helmet()); // 一行加上默认全套安全头

helmet() 默认会设置这些关键头(不同版本略有差异):

  • Content-Security-Policy(CSP):告诉浏览器「只允许从哪些来源加载脚本/图片/样式」。它是防 XSS 的终极防线——即便有人往页面里注入脚本,只要来源不在白名单,浏览器就不执行。
  • X-Content-Type-Options: nosniff:禁止浏览器「猜」响应内容的 MIME 类型,防止 MIME 嗅探攻击。
  • X-Frame-Options / frameguard:阻止别人把你的页面塞进 <iframe> 搞点击劫持(clickjacking)。
  • Strict-Transport-Security(HSTS):告诉浏览器「之后一段时间里只准用 HTTPS 访问我」,防 SSL 剥离。
  • Referrer-Policy:控制请求里带多少来源信息出去,避免泄露敏感路径。
  • Permissions-Policy:限制网页能用哪些浏览器高级能力(摄像头、麦克风、地理位置等)。

自定义 CSP

默认 CSP 往往太严,你的前端/CDN 加载不了资源。按需放宽:

app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", 'https://cdn.jsdelivr.net'],
    styleSrc: ["'self'", "'unsafe-inline'"],
    imgSrc: ["'self'", 'data:', 'https://img.example.com'],
  },
}));
Tip

CSP 是防 XSS 最有效的一招,但它要求你对「页面会从哪加载东西」心里有数。上线前先用 Content-Security-Policy-Report-Only 模式跑一阵,把违规报告收集起来调白名单,别一上来就 defaultSrc: 'none' 把自家页面也锁死。

反向代理场景

如果你用 Nginx 这类反向代理在前面,helmet 设的头可能被代理覆盖或丢失。常见做法是让 Nginx 来设安全头,或者确认代理配置 proxy_pass_header 把头透传过去。两边只留一边设,避免冲突。

Warning

安全头不是「加了就无敌」。helmet 解决了很大一块,但它不替你做输入校验、不防逻辑漏洞。下一章专门讲输入校验和各种注入攻击的防护,那才是安全闭环里最硬的一块。

把 CORS 配成「最小必要放行」、把 helmet 安全头开起来,你的服务在浏览器侧就立起了两道像样的门。但攻击者更多时候是从「你信任的用户输入」下手——这才是下一章的重点。