套上 Cloudflare 的那一刻,你的网站确实安全了、快了、扛得住打了。但没过多久,商户开始陆陆续续反馈:有的订单钱扣了,状态却一直是"待支付";有的回调时好时坏,同一笔订单一会儿到账一会儿不到账。你打开易支付后台一看,回调日志里一片红。

这就是典型的"套 CF 后回调失败"。它最折磨人的地方在于时好时坏、很难复现——你本地测试好好的,一上线就丢,关了 CF 又正常,开了又出问题。你开始怀疑是不是易支付的问题、是不是自己代码的问题、是不是 CF 抽风了。

这篇文章我一次性把根子说清楚。套 CF 导致回调失败,翻来覆去就两个源头:要么是"真实 IP 没还原",要么是"CF 的 WAF 规则把回调请求拦了"。这两个源头,网上能搜到的内容大多只讲了一半——讲还原 IP 的不讲 WAF 拦截,讲 WAF 的不讲 IP 被替换后验签为什么会失败。我把它们串起来,给你一套能直接照着排查的顺序。

先说明一点:如果你的回调验签本身都还没搞明白,建议先看支付回调 sign 签名:MD5 和 RSA 到底有什么区别这篇,把验签的原理吃透,下面讲"验签为什么失败"你才能对上号。如果你连回调地址设置都还没跑通,那先看易支付回调地址设置后不生效,那是更前置的问题。今天这篇,默认你的回调在没套 CF 之前是正常工作的,套了 CF 之后才出的问题。

第一步:先分清"回调丢了"还是"到了但验签失败"

这是整个排查的第一分水岭,也是最容易被跳过的一步。很多人一看到"回调失败",第一反应就是去查验签代码,结果方向全错,白折腾一晚上。

套 CF 之后,回调失败其实分两种完全不同的情况,它们的排查方向南辕北辙:

第一种:回调请求根本没到你的服务器。也就是说,易支付确实发起了回调,但请求在到达你源站之前就被 CF 拦下来了,或者回源失败了。这种时候,你的 Nginx 访问日志里压根没有这条回调记录,验签代码根本没机会执行。你看到的"失败",是易支付那边提示"回调超时"或"回调返回异常"。

第二种:回调到了,但验签没通过。请求成功到达源站,你的代码也跑了,但算出来的签名对不上,或者你做的来源校验没过,于是你返回了失败,易支付认为回调失败。

怎么快速区分?看两处:

  • 看 Nginx 访问日志。搜一下易支付回调的 URL(通常是 /notify 或 /pay/callback 之类)。如果日志里根本没有这条请求,那就是第一种——回调被 CF 拦了或回源失败。如果日志里有,但状态码是 200 之外的值,或者你的业务日志里报了"验签失败",那就是第二种。
  • 看易支付后台的回调日志。易支付通常会把每次回调的返回结果记下来。如果显示"连接失败"或"超时",是第一种;如果显示"验签失败"或"返回非 success",是第二种。

这个二分法一确立,后面你就不会瞎撞了。下面分别针对这两种情况,讲根子和解法。

真实 IP 还原:CF 三个头字段的真相

先说第二种情况里最常见的一个根子——真实 IP 没还原。

套了 CF 之后,你源站看到的客户端 IP,不再是用户真实的 IP,而是离你最近的那个 CF 边缘节点的 IP。CF 会把真实 IP 塞进请求头传给你,但它塞在哪、你该怎么取,这里面的门道,网上大多数教程都讲得含糊。

CF 处理 IP 相关头字段,实际是这样的:

  • CF-Connecting-IP:这是 CF 自己设置的头,里面是真实访问者的 IP。这是唯一可信的一个,因为它是 CF 在边缘节点直接写入的,中间没人能伪造。
  • X-Forwarded-For:CF 会往这个头里追加真实 IP,而不是覆盖。也就是说,如果原始请求自己带了一个 X-Forwarded-For: 1.2.3.4(这可以随便伪造),CF 会把它变成 1.2.3.4, 真实IP。你如果傻乎乎地取第一个值,拿到的是攻击者伪造的那个。
  • X-Real-IP:CF 默认根本不设置这个头。它是 Nginx 系反向代理的习惯用法,不是 CF 的。很多人在 Nginx 里配 real_ip_header X-Real-IP,套了 CF 之后发现拿到的还是 CF 节点 IP,就是因为这个头根本不存在。

所以正确做法是:信任 CF-Connecting-IP,并且只信任 CF 的 IP 段传来的这个头。对应的 Nginx 配置长这样:

# 只信任 Cloudflare 的 IP 段(从 CF 官方拉取,别偷懒)
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
# ... 其余 CF 的 IPv4/IPv6 段全部列上 ...
real_ip_header CF-Connecting-IP;
real_ip_recursive on;

这里有个极其关键、但网上很少点破的安全陷阱:很多人图省事,写成 set_real_ip_from 0.0.0.0/0;——意思是"信任所有来源的 XFF 头"。这等于把门彻底打开:任何一个攻击者都可以在自己请求里伪造 X-Forwarded-For: 127.0.0.1 或 X-Forwarded-For: 你的白名单IP,绕过你基于 IP 的访问控制。

为什么必须精确列出 CF 的 IP 段?因为 set_real_ip_from 的意思是"只有来自这些 IP 的请求,我才相信它带的 XFF/CF-Connecting-IP 头"。CF 节点 IP 是固定的、官方公开的,你从 https://www.cloudflare.com/ips-v4 和 ips-v6 拉下来列全即可。这样,只有真正经过 CF 的请求,它的头才会被采纳;攻击者绕过 CDN 直接打你源站、伪造 XFF,是无效的。

还原了真实 IP 之后,你回调代码里 $_SERVER['REMOTE_ADDR'] 拿到的才是有意义的真实地址。这一步,是后面所有"来源校验"和"IP 白名单"能正常工作的前提。

最隐蔽的坑:CF 质询页把服务器 POST 拦了

这是第一种情况(回调根本没到)里,最隐蔽、最容易被忽略的一个元凶,也是我认为网上几乎没人系统讲清楚的地方。

CF 有一类防护,叫"质询"(Challenge),包括三种:

  • Under Attack Mode(被攻击模式):对所有访客弹质询;
  • Bot Fight Mode(机器人战斗模式):对"疑似机器人"的请求弹质询;
  • WAF 自定义规则里的"质询"动作:你设了某条规则,命中就弹质询。

质询的原理,是给请求返回一个 JavaScript 挑战页(老的叫 JS Challenge,新的叫 Turnstile),要求浏览器执行 JS、通过校验后,才放行后续请求。

问题就出在这:易支付的回调,是易支付服务器到服务器发起的 POST 请求,它没有浏览器、不会执行任何 JavaScript。当这个 POST 命中质询时,CF 返回的不是"拒绝",而是一个质询页面。易支付那边拿到的是一个 HTML 质询页(或者连接被中断),既不是它预期的响应,也无法完成质询,于是回调就"静默丢失"了。

这个坑最阴险的地方在于:它不会稳定报错。比如 Bot Fight Mode,它对"疑似机器人"的判定有时效性和概率性,可能这秒拦、下一秒不拦,同一个易支付回源 IP 有时被判定为机器人、有时不会。于是你就看到"回调时好时坏",排查时它好了,走了它又坏,跟闹鬼似的。

更隐蔽的是,如果你开了 Under Attack Mode 或者某些 WAF 规则,它拦的不是"恶意流量",而是无差别地拦所有没通过质询的请求——包括你自己合法的支付回调。你在 CF 面板看着一切正常,因为浏览器访问你的网站是能过质询的,但服务器发起的回调过不了。

怎么解?核心思路是:让易支付的回调请求,绕开质询。具体几种做法:

  • 质询规则加例外:在 WAF 规则里,把易支付回调的 URL 路径(如 /notify*)设为"跳过"(Skip),并指定跳过"质询"这类动作。
  • 别用 Under Attack Mode 做常态:它只该在你真的被大规模攻击时临时开,平时关掉,换成更精准的自定义规则。
  • Bot Fight Mode 慎开:如果你的业务依赖服务器到服务器的回调(支付、Webhook 都是),Bot Fight Mode 误伤的概率不低,要么不开,要么配合例外规则。

判断是不是这个坑,也有个很直接的办法:去 CF 的"安全事件"或"防火墙事件"日志里,搜易支付回调的 URL 或回源 IP,看是否命中过"质询"(Challenge)动作。如果看到了,基本就是它。

易支付回源 IP 与防火墙放行

接着说第一种情况的另一个常见根子:你的 CF 防火墙规则,把易支付的回源 IP 给挡了。

很多站长为了安全,会在 CF 上设一些 IP 访问规则(IP Access Rules),比如"阻止某个国家/地区的访问""只允许某几个 IP 访问后台"。这些规则是无差别的,易支付的回源 IP 如果落在被阻止的范围内,回调请求也会被一并挡掉。

更常见的一种:你为了防 CC 攻击,设了"请求频率限制"或"只允许白名单 IP"。易支付的回源 IP 不在你的白名单里,或者它的回调频率触发了你的限流阈值,回调就被拒了。

解法分两步:

第一步,确认易支付的回源 IP 段。这个得向易支付平台要,或者从它在没套 CF 之前正常工作时、你 Nginx 日志里记录的历史回源 IP 去归纳。拿到之后,在 CF 的防火墙里为这些 IP 段设置"允许"规则,放在"阻止"规则之前(CF 规则按顺序匹配,允许规则要排前面)。

第二步,把回调路径从限流和地区封锁里摘出来。在限流规则里加例外,把 /notify 这类回调路径排除掉。回调是低频的、关键的请求,不该和普通页面流量共用一套限流逻辑。

这里再补一个容易踩的细节:CF 的 IP Access Rules 和 WAF 自定义规则是两套东西,优先级和生效方式不同。IP Access Rules 是"IP 维度"的全局规则,WAF 自定义规则是"请求维度"的。你要放行易支付回源 IP,可能要在这两处都检查一遍,别只改了一处就以为完事了。

签名里带 IP 或域名的坑

回到第二种情况(回调到了但验签失败)。除了"真实 IP 没还原",还有一个更隐蔽的根子:你的验签参数里,包含了 IP 或域名,而套 CF 之后这两样都变了。

易支付标准的验签,通常是把一堆参数按字典序拼起来,加上商户密钥算 MD5,对比 sign 参数。这套标准验签里,一般是不包含 IP 的。但问题出在两个方面:

一是你自己在回调处理里加了"来源 IP 校验"。很多人为了防止伪造回调,会在验签之外,再校验一下"请求来源 IP 是不是易支付的 IP"。这个思路本身没错,但套了 CF 之后,你拿到的 REMOTE_ADDR 是 CF 节点 IP,不是易支付 IP,于是你的来源校验全部失败——注意,这时不是易支付验签失败,而是你自己加的那层校验失败。很多人没分清这个,以为是易支付的问题。

二是某些聚合支付或二次封装平台的验签,确实会把 IP 或域名算进签名。如果你的上游是这种,套 CF 后客户端 IP 变成了 CF 节点 IP、回调域名可能经过了 CF 的重写(比如你配了 CNAME 接入、或 CF 改了 Host 头),签名参与值变了,算出来的 sign 自然对不上。

这两种情况的解法,都指向同一件事:先把真实 IP 正确还原(前面第二节的配置),再让验签和来源校验使用还原后的真实 IP,而不是 CF 节点 IP。如果验签参数里含 IP,那要确认"易支付期望的 IP 到底是哪个"——是它自己记录的、发起回调那一刻的客户端 IP,还是你源站看到的 IP。这个得对照易支付的文档或跟它确认,别想当然。

一套能直接照着走的排查顺序

把前面讲的串起来,给你一个套 CF 后回调失败的排查顺序,按顺序走,基本能一次定位:

第 1 步:二分法定位。看 Nginx 日志有没有这条回调请求。没有 → 走第 2 步;有但验签失败 → 跳第 4 步。

第 2 步:查 CF 拦截。去 CF 防火墙事件日志,搜回调 URL 和回源 IP,看有没有命中质询(Challenge)、阻止(Block)、限流。命中了质询 → 关 Under Attack Mode / Bot Fight Mode,或给回调路径加 Skip 例外;命中了阻止 → 放行易支付回源 IP。

第 3 步:查回源连通。确认 CF 到源站的 SSL 模式。如果是 Full (strict),源站证书必须有效且域名匹配,否则回源握手失败,回调也会丢。这一步常被忽略,但很容易查。

第 4 步:还原真实 IP。确认 Nginx 配了 set_real_ip_from(精确列 CF IP 段)+ real_ip_header CF-Connecting-IP,别用 X-Real-IP(CF 不设这个)、别用 0.0.0.0/0。

第 5 步:核对验签参数。确认验签参与参数里有没有 IP/域名,套 CF 后这些值有没有变化,来源校验用的是不是还原后的真实 IP。

这五步走完,套 CF 导致的回调失败,九成以上能定位到根子。剩下的那一成,多半是 CF 的某个具体功能(比如某些 301 重写、HTTP/2 到 HTTP/1.1 的转换)和易支付回源方式的兼容问题,那种就需要抓包逐段比对了。

几个容易忽略的边角,但都真实存在

最后补几个零散但真实存在的点,它们不常见,但一旦撞上就很磨人:

CF 的 Rocket Loader 和 Auto Minify 对 API 请求的影响。理论上这两个功能只处理 HTML/JS,不碰 API 的 POST,但如果你把回调 URL 意外配成了会被 CF 缓存或处理的路径,或者开启了"缓存一切"(Cache Everything),回调响应可能被缓存,导致易支付拿到的响应不对。回调路径务必设成 Cache Rule: Bypass,绝不缓存。

CF 改了 Host 头或 Origin 头。如果你的验签或业务逻辑依赖这些头的原始值,套 CF 后可能变了。排查时留意一下。

IPv6 的问题。如果你的源站只监听 IPv4,而 CF 回源走了 IPv6(或反之),回源会失败。确保源站对 CF 回源的地址族是通的。

这些边角不用死记,关键是建立一个意识:CF 不是透明的,它会改请求的 IP、头、甚至拦截请求本身。套 CF 之后,你的回调处理逻辑里所有"依赖客户端原始信息"的地方,都要重新审视一遍。

Cloudflare 是一把双刃剑——它帮你挡掉了攻击,也可能顺手挡掉你合法的回调。区别只在于,你有没有搞清楚它到底改了你请求里的哪些东西。搞清楚了,它就是盾;搞不清楚,它就是那个"时好时坏、查不出原因"的元凶。