背景
支付类页面被仿冒,靠的不是技术漏洞,而是「视觉几乎一致」。攻击者复制页面结构、复用配色,只把收款账户换掉,用户在实际操作中很难分辨。
这篇文章从工程角度梳理五个可以在前端与后端落地的核对点。它们都很基础,但真正在代码里落实的系统并不多。
一、账户标识(UID)必须显式展示并与订单绑定
多数支付页只展示脱敏后的账号信息,比如手机号后四位。问题在于:同一台设备上切换账号后,页面缓存的上一账号信息可能残留。
工程做法:
支付页顶部固定展示完整 UID(或可点击展开)
订单创建时把 uid 写进订单记录,支付回调时校验 order.uid === session.uid
不匹配直接拒绝,并记录审计日志
javascript
// 支付页初始化时强校验
const order = await fetchOrder(orderId);
if (order.uid !== currentUser.uid) {
report({ type: 'uid_mismatch', orderId, expect: currentUser.uid, got: order.uid });
throw new Error('订单与当前账户不匹配');
}
这一步拦住的是「充错账户」,属于事后最难追回的一类。
二、金额三处一致:订单、支付页、回调
订单金额、支付页展示金额、支付渠道回调金额,三者必须严格相等。任何一处允许「手动调整」,都是风险入口。
工程做法:
金额只由服务端生成,前端只读展示
支付回调验签后,比对 callback.amount === order.amount
差额不为 0 时进入人工复核,不自动入账
php
if (bccomp($callback['total_fee'], $order['amount'], 2) !== 0) {
$this->markForReview($order['id'], 'amount_mismatch');
return false;
}
金额校验失败时不要自动退款也不要自动入账,冻结在待复核状态,比任何自动处理都安全。
三、域名与证书:Homograph 攻击的主要入口
仿冒域名最典型的手法是同形异义字符(Homograph):
真实字符 仿冒字符 视觉差异
m rn 极小
0 o 极小
l 1 极小
a а(西里尔字母) 几乎为零
工程做法:
支付页前端做域名白名单校验,location.hostname 不在白名单直接阻断
下单接口校验 Origin / Referer
关键操作页启用 HTTPS + HSTS,减少中间人注入
javascript
const ALLOW = ['pay.example.com', 'm.example.com'];
if (!ALLOW.includes(location.hostname)) {
document.body.innerHTML = '
页面来源异常,请从官方入口重新进入。
';throw new Error('hostname_not_allowed');
}
前端校验可以被绕过,它的价值在于「拦截普通用户误入仿冒页」,真正的防线仍然是后端对订单归属的校验。
四、凭证留存:结构化的三张记录
用户出事之后,客服需要的是可核验的字段,而不是一张截图。
记录 关键字段
订单 order_id、amount、uid、created_at
支付 channel、trade_no、paid_amount、paid_at
结果 status、balance_before、balance_after
工程做法:在支付结果页提供「导出凭证」功能,把上述字段生成为一张卡片或 PDF,用户可直接保存。这比让用户自己截图可靠得多,也减少了客服的沟通成本。
五、到账判断以服务端流水为准
客户端截图、聊天窗口里的「已到账」图片,在技术上都不构成证据——它们只证明「有人截了一张图」。
工程做法:
余额变动以服务端流水为准,前端只做展示
回调处理必须幂等:同一 trade_no 重复回调只入账一次
对账任务每天跑一次,比对支付渠道流水与本地订单
sql
-- 幂等入账:依赖唯一索引而非应用层判断
INSERT INTO balance_log (uid, order_id, trade_no, amount)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE id = id;
小结
五个核对点串起来是一条完整链路:
代码块
展示 UID → 绑定订单 → 校验金额 → 校验来源 → 以流水为准
前端负责「让用户看见并能自查」,后端负责「让错误无法落库」。两侧都做,才拦得住绝大多数仿冒与误操作。
完整核对清单整理在这里:s1.j-1.net