ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

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:5173或https://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-devcloudflare/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.tomlname cloudflare-express-api main src/index.js compatibility_date 2025-01-01 compatibility_flags [nodejs_compat]注意compatibility_flags里的nodejs_compatExpress 依赖 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 corsimport 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 了这套头部注入逻辑依然能复用。坏处是要自己多写几行代码而且得注意复制Response的status、statusText和原有 headers否则业务响应的状态码和内容类型可能丢失。3.4 三个方案的取舍建议方案优点缺点适用场景手动中间件零依赖、逻辑透明、可精细控制每个响应代码量大、容易遗漏学习原理、极其简单的 APIcors 包配置方便、社区标准、覆盖绝大多数场景多一个依赖、默认*需注意大多数 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-Origin、Access-Control-Allow-Methods、Access-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 的配置要求会变得更严格Access-Control-Allow-Origin不能是*必须是具体的源必须设置Access-Control-Allow-Credentials: trueAccess-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 头。排查思路按优先级排序确认请求是否到达服务器先看 Worker 日志或后端控制台有没有对应的请求记录。没有记录说明请求在更早的环节被拦截了比如 WAF 规则、路由匹配失败确认响应是否真的带了 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配置里带了路径就会失败。还有一个细节如果是Path或Query参数触发 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开启 credentialsRequest header field x-custom is not allowed缺少 Allow-Headers 配置在 headers 里加上 Authorization、Content-Type 等接口偶发 CORS 报错CDN 缓存了旧响应调整缓存规则或增加 Vary: Origin6. 部署上线与验证确保线上 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 平稳跑起来。
返回列表