Skip to content

一次 VPN 访问收紧导致支付回调中断的事故复盘

事故背景

一次后台业务系统发生非法访问并造成资金损失后,公司开始对后台管理系统进行安全加固。

其中一项核心措施是:

后台管理系统不再允许直接从公网访问,统一通过 VPN 进入。

这项整改时间窗口较短,因此多个后台系统很快完成了网络访问策略收紧。

教育系统也在这次范围内。

从表面看,这个系统属于“后台系统”,内部业务人员平时通过后台页面处理订单、审核、查询等业务,因此把它放进 VPN 是合理的。

但当时忽略了一个关键事实:

这个应用虽然是后台系统,但它同时还承担了第三方支付渠道的公网回调入口。

也就是说,同一个系统实际上同时存在两类完全不同的访问入口:

text
后台管理入口
→ 内部人员访问
→ 适合通过 VPN 收紧

支付回调入口
→ 微信、支付宝、连连支付等第三方服务器访问
→ 必须保持公网可达

当网络策略按照“整个系统”统一收紧以后,这两个不同信任边界被一起关进了 VPN。

事故由此产生。

故障发现

VPN 改造完成后,并没有立即出现明显的应用异常。

系统能够正常启动,业务人员也可以通过 VPN 登录后台。

真正的问题发生在支付完成后的异步通知链路。

两天后,客服人员首先反馈:

用户已经支付成功,但个人中心里的订单状态长时间仍然显示“未支付”。

客服平时会持续关注订单付款状态,因此最先发现了这一异常。

同时还有另一个旁证:

原本会同步业务动态的企微机器人,已经连续两天没有推送“订单付款成功”通知。

教育系统当时由单人同时承担研发、生产问题处理和部分运维配合工作。

VPN 改造完成后,没有独立的自动化机制持续验证支付回调链路是否仍然可用,因此问题没有被系统主动发现,而是最终由业务人员反馈暴露出来。

这次事故有一个很明显的特点:

定位很快,但发现很晚。

收到反馈后,第一时间联想到最近的 VPN 变更

收到客服反馈后,第一反应就是最近刚完成的 VPN 改造。

原因很直接。

用户已经完成支付,但系统订单仍然停留在待支付状态,这意味着:

text
用户支付动作可能已经完成

但支付结果没有进入系统

而教育系统中的支付状态主要依赖第三方渠道异步回调更新。

两天前又刚好做过一次全局网络入口收紧。

把这两个现象放在一起,故障方向就已经非常明确:

支付本身可能没有失败,真正中断的是支付成功后的 Callback 通知链路。

随后检查回调入口和网络策略,确认第三方公网请求已经无法进入该系统。

恢复 Callback 入口的公网可达性后,新产生的支付回调重新恢复正常。

但网络恢复只能解决后续请求。事故期间已经完成支付、却因为回调没有进入系统而仍停留在“待支付”状态的订单,还需要单独处理。

因此又筛选出事故时间窗口内的相关异常订单,并联系支付渠道对这些订单进行补偿回调,让系统重新收到支付结果通知并完成订单状态修复。

到这里,这次故障才真正完成恢复:

text
恢复 Callback 公网可达性

后续回调恢复正常

筛选事故期间异常订单

渠道补偿回调

历史订单状态修复

根因:把“系统边界”误当成了“信任边界”

事故根因并不是 VPN 本身。

VPN 作为后台管理入口的访问控制手段没有问题。

真正的问题是:

网络策略是按照“这个系统是后台系统”来制定的,而不是按照“这个入口是谁访问、需要什么信任模型”来制定的。

教育系统虽然在部署上是同一个应用,但从访问职责上看至少包含两类入口。

Internal:后台管理入口

例如:

text
/admin/**
/management/**

这类接口只供内部人员使用。

它们适合:

text
VPN
+
登录认证
+
权限控制

并且不应该直接暴露到公网。

Public Callback:第三方渠道回调入口

例如:

text
/callback/wechat/**
/callback/alipay/**
/callback/lianlian/**

这类接口的调用方不是内部人员,而是第三方支付渠道服务器。

它们无法进入企业 VPN,因此必须保持公网可达。

但“公网可达”并不意味着“无保护”。

它们应该采用另一套安全模型:

text
公网可达
+
严格验签
+
幂等处理
+
限流
+
必要的来源限制

所以真正需要划分的不是:

text
这个系统是内网系统还是公网系统

而是:

text
这个接口属于哪个信任边界

为什么支付成功了,订单状态却没有变化

支付流程里,用户付款成功和业务系统更新订单状态通常不是同一件事。

简化后的链路是:

text
用户

支付渠道

支付成功

支付渠道服务器

POST Callback

业务系统

更新订单状态

发送业务通知

VPN 改造以后,前半段仍然正常:

text
用户 → 支付渠道 → 支付成功

但后半段被网络策略切断:

text
支付渠道服务器

Callback

X 业务系统入口不可达

因此产生了一个典型的状态分裂:

text
资金状态:已经支付

业务状态:仍然待支付

企微机器人没有付款成功通知,也是同一个原因。

业务系统根本没有收到支付成功事件,自然也不会触发后续通知。

暴露出的第一个问题:变更前没有完成外部调用链梳理

这次网络变更的目标非常明确:

收紧后台管理系统的公网访问。

但在执行过程中,没有先把系统所有入口按调用方重新梳理一遍。

如果当时先列出:

text
谁从公网调用这个系统?
谁从 VPN 内调用这个系统?
哪些接口只供管理员使用?
哪些接口必须被第三方访问?

支付回调会很快暴露出来。

因此这次事故留下的第一个直接经验是:

涉及防火墙、VPN、安全组、Nginx 入口等底层网络策略的变更,不能只看“系统名称”,必须先看真实流量方向。

暴露出的第二个问题:紧急变更也需要最小风险检查

这次安全整改的时间窗口较短,网络策略需要快速完成收紧。

这类场景很难进行长时间设计评审,但仍然应该保留一个最小检查清单。

即使只有很短的准备时间,至少需要确认:

text
1. 谁从公网调用我?
2. 我必须继续暴露哪些公网接口?
3. 哪些第三方依赖这些入口?
4. 变更完成后如何验证关键链路?
5. 出问题以后如何快速回滚?

紧急变更不一定能做完整规划,但不能完全没有风险检查。

暴露出的第三个问题:关键链路过度依赖人工发现

这次事故真正值得警惕的不是“为什么没有马上猜到原因”。

实际上,一旦客服反馈出现,问题很快就和 VPN 变更关联起来了。

真正的问题是:

为什么支付回调已经中断两天,系统没有主动告诉维护者?

事故期间:

text
应用正常运行
后台可以正常登录
接口没有整体宕机

因此普通的:

text
进程存活监控
CPU
内存
HTTP 200

都无法发现这个问题。

真正应该关注的是业务链路监控。

例如:

text
支付订单创建量
支付成功查询量
Callback 到达量
订单从 TO_PAY → PAID 的转换量
付款成功通知量

如果平时每天都有付款成功回调,而某个时间点以后突然变成:

text
0

就应该触发告警。

因此这次事故进一步说明:

关键业务链路不能依赖某个人“记得去看”,应该尽可能通过自动化监控发现异常。

整改:重新划分 Internal 与 Public Callback 边界

事故恢复以后,并没有立即把整个应用拆成两个独立服务。

当时首先做的是重新梳理接口职责和访问边界。

逻辑上明确分为两类。

Internal

后台管理接口:

text
Internal
→ 管理员访问
→ VPN
→ 登录认证
→ 权限校验

Public Callback

第三方回调:

text
Public Callback
→ 第三方渠道访问
→ 公网可达
→ 不走后台登录态
→ 必须独立进行请求真实性校验

这是一种逻辑边界收敛。

即使它们暂时仍然部署在同一个 Spring Boot 应用里,网络和认证策略也不能继续按照“整个系统一套规则”处理。

后续在接口路径设计上,也应该明确区分职责,例如:

text
/api/**
→ 页面和业务接口

/callback/**
→ 第三方回调

让网络策略、Spring Security、网关规则和日志监控都能围绕路径职责进行配置。

公网 Callback 的安全模型不能只依赖网络封禁

第三方 Callback 必须公网可达,因此不能简单依赖“只有 VPN 才能访问”。

真正重要的是请求本身是否可信。

严格验签

支付渠道回调必须按照渠道协议进行签名校验。

只有验签成功的请求才能进入业务处理。

因此:

text
公网可达

任何请求都可信

而是:

text
公网可达
+
应用层验证请求真实性

幂等

支付渠道通常可能因为:

text
超时
响应丢失
网络抖动

重复发送 Callback。

因此同一笔支付回调必须可以安全重复处理。

例如:

text
TO_PAY → PAID

只允许合法状态迁移。

重复到达时不能重复扣减、重复入账或重复触发其他不可逆业务。

限流

Callback 接口可以针对异常请求频率增加限流,降低恶意请求或突发流量带来的影响。

但限流策略必须避免误伤合法渠道重试。

IP 白名单只能作为附加措施

如果某个渠道明确提供并维护稳定的服务器出口 IP 范围,可以进一步作为网络层的辅助限制。

但 IP 白名单不应该成为判断 Callback 真伪的唯一手段。

原因是:

text
第三方 IP 可能变化
CDN / 云环境可能调整
不同渠道网络模型不同

一旦白名单没有及时同步,仍然可能把合法回调挡在外面。

因此更合理的优先级是:

text
验签
→ 核心身份验证

IP 白名单
→ 条件允许时的附加防护

回调不能成为唯一的支付状态确认路径

这次事故还暴露出另一个问题:

如果支付结果完全依赖 Callback,一旦 Callback 因网络或第三方异常丢失,业务状态就可能长期停留在 TO_PAY。

更稳妥的支付状态链路应该有多级兜底。

第一层:Callback

实时主链路:

text
支付渠道

Callback

业务系统更新订单

这是最快的状态确认方式。

第二层:主动查询或补偿任务

对于长时间仍处于:

text
TO_PAY
PAYING

状态的订单,可以周期性调用支付渠道查询接口主动确认支付状态。

这样即使 Callback 偶发丢失,也可以在较短时间内修复业务状态。

第三层:T+1 对账

最终再通过渠道账单进行资金核对:

text
系统订单
vs
渠道账单

发现漏单、状态不一致时进行补偿处理。

T+1 对账的定位应该是:

最终资金一致性的兜底。

它不能替代实时 Callback,因为让用户等待到第二天才能看到正确支付状态,业务体验通常无法接受。

变更后的验证也应该覆盖业务链路

这次事故如果只验证:

text
管理员能不能通过 VPN 打开后台

那么结果一定是“变更成功”。

但真正完整的验证应该覆盖不同入口。

例如:

text
Internal
✓ VPN 内可以访问
✓ 公网不能直接访问

Public Callback
✓ 公网仍然可达
✓ 未签名请求被拒绝
✓ 合法签名请求正常进入业务

对于支付系统,还应该补一条真实或模拟链路:

text
创建测试订单

支付

收到 Callback

订单状态变更

通知正常发送

这也是这次事故以后最值得补上的变更验证方式。

最终回看:这不是 VPN 的问题,而是边界划分问题

如果只看表面现象,这次事故可以被描述成:

开启 VPN 后,支付回调挂了。

但 VPN 并不是根因。

真正的问题是:

把整个应用简单定义为“后台系统”,然后用同一套网络策略处理了所有入口。

而实际上:

text
后台管理入口
第三方支付回调

虽然部署在同一个服务里,却属于完全不同的信任边界。

所以这次事故最终留下的核心经验是:

不要按照“系统名称”决定网络边界,要按照真实调用关系和接口职责决定网络边界。

内部管理接口可以严格进入 VPN。

公网 Callback 也可以保持开放。

关键不是追求:

text
全部开放

或者:

text
全部关闭

而是明确:

text
谁需要访问
访问哪个入口
通过什么方式证明身份
失败以后如何补偿
异常以后如何及时发现

安全策略只有建立在真实调用关系之上,才能既收紧攻击面,又不切断正常业务链路。