第三方回调接口必须实现快速响应、幂等保障与事务兜底:禁用csrf校验,直读原始body解析;签名验证需防重放与时序攻击,时间戳窗口±300秒,用hash_equals比对;核心更新逻辑置于db事务内,耗时操作异步化;幂等校验前置,优先用event_id或业务指纹(如provider+order_no+timestamp_minute哈希)结合redis setnx或数据库唯一约束。

第三方回调接口在高并发场景下极易出问题:重复处理、数据不一致、签名被重放、响应超时导致对方重发……关键不是“能不能跑通”,而是“能不能扛住、能不能防住、能不能只处理一次”。核心要抓住三点:快速响应、幂等保障、事务兜底。
路由与中间件必须精简,禁用 CSRF 并直读原始 Body
微信、支付平台、短信网关等回调请求不带 session 和 token,Laravel 默认的 VerifyCsrfToken 中间件会直接返回 419。这不是 bug,是设计冲突。
- 把回调路由注册在
routes/api.php(默认跳过 CSRF 校验),别放web.php - 若必须走 web 组,需在
app/Http/Middleware/VerifyCsrfToken.php的$except明确添加路径,如'webhook/*' - 别依赖
$request->json()或$request->all()—— 很多平台发的是纯 JSON 字符串或 XML,要用$request->getContent()拿原始体,再手动解析:$body = $request->getContent(); if (empty($body)) abort(400, 'Empty body'); - 微信回调用
text/xml,得用simplexml_load_string($body),不是json_decode
签名验证必须防重放 + 防时序攻击,且放在事务外
只比对 X-Hub-Signature 或 signature 字段远远不够。攻击者截包后延时重发,系统照样认。
- 强制要求第三方在 Header 中传
X-Timestamp(秒级时间戳),服务端检查是否落在 ±300 秒窗口内 - 签名比对必须用
hash_equals($expected, $given),严禁用===,否则存在时序攻击风险 - 密钥从配置读:
config('services.wechat.secret'),绝不硬编码 - 签名和时间戳校验必须在
DB::transaction()外完成——无效请求不该进数据库事务,避免无谓开销
用事务保证原子性,但关键操作要拆解
回调常含「验签→查订单→改状态→写日志→发通知」一连串动作。任一环节失败,前面已写的数据库记录就得回滚,否则出现“已付款但未发货”这类资损。
- 推荐用
DB::transaction()包裹核心更新逻辑(如更新orders.status、插入callback_logs) - 耗时操作(如调外部 API、发邮件、推消息)必须剥离到队列中异步执行,HTTP 响应前只返回 200
- 若需在主事务中触发后续动作但又怕失败影响一致性,可用保存点:
DB::transaction(function () { DB::getPdo()->exec('SAVEPOINT before_notify'); ... dispatch(new NotifyOrderPaid($order)); });
队列分发失败时,可ROLLBACK TO SAVEPOINT回退到该点 - 模型 Observer 不适合做回调校验主逻辑——它在事务提交后才触发,无法参与回滚
必须实现幂等控制,靠唯一事件 ID 或业务指纹
网络抖动、平台重试机制、负载均衡转发都可能导致同一回调被投递多次。不能靠“前端只点一次”来假设。
- 优先使用第三方提供的
idempotency-key或event_id字段:插入callback_events表时用INSERT ... ON CONFLICT DO NOTHING(PostgreSQL)或INSERT IGNORE(MySQL)去重 - 若平台不提供,构造业务指纹:拼接
provider + order_no + timestamp_minute取 SHA256,用 RedisSETNX callback_fingerprint:{$hash} 1 EX 3600占位 - Redis 故障时要有降级:记录告警日志,允许最多一次重试(通过本地内存缓存,仅限单机开发环境)
- 幂等校验必须放在所有数据库操作之前,包括权限检查和库存锁——否则校验失败后指纹还占着,用户无法重试











