PAYATHON 2026

Laravel 如何获取 POST callback 请求数据

支付小周

结论

POST 路由本身没有问题。接收 callback 时,要根据请求的 Content-Type,使用 $request->all()、$request->input() 或 $request->getContent() 读取数据。

第三方 callback 收不到数据,常见原因有两个:

  • 第三方发送的是表单数据,代码却把请求体当作 JSON 解码。
  • callback 路由受 Laravel CSRF 校验保护。第三方请求没有 CSRF token,因此返回 419。

不建议用 dd() 调试线上 callback。dd() 会立即终止请求,第三方可能因此判断回调处理失败。更合适的方式是将数据写入日志,并返回明确的 2xx 响应。

接收并记录 callback 数据

可以先记录解析后的参数、原始请求体和请求头,以确认第三方实际发送的数据格式:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;

class PackController extends Controller
{
    public function callback(Request $request)
    {
        Log::info('Payment callback received', [
            'content_type' => $request->header('Content-Type'),
            'data' => $request->all(),
            'raw_body' => $request->getContent(),
        ]);

        return response('OK', 200);
    }
}

日志通常位于:

storage/logs/laravel.log

如果 Laravel 配置了其他日志 channel,则以项目的日志配置为准。

根据请求格式读取数据

表单格式

如果 callback 的 Content-Type 是:

application/x-www-form-urlencoded

或者:

multipart/form-data

可以使用:

$data = $request->all();

读取单个字段时,可以使用:

$orderId = $request->input('order_id');
$status = $request->input('status');

也可以只读取预期字段:

$data = $request->only([
    'order_id',
    'status',
    'price_amount',
    'price_currency',
]);

字段名称要以 callback 服务实际发送的参数为准。

JSON 格式

如果 Content-Type 是:

application/json

Laravel 通常也能直接解析:

$data = $request->all();

或者:

$data = $request->json()->all();

如果需要读取未经处理的原始 JSON,可以使用:

$rawBody = $request->getContent();

$data = json_decode($rawBody, true);

if (json_last_error() !== JSON_ERROR_NONE) {
    Log::warning('Invalid callback JSON', [
        'error' => json_last_error_msg(),
        'raw_body' => $rawBody,
    ]);

    return response('Invalid JSON', 400);
}

原来的代码:

json_decode($request->getContent(), true)

只有当请求体确实是 JSON 时才会得到数组。如果对方发送的是 URL 编码表单,json_decode() 会返回 null。

处理 CSRF 校验

定义在 routes/web.php 中的 POST 路由通常会经过 CSRF 中间件。第三方服务器无法提供当前 Laravel 应用生成的 CSRF token,所以 callback 可能返回 419 Page Expired。

只需为这个 callback 地址排除 CSRF 校验,不要关闭整个应用的 CSRF 保护。

如果 Laravel 项目通过 VerifyCsrfToken 中间件配置 CSRF,可以加入:

protected $except = [
    'callback',
];

部分项目的中间件文件位于:

app/Http/Middleware/VerifyCsrfToken.php

较新的 Laravel 项目可能会在 bootstrap/app.php 中配置例外路径,例如:

use Illuminate\Foundation\Configuration\Middleware;

->withMiddleware(function (Middleware $middleware) {
    $middleware->validateCsrfTokens(except: [
        'callback',
    ]);
})

具体写法取决于 Laravel 版本和项目结构。

也可以将第三方 callback 定义在 routes/api.php 中,但 API 路由可能会自动添加 /api 前缀。例如:

use App\Http\Controllers\PackController;
use Illuminate\Support\Facades\Route;

Route::post('/callback', [PackController::class, 'callback'])
    ->name('payment.callback');

此时,实际地址可能是:

https://example.com/api/callback

是否包含 /api 前缀,可以查看项目的路由配置,或运行:

php artisan route:list

关于请求方法错误

callback 路由定义为:

Route::post('/callback', [PackController::class, 'callback']);

这表示它只接受 POST 请求。直接在浏览器地址栏访问 /callback 时,浏览器发送的是 GET 请求,因此可能出现 405 Method Not Allowed。这并不代表第三方使用 POST 请求时,callback 路由的配置有问题。

本地可以这样测试:

curl -X POST https://example.com/callback \
  -H 'Content-Type: application/json' \
  -d '{"order_id":"123","status":"paid"}'

如果对方发送的是表单数据,可以这样测试:

curl -X POST https://example.com/callback \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data 'order_id=123&status=paid'

推荐的处理方式

完成调试后,可以将 callback 方法整理为:

public function callback(Request $request)
{
    $data = $request->all();

    Log::info('Payment callback received', [
        'data' => $data,
    ]);

    // 校验 callback 的真实性
    // 校验必要字段
    // 根据订单号查询订单
    // 幂等地更新支付状态

    return response('OK', 200);
}

还需要注意以下几点:

  • 不要在正式 callback 中使用 dd(),否则代码无法执行到后面的 return。
  • 应尽快返回第三方要求的成功状态码,通常是 200,具体仍要以对方当前的 callback 文档为准。
  • callback 可能因网络重试而重复发送,因此订单更新逻辑必须具备幂等性。
  • 不要只根据请求中的 status 确认付款。应按照服务商提供的签名、token、订单查询接口或其他验证机制,确认请求的真实性。
  • 生产环境中不要长期记录完整的支付信息、密钥、签名或个人数据。排查结束后,应减少日志字段。
  • callback URL 必须能被第三方服务器公开访问。localhost 或只能从内网访问的地址,通常无法接收线上 callback。

备注:内容仅供参考。