从 CORS 报错到两套方案选型:一份能直接抄的跨域复盘

一次H5页面跨域故障复盘:从CORS报错到两种方案的安全选型

前阵子在测试环境联调一个H5页面的业务接口。页面能开,接口在日志里也正常返回,可前端就是拿不到数据。F12一开,满屏 CORS error

我盯着 Network 面板愣了几秒。后端说它回了,前端说它没收到。中间那道墙,是浏览器。

后端接口明明收到请求、正常返回,前端却拿不到响应。这篇从一次真实故障讲起,把跨域的本质、排查思路、两种落地方案和安全选型一次说清。生产环境可以直接照着用。

问题现象:明明通了,又没完全通

撞上的这个故障特别典型,说它矛盾,是因为四个现象凑在一起怎么看都不合理:

  1. 服务端链路完全正常:Apache反向代理调用后端 .do 接口,请求接收、业务处理、数据返回全程无报错,日志干干净净。
  2. 前端H5请求失败:页面挂在 https://test.business.com:1443,Ajax调443端口的接口,浏览器控制台报 CORS errorreadyState=0,响应体是空的,前端一行数据都拿不到。
  3. 历史能用,突然挂了:1443这个页面长期跑得好好的,前端代码没动过,业务接口没改过,毫无征兆地开始报跨域。
  4. 换端口也没用:把页面迁到9090端口,故障一模一样地复现,还是 CORS error

最让人懵的是:后端明明把数据吐出来了,前端为什么就是接不住?

先搞懂本质:什么是跨域,为什么后端正常前端报错

同源策略:浏览器的底层安全规则

跨域问题的根,是浏览器的同源策略。它要求两个地址的协议、域名、端口三者完全一致,才算同源。任意一项对不上,就是跨域。

套回我们的场景:

  • 页面地址:https://test.business.com:1443/index.html
  • 接口地址:https://test.business.com/service.do(默认HTTPS 443端口)

协议、域名都对得上,唯独端口 1443 ≠ 443。所以浏览器判它跨域。

为什么服务端调用永远不跨域

很多人会问:同一个接口,Apache在后端调就一切正常,浏览器里就不行?

差别在请求是谁发出的。服务端和服务端之间发HTTP调用,没有浏览器掺和,不受同源策略管,想调谁调谁。浏览器里的JS发请求,必须遵守同源策略,这是浏览器为了保护用户数据安全硬设的一道墙。

CORS:浏览器给的放行条

CORS(跨域资源共享)是浏览器给出的跨域解法。核心就一句:跨域响应能不能交给前端JS,由被请求的接口服务说了算。接口服务得在响应头里带上 Access-Control-Allow-Origin 这类字段,明明白白告诉浏览器,哪些域名的页面可以读我的返回。

回到故障:后端接口确实收到了请求,数据包也完整返回了。但响应头里没有合法的跨域放行标记,浏览器收完包直接拦下丢弃,拒绝把响应交给页面JS。

接口回了数据。浏览器把那包数据扣下了,页面JS什么都没拿到。

根因定位:四步排查锁定真相

第一步:看Network面板,实锤跨域

打开F12的Network面板,直接看到 service.do 请求标着 CORS error,基本就能定性跨域了,不用再怀疑接口业务逻辑。

第二步:核对三要素,确认成因

把页面地址和接口地址一比,端口对不上,这是最直接的跨域触发点。

这里有个常见误区:不少人以为域名相同就不算跨域。其实端口是同源三要素之一,哪怕只差一个数字,也是标准跨域。

第三步:curl模拟预检,验证配置丢失

为什么以前好好的,现在突然报错?最可能是接口侧的CORS配置丢了。

用curl模拟浏览器的OPTIONS预检请求,直接验证接口有没有回跨域头:

bash 复制代码
curl -I -X OPTIONS \
-H "Origin: https://test.business.com:1443" \
https://test.business.com/service.do

回来看响应头,完全没有 Access-Control-Allow-Origin 这些跨域字段,实锤接口侧CORS配置失效。

第四步:定位配置失效原因

最后确认:接口侧的Apache网关前段时间做了一次配置重载,原有的跨域Header配置被新版本覆盖,而且少了 always 参数。预检请求拿不到跨域头,浏览器直接拦。

方案一:接口侧配CORS白名单

最直接的解法,是在提供接口的Apache网关(443端口)补上跨域放行配置。分两个版本。

临时应急版(仅测试环境联调慎用)

最快解决问题的写法,直接放行所有来源:

apache 复制代码
Header always set Access-Control-Allow-Origin "*"
Header always set Access-Control-Allow-Methods "GET, POST, OPTIONS"
Header always set Access-Control-Allow-Headers "*"

RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=204,L]

这套只适合测试环境快速联调。生产环境绝对不能上 * 通配符,后面细说风险。

生产标准安全版(生产环境适用)

用精准域名白名单,只放行可信前端来源,同时兼容凭证鉴权:

apache 复制代码
SetEnvIf Origin "^https://test.business.com:(1443|9090)$" FRONT_ORIGIN=$0
SetEnvIf Origin "^https://prod.business.com$" FRONT_ORIGIN=$0

Header always set Access-Control-Allow-Origin "%{FRONT_ORIGIN}e" env=FRONT_ORIGIN
Header always set Access-Control-Allow-Methods "GET,POST,OPTIONS" env=FRONT_ORIGIN
Header always set Access-Control-Allow-Headers "*" env=FRONT_ORIGIN
Header always set Access-Control-Allow-Credentials "true" env=FRONT_ORIGIN

RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=204,L]

补充两点关于 RewriteEngine 的事,容易踩坑也容易被误解。

一是依赖。上面两段配置都用到了 RewriteEngineRewriteRule,前提是 Apache 已经加载 mod_rewrite 模块。模块没加载却写了 RewriteEngine,Apache 启动或重载会直接报错:Invalid command 'RewriteEngine', perhaps misspelled or defined by a module not included in the server configuration。要用这段,记得在配置里补一行:

apache 复制代码
LoadModule rewrite_module modules/mod_rewrite.so

二是可省。如果 mod_rewrite 已加载,但没写 RewriteEngine On,那几行 RewriteRule / RewriteCond 会被静默忽略,OPTIONS 预检请求会直接透传到后端接口,不会在网关层被拦成 204。后果不是报错。预检会继续往后走。只要后端接口本身能正确响应 OPTIONS(返回 204 并带 CORS 头),业务就完全不受影响。我自己的配置就是没写 RewriteEngine On,预检照样过,因为后端兜住了。

所以这段 rewrite 不是必选项。它的作用只是让网关在预检阶段就直接回 204、不进业务逻辑,省一次后端调用。后端能处理 OPTIONS 的话,不写也完全没问题。

这套方案的特点

  • 优点:前端代码不用改请求地址,链路最短,性能损耗最小。
  • 缺点:得协调接口侧团队改网关配置,运维自主权低;前端每加一个新域名,就得回头改一次网关。

方案二:页面侧反向代理,绕开跨域

如果协调不上游接口服务改配置,反向代理是最稳的替代。思路一句话:让浏览器看不到跨域。

原理:把跨域请求变成同域转发

不让前端JS直接打443端口的接口,改成调页面自己同端口的代理路径。承载H5的Web服务收到请求,在后台转发到真实后端接口,再把结果回给前端。

完整数据流:

  1. 前端JS请求:https://test.business.com:1443/api/service.do(同端口,浏览器判同源)
  2. 1443端口的Web服务收到,剥掉 /api 前缀,转发到真实接口:https://test.business.com/service.do
  3. 后端接口正常回数据,代理服务原样传回前端。

全程后端接口零改动,完全不知道有代理这层存在。

Apache配置示例

在承载H5页面的1443虚拟主机里加:

apache 复制代码
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_ssl_module modules/mod_proxy_ssl.so
LoadModule proxy_http_module modules/mod_proxy_http.so

<VirtualHost *:1443>
    # 原有页面、SSL配置保持不变
    
    ProxyPass "/api/" "https://test.business.com/"
    ProxyPassReverse "/api/" "https://test.business.com/"
    ProxyPreserveHost On
</VirtualHost>

这套方案的特点

  • 优点:完全不动上游接口服务,配置自主权在己方;前端加新渠道,不用再回头协调网关。
  • 缺点:前端得改接口请求地址;多一层转发,有轻微性能损耗;真实客户端IP得额外透传。

两种方案怎么选:安全与架构的权衡

团队常卡在两种方案怎么取舍。我从生产系统最在意的几个维度拉了一张对比:

对比维度 CORS精准白名单 反向代理
安全等级(规范配置下) 相当,均可控 相当,均可控
安全风险(配置不规范时) 极高,误用 * 会开放全网调用 较低,不改变接口暴露面
运维自主权 低,依赖上游网关团队 高,己方完全可控
架构扩展性 差,新增前端域名需改网关 好,一次配置长期复用
链路长度 短,无额外转发 多一层代理节点
真实客户端IP获取 直接获取 需额外配置透传

选型建议

  1. 短期联调、接口变更成本低:优先CORS精准白名单,改动最小,链路最简单。
  2. 长期多渠道扩展、上游协调成本高:优先反向代理,一次配置,后面加前端页面都不用再动网关。
  3. 生产环境:不管选哪套,绝对禁止 Origin: * 通配符。

几个最容易踩的CORS坑

坑1:Header set 漏了 always,时好时坏

Header set 只在2xx成功响应时加头,OPTIONS预检、4xx/5xx异常响应都不会带跨域头,于是偶发跨域报错。

正确写法:统一用 Header always set,保证所有状态码的响应都带上跨域字段。

坑2:withCredentials 配上 Origin:* 直接失效

前端开了 withCredentials: true(带Cookie/Token鉴权)时,浏览器强制禁止 Access-Control-Allow-Origin* 通配符。两者共存,跨域直接失败。

生产系统几乎都走凭证鉴权,这是最高频的坑。

坑3:不处理OPTIONS预检,POST发不出去

带自定义请求头的POST属于复杂跨域请求,浏览器会先发OPTIONS预检。后端不处理OPTIONS,预检被业务逻辑拦下返回403/404,真正的业务请求根本不会发出。

坑4:把CORS当安全屏障,是大错

CORS只是浏览器层面的限制,只能管网页里的JS。对curl、Postman、爬虫、后端服务调用,CORS完全没用。

接口的安全防护得靠Token、签名、会话校验,绝不能指望CORS做安全拦截。

坑5:在页面服务加CORS头,白忙活

CORS响应头必须由被请求的接口服务返回才有效。不少人在承载H5页面的服务上加跨域头,找错了对象,配半天也不生效。

总结

这次故障看着是个简单的跨域报错,背后其实串起了浏览器安全机制、网关配置、架构选型、安全合规好几层东西。

三条结论:

  1. 后端正常返回,不等于前端拿得到数据。浏览器的CORS校验是独立于后端业务的一道关,数据到了浏览器也可能被拦。
  2. 跨域问题本质就两类解法:让接口服务开放行证明(CORS配置),或者绕开浏览器校验(反向代理)。
  3. 生产环境永远别偷懒用 * 。一时的方便可能换来高危漏洞,这类合规场景更是红线。
相关推荐
凤山老林1 天前
从美团全栈化看 AI 冲击:前端转全栈,是自救还是必然
前端·人工智能·状态模式
码云数智-大飞2 天前
前后端沟通总吵架,一套高效协作规范分享
状态模式
不吃辣4902 天前
vibe coding | 如何做一个skill?
ai·状态模式
Draina3 天前
CBC填充预言攻击-CBC Padding Oracle Crypto Attack
python·安全·web安全·网络安全·密码学·安全性测试
早点睡啊Y3 天前
深入学LangChain官方文档(二十二):Frontend 高级形态——Headless Tools、Time Travel 与 Generative UI
ui·langchain·状态模式
xxwl5854 天前
markdown基础语法
状态模式
三川6984 天前
深入浅出SSD 01:SSD综述
状态模式
cyadyx4 天前
MapStruct 的转换实践
状态模式·mapstruct·充血模式
c238569 天前
第二篇:《测试指挥官:可视化单题自测框架(含 assert 实操)》
java·数据库·c++·算法·安全性测试