一次H5页面跨域故障复盘:从CORS报错到两种方案的安全选型
前阵子在测试环境联调一个H5页面的业务接口。页面能开,接口在日志里也正常返回,可前端就是拿不到数据。F12一开,满屏 CORS error。
我盯着 Network 面板愣了几秒。后端说它回了,前端说它没收到。中间那道墙,是浏览器。
后端接口明明收到请求、正常返回,前端却拿不到响应。这篇从一次真实故障讲起,把跨域的本质、排查思路、两种落地方案和安全选型一次说清。生产环境可以直接照着用。

问题现象:明明通了,又没完全通
撞上的这个故障特别典型,说它矛盾,是因为四个现象凑在一起怎么看都不合理:
- 服务端链路完全正常:Apache反向代理调用后端
.do接口,请求接收、业务处理、数据返回全程无报错,日志干干净净。 - 前端H5请求失败:页面挂在
https://test.business.com:1443,Ajax调443端口的接口,浏览器控制台报CORS error,readyState=0,响应体是空的,前端一行数据都拿不到。 - 历史能用,突然挂了:1443这个页面长期跑得好好的,前端代码没动过,业务接口没改过,毫无征兆地开始报跨域。
- 换端口也没用:把页面迁到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 的事,容易踩坑也容易被误解。
一是依赖。上面两段配置都用到了 RewriteEngine 和 RewriteRule,前提是 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服务收到请求,在后台转发到真实后端接口,再把结果回给前端。
完整数据流:
- 前端JS请求:
https://test.business.com:1443/api/service.do(同端口,浏览器判同源) - 1443端口的Web服务收到,剥掉
/api前缀,转发到真实接口:https://test.business.com/service.do - 后端接口正常回数据,代理服务原样传回前端。
全程后端接口零改动,完全不知道有代理这层存在。
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获取 | 直接获取 | 需额外配置透传 |
选型建议
- 短期联调、接口变更成本低:优先CORS精准白名单,改动最小,链路最简单。
- 长期多渠道扩展、上游协调成本高:优先反向代理,一次配置,后面加前端页面都不用再动网关。
- 生产环境:不管选哪套,绝对禁止
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页面的服务上加跨域头,找错了对象,配半天也不生效。
总结
这次故障看着是个简单的跨域报错,背后其实串起了浏览器安全机制、网关配置、架构选型、安全合规好几层东西。
三条结论:
- 后端正常返回,不等于前端拿得到数据。浏览器的CORS校验是独立于后端业务的一道关,数据到了浏览器也可能被拦。
- 跨域问题本质就两类解法:让接口服务开放行证明(CORS配置),或者绕开浏览器校验(反向代理)。
- 生产环境永远别偷懒用
*。一时的方便可能换来高危漏洞,这类合规场景更是红线。