news 2026/9/7 19:32:44

Cloudflare Workers上运行Express API的CORS跨域配置实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Cloudflare Workers上运行Express API的CORS跨域配置实践

最近在给前端项目搭 API 服务,又是 CORS 问题把我卡住了。前端跑在 localhost:5173,后端接口部署在 Cloudflare Workers 上,浏览器直接报“has been blocked by cors policy: No 'Access-Control-Allow-Origin' header is present”。这个报错你们应该不陌生,尤其是用 Express 写 API 再部署到 Cloudflare 边缘场景的同学,十个里有九个会撞上。之所以拿这个组合出来写,是因为它确实有代表性:Express 是 Node 生态里最常见的 API 框架,Cloudflare Workers 又是目前最便宜的边缘部署方式之一,两边一拼,跨域配置稍有不注意就全线飘红。

这篇文章我会从一个实际可运行的 Express API 项目出发,讲清楚为什么会出现 CORS 拦截、在 Cloudflare Workers 上跑 Express 时有哪些适配工作、以及三种我从项目实践中整理出来的 CORS 解决方案。不管你是刚接触 Cloudflare Workers 的新手,还是已经被跨域问题折磨过几轮的老人,这篇都能给你一套直接能用的配置方案,照着抄就行。

1. 先搞清楚:为什么 Express API 部署到 Cloudflare 后会被 CORS 拦住

1.1 跨域问题的本质是什么

浏览器跨域拦截这事儿,很多人第一反应是后端搞的鬼,其实真正的“执法者”是浏览器本身。你的前端页面加载的时候,JavaScript 代码向另一个域名发起 AJAX 请求,浏览器会先检查响应头里有没有Access-Control-Allow-Origin,并且这个头是否匹配当前页面的域名。如果不匹配或没有,浏览器就拒绝把响应交给 JavaScript,控制台就出现那条经典的报错。

这里有个关键认知:API 服务器其实已经把数据返回了,只是浏览器帮你拦下来了。这也是为什么你用 Postman、curl 测接口一切正常,一放到浏览器里就炸的原因。理解这一点,排查 CORS 问题时你会容易很多——先确认请求是否真的到达了服务器、响应头是否正确,再考虑其他因素。

1.2 Cloudflare Workers 上的 Express 有什么特殊性

Cloudflare Workers 本身是运行在 V8 隔离环境里的,并不是传统的 Node.js 服务器。早期想在 Workers 里跑 Express,需要做不少适配,因为 Workers 的运行时模型是“一个请求进来,你返回一个 Response”,而 Express 是“监听端口,处理请求”。

但现在的官方生态已经推进得很完善了。你可以通过@cloudflare/workers-express这个官方适配包,以非常接近本地开发的方式运行 Express 应用。实际部署后,你的 Worker 域名是类似https://your-api.workers.dev的地址,而前端页面可能是http://localhost:5173https://your-frontend.pages.dev。这两个地址的源不同,浏览器自然要执行同源策略。

换句话说,CORS 问题在这个组合下几乎是必然出现的,除非你前端页面和 API 在同一个域名下,否则就必须在服务端显式处理跨域。所以标题里提到的“cloudflare 使用 express 实现 api 防止跨域 cors”,本质上做的是三件事:让 Express 应用在 Workers 上跑起来、正确设置响应头、处理预检请求。

2. 环境准备与项目初始化:让 Express 跑在 Cloudflare Workers 上

2.1 初始化项目与安装依赖

先把我实际用的项目结构展示给你。我习惯用 npm workspace 管理,但单项目也不复杂。创建一个新目录,然后初始化:

mkdir cloudflare-express-api cd cloudflare-express-api npm init -y npm install express @cloudflare/workers-express wrangler --save-dev

@cloudflare/workers-express是官方适配器,它会把 Worker 的 fetch 事件转发给 Express 应用实例去处理。安装wrangler用于本地调试和部署。如果你用的是 TypeScript,再补上typescript@types/express

2.2 入口文件怎么写

Workers 的入口文件和传统 Node.js 服务不同,不需要app.listen(),而是导出一个包含fetch方法的默认对象。基于官方适配器,代码可以写成这样:

// src/index.js import express from 'express'; import { createHandler } from '@cloudflare/workers-express'; const app = express(); app.get('/api/hello', (req, res) => { res.json({ message: 'Hello from Cloudflare Workers + Express!' }); }); export default createHandler(app);

这里的createHandler会返回一个 Worker 请求处理器,内部把 Web 标准的Request转换成 Express 风格的对象,再把 Express 返回的Response转回 Web 标准响应。这一步是适配核心,也解决了很多人手动包装时响应头丢失的问题。

2.3 配置 wrangler 与本地调试

项目根目录下新建wrangler.toml

name = "cloudflare-express-api" main = "src/index.js" compatibility_date = "2025-01-01" compatibility_flags = ["nodejs_compat"]

注意compatibility_flags里的nodejs_compat,Express 依赖 Node.js 的部分内置模块,打开这个标志位后才能在 Workers 里正常运行 Express。这是很多人忽略的点——不开nodejs_compat,本地跑得好好的,部署上去就开始报一些莫名其妙的模块错误。

启动本地开发:

npx wrangler dev

实测下来,本地调试体验和普通 Express 开发差别不大,改代码后热更新也生效。但要注意,如果你在本地用了app.listen(3000)这种方式,wrangler dev会直接白屏或报错,一定要用createHandler导出。

3. 三种 CORS 解决方案实测对比

3.1 方案一:手动中间件,零依赖、完全可控

我一开始用的是最原始的方式——写一个 Express 中间件,手动给每个响应加上 CORS 头。这个方案的好处是没有任何额外依赖,逻辑完全透明,适合想彻底搞清楚 CORS 原理的场景。

// src/middleware/cors.js export function corsMiddleware(req, res, next) { const allowedOrigins = [ 'http://localhost:5173', 'https://my-frontend.pages.dev' ]; const origin = req.headers.origin; if (allowedOrigins.includes(origin)) { res.setHeader('Access-Control-Allow-Origin', origin); } res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); res.setHeader('Access-Control-Max-Age', '86400'); if (req.method === 'OPTIONS') { return res.sendStatus(204); } next(); }

然后在入口里:

app.use(corsMiddleware);

这个方案的关键点在于,你必须在所有路由之前使用这个中间件。否则请求还没走到中间件就返回响应了,CORS 头自然加不上。另外一个容易踩坑的地方是:OPTIONS请求必须直接返回,不能再往后走业务逻辑,否则前端会收到一个非 2xx 的响应,预检照样失败。

3.2 方案二:使用 cors 包,一行代码搞定大部分需求

如果你不追求手写底层细节,直接用cors包是更高效的选择。这是 Express 社区的事实标准,支持各种配置项,绝大多数服务端框架都会用到它。

npm install cors
import cors from 'cors'; const corsOptions = { origin: ['http://localhost:5173', 'https://my-frontend.pages.dev'], methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization'], credentials: true, maxAge: 86400 }; app.use(cors(corsOptions));

这里有几个细节值得提醒:

  • origin可以传数组,也可以传函数,函数可以更灵活地判断请求来源。如果你需要根据环境变量动态配置允许的域名,用函数模式会很方便。
  • credentials: true表示前端可以携带 Cookie 和 HTTP 认证信息。很多人在这一步碰壁,因为一旦开启credentialsAccess-Control-Allow-Origin就不能是*,必须指定具体域名,浏览器会直接拒绝*和凭据同时出现的情况。
  • maxAge是预检请求结果缓存的时间,单位是秒。设成86400(一天)能显著减少浏览器重复发送OPTIONS的次数,对接口响应速度提升有帮助。

3.3 方案三:在 Worker 入口统一注入,适用于多路由或 Pages 场景

有时候你的 Worker 不只是跑 Express,还混了一些静态资源、重定向逻辑,或者你就是希望 CORS 逻辑独立在应用之外。这种情况下,可以在 Worker 的fetch阶段统一处理,而不是交给 Express 中间件。

// src/index.js import express from 'express'; import { createHandler } from '@cloudflare/workers-express'; const app = express(); app.get('/api/hello', (req, res) => { res.json({ message: 'Hello from Cloudflare Workers + Express!' }); }); const handler = createHandler(app); const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', 'Access-Control-Max-Age': '86400' }; export default { async fetch(request, env, ctx) { if (request.method === 'OPTIONS') { return new Response(null, { status: 204, headers: corsHeaders }); } const response = await handler.fetch(request, env, ctx); const newResponse = new Response(response.body, response); Object.entries(corsHeaders).forEach(([key, value]) => { newResponse.headers.set(key, value); }); return newResponse; } };

这个方案的好处是 CORS 逻辑和应用逻辑彻底解耦,以后哪怕你不跑 Express 了,这套头部注入逻辑依然能复用。坏处是要自己多写几行代码,而且得注意复制ResponsestatusstatusText和原有 headers,否则业务响应的状态码和内容类型可能丢失。

3.4 三个方案的取舍建议

方案优点缺点适用场景
手动中间件零依赖、逻辑透明、可精细控制每个响应代码量大、容易遗漏学习原理、极其简单的 API
cors 包配置方便、社区标准、覆盖绝大多数场景多一个依赖、默认*需注意大多数 Express 项目首选
Worker 入口统一注入与应用解耦、适合混合场景需要处理 Response 克隆多路由、需要全局控制的复杂项目

从我踩坑的经验看,单一 Express API 服务直接选 cors 包就够用,但一定要把origin从默认的*改成明确的域名列表。而如果你的 Worker 里还有其他非 Express 路由,或者你本来就在用 Pages 混合渲染,入口统一注入反而更省心。

4. 预检请求与凭据问题:最容易被忽略的两个坑

4.1 预检请求(OPTIONS)到底是什么

但凡涉及自定义 Header、非简单请求(比如Content-Type: application/json)、或者使用了PUT/DELETE方法,浏览器都会在正式请求之前先发一个OPTIONS请求,这叫“预检请求”。预检通过之后,浏览器才会真正发出业务请求。

很多人在后端看着日志里全是OPTIONS,还以为是有人攻击,其实这是浏览器的正常行为。你要做的不是屏蔽它,而是保证它得到正确响应:

  • 状态码必须是2xx,通常是204 No Content,也可以是200 OK
  • 必须返回Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-Headers
  • 响应体不需要内容,但Content-Length: 0或空 body 都行。

上面方案一和方案二都对这个情况做了处理,方案一里用res.sendStatus(204)专门拦 OPTIONS,方案二里cors包默认处理了预检。最容易出问题的反而是方案三,如果你在 Worker 入口没有对OPTIONS单独判断,预检请求会一直往下走,最终可能落到 Express 某个路由里,返回 404,前端就会报预检失败。

4.2 携带 Cookie 与 Authorization 时的特殊配置

如果你的 API 需要登录态,前端请求带着Authorization: Bearer xxx或者是 Cookie,这时 CORS 的配置要求会变得更严格:

  1. Access-Control-Allow-Origin不能是*,必须是具体的源;
  2. 必须设置Access-Control-Allow-Credentials: true
  3. Access-Control-Allow-Headers必须包含前端实际发送的 Header,比如Authorization

我自己在这块就吃过亏。一开始图省事,origin写的是*,前端登录接口一直报 CORS 错误,查了半天才发现是凭据和通配符冲突的问题。后来改成显式列出所有允许的域名,再加credentials: true,问题才彻底解决。

如果你不确定前端到底发了哪些 Header,直接打开浏览器的“网络”面板,看预检请求的Access-Control-Request-Headers,后端按这个值去配置准没错。

5. 常见报错与排查思路实录

5.1 “No 'Access-Control-Allow-Origin' header is present”

这是出现频率最高的报错,几乎每个做前后端分离的人都会遇到。它说明响应里根本没有 CORS 头。排查思路按优先级排序:

  1. 确认请求是否到达服务器:先看 Worker 日志或后端控制台,有没有对应的请求记录。没有记录,说明请求在更早的环节被拦截了,比如 WAF 规则、路由匹配失败;
  2. 确认响应是否真的带了 CORS 头:用 curl 模拟:
curl -i -H "Origin: http://localhost:5173" https://your-api.workers.dev/api/hello

看输出里有没有Access-Control-Allow-Origin。没有的话,说明你的中间件没有生效,或者顺序不对; 3.确认状态码:如果业务逻辑抛了异常,返回的是 500,某些错误响应可能没经过 CORS 中间件。Express 的错误处理中间件也要放在所有路由之后,并且把 CORS 头加上。

5.2 预检请求返回 404 或 500

预检请求如果返回的不是 2xx,浏览器会把整个请求拦截。常见原因有两个:

  • 路由里没有匹配OPTIONS的处理器,cors包或中间件没在路由之前执行;
  • Worker 入口对OPTIONS的直接响应逻辑缺失。

排查时可以先手动发一个OPTIONS请求,看返回码和响应头是否符合预期。如果后端框架或者网关层面做了鉴权,也要确保OPTIONS请求跳过鉴权,否则预检阶段就直接被 401 拒了。

5.3 接口 200 能通,但前端读不到数据

这种情况还挺隐蔽的:响应看起来正常,Access-Control-Allow-Origin也有,前端还是报错。原因往往是响应头里Access-Control-Allow-Origin的值和前端域名不匹配,比如配置了https://my-frontend.pages.dev,但实际页面在https://my-frontend.pages.dev/some/path。注意,CORS 匹配的是“源”,包括协议、域名、端口,但不包括路径。如果你在origin配置里带了路径,就会失败。

还有一个细节:如果是PathQuery参数触发 CDN 层缓存,可能你更新了 Worker 代码,但边缘节点还缓存了旧的响应头。排查时看响应头里的Cf-Cache-Status是否为HIT

5.4 常见问题速查表

报错信息大概率原因快速解法
No 'Access-Control-Allow-Origin' header响应未带 CORS 头确认中间件顺序,或在 Worker 入口统一注入
Preflight request failedOPTIONS 返回非 2xx拦截 OPTIONS 并返回 204,带全 CORS 头
Credentials flag is 'true' but header is '*'凭据和通配符冲突显式指定 origin,开启 credentials
Request header field x-custom is not allowed缺少 Allow-Headers 配置在 headers 里加上 Authorization、Content-Type 等
接口偶发 CORS 报错CDN 缓存了旧响应调整缓存规则或增加 Vary: Origin

6. 部署上线与验证:确保线上 CORS 真正生效

6.1 部署到 Cloudflare Workers

代码和配置都验证没问题后,部署其实就一条命令:

npx wrangler deploy

部署完成后,控制台会输出你的 Worker 域名,类似https://cloudflare-express-api.你的子域.workers.dev。如果你绑定了自定义域名,那就更好了,因为自定义域名可以和前端同源,从根上绕过 CORS 问题。

这里有个建议:生产环境尽量不要直接用*.workers.dev域名对外提供业务服务,一方面是企业版和免费版对这个域名有频率限制,另一方面是自定义域名可以统一走你自己的 CDN、WAF 策略,便于管理。

6.2 用 curl 和浏览器双重验证

部署完后,我用一条命令把 CORS 相关头全部打印出来,基本能搞定 80% 的排查:

curl -i -X OPTIONS \ -H "Origin: http://localhost:5173" \ -H "Access-Control-Request-Method: GET" \ -H "Access-Control-Request-Headers: Content-Type" \ https://your-api.workers.dev/api/hello

预期的响应头应该是:

HTTP/2 204 access-control-allow-origin: http://localhost:5173 access-control-allow-methods: GET, POST, PUT, DELETE, OPTIONS access-control-allow-headers: Content-Type, Authorization access-control-max-age: 86400

如果这一步没问题,再打开浏览器开发者工具,切到“网络”面板,发起一个真实请求,观察预检请求和实际请求的响应头。两个都通过,就说明 CORS 配置基本到位了。

6.3 一个值得留意的额外建议

最后,虽然标题和主题都在讲“防止跨域 CORS”,但我想提醒一句:CORS 只是浏览器层面的安全策略,它不能让你的 API 变成“公开免鉴权”的接口。真正要保护后端资源,还是得靠身份认证和授权。CORS 配置严格一些是好事,但别把它当成唯一的安全防线。

我在实际项目中还遇到过一个问题:开发环境经常需要调试多个前端项目,有时是 React 的 5173 端口,有时是 Vue 的 8080 端口。这时候把origin写成固定数组就很痛苦,每次还得改代码重新部署。我的做法是读环境变量:

const allowedOrigins = (process.env.ALLOWED_ORIGINS || '').split(',').filter(Boolean);

然后在 Cloudflare Workers 的环境变量里配置不同环境的域名列表。这样代码完全不用动,改环境变量就能调整跨域策略,线上也不容易因为误操作暴露接口。

还有一个实战技巧是配合Vary: Origin响应头使用。当你的Access-Control-Allow-Origin是根据请求的Origin动态变化时,建议把Vary也带上,避免 CDN 层缓存给不同来源的用户返回错误的 CORS 头。虽然 Workers 场景下缓存规则不完全等同于传统 CDN,加上这个头总归是更稳妥的做法。

以上基本覆盖了我这次“Cloudflare Workers 上运行 Express API 并处理 CORS”的完整过程。说实话,折腾一遍下来,感觉 CORS 本身并不复杂,坑大多出在环境适配和配置细节上。尤其是 Workers 这种边缘计算模型,和传统 Node.js 服务在生命周期、运行时行为上都有差异,照搬本地代码是行不通的。希望这篇能帮你少踩几个坑,把 API 平稳跑起来。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 19:32:03

编程题练习30天与计算机英语翻译23天:双线打卡的成长复盘

有没有过这种经历:收藏夹里躺着几十篇“刷题攻略”,却连第一页题都没看完;背单词App打卡三百天,真拿到一份英文技术文档还是读得磕磕绊绊。我之前也这样,直到把“编程题练习”和“计算机英语翻译”拆成两条独立的每日打…

作者头像 李华
网站建设 2026/9/7 19:31:57

网络安全从业人员必收藏的几个网站!

1 网安类知识库 (1)看雪知识库 https://www.kanxue.com/chm.htm (2)白阁文库 白阁文库是白泽Sec团队维护的一个漏洞POC和EXP披露以及漏洞复现的开源项目,欢迎各位白帽子访问白阁文库并提出宝贵建议。 https://wik…

作者头像 李华
网站建设 2026/9/7 19:30:21

太赫兹UM-MIMO与IRS混合信道估计:球面波与平面波联合稀疏恢复

最近这个太赫兹集成UM-MIMO和IRS系统的混合信道估计项目在仿真圈子里讨论度挺高,版本编号都出到14942期了。我也照着思路自己完整跑了一遍,把代码结构、信道建模、字典设计这些核心环节都重新捋清楚了。这个项目本质上不是单纯调一个函数就能出结果的dem…

作者头像 李华
网站建设 2026/9/7 19:29:21

css实现图片大小自适应

方法一:css的background属性来设置背景图知识点总结background的属性有以下这些: background-colorbackground-positionbackground-sizebackground-repeatbackground-originbackground-clipbackground-attachmentbackground-image1.background-color就不…

作者头像 李华
网站建设 2026/9/7 19:28:49

猫抓cat-catch安装使用教程:3分钟下载网页视频资源的完整流程

猫抓cat-catch安装使用教程:3分钟下载网页视频资源的完整流程 【免费下载链接】cat-catch 猫抓 浏览器资源嗅探扩展 / cat-catch Browser Resource Sniffing Extension 项目地址: https://gitcode.com/GitHub_Trending/ca/cat-catch 猫抓(cat-cat…

作者头像 李华