PAYATHON 2026

Laravel 集成 GoPay REST API 时无法设置请求头

支付阿杰

结论

Laravel 通常不会修改或拦截 GoPay SDK 通过 cURL 设置的出站请求头。Laravel 的 Request、中间件和 header() 主要处理浏览器发给应用的入站请求,以及应用返回给浏览器的响应。它们不会自动介入 SDK 单独创建的 cURL 请求。

同一段代码在普通 PHP 脚本中可以运行,在 Laravel 中却失败,常见原因包括:

  • 两段代码运行在不同的 PHP 环境中,例如 CLI 与 PHP-FPM 加载了不同的 php.ini。
  • Laravel 加载了不同版本的 GoPay SDK 或依赖包。
  • .env、配置缓存或命名空间导致 SDK 读取了错误的配置。
  • 获取 access token 的请求已经失败,后续请求因而无法生成 Authorization: Bearer ...。
  • 实际请求经过代理或重定向,Authorization 在转发时被删除。
  • 检查的是 Laravel 的入站请求头或响应头,并非 SDK 发出的 cURL 请求头。

排查时不必先尝试让 Laravel 修改请求头。应依次检查运行环境、SDK 版本、token 请求,以及 cURL 实际发送的数据。

首先处理已经暴露的凭据

示例中包含 clientId 和 clientSecret。如果它们是真实凭据,应立即在 GoPay 后台撤销或更换。不要继续使用,也不要提交到 Git 仓库。

在 Laravel 中,应通过环境变量保存这些配置:

GOPAY_GOID=your_goid
GOPAY_CLIENT_ID=your_client_id
GOPAY_CLIENT_SECRET=your_client_secret
GOPAY_PRODUCTION=false

然后在配置文件中读取:

// config/services.php

return [
    'gopay' => [
        'goid' => env('GOPAY_GOID'),
        'client_id' => env('GOPAY_CLIENT_ID'),
        'client_secret' => env('GOPAY_CLIENT_SECRET'),
        'production' => env('GOPAY_PRODUCTION', false),
    ],
];

控制器应使用 config(),不要直接在业务代码中调用 env():

$gopay = \GoPay\payments([
    'goid' => config('services.gopay.goid'),
    'clientId' => config('services.gopay.client_id'),
    'clientSecret' => config('services.gopay.client_secret'),
    'isProductionMode' => (bool) config('services.gopay.production'),
    'scope' => \GoPay\Definition\TokenScope::ALL,
    'language' => \GoPay\Definition\Language::CZECH,
], [
    'logger' => new \GoPay\Http\Log\PrintHttpRequest(),
]);

修改 .env 后,如果项目启用了配置缓存,需要执行:

php artisan config:clear
php artisan cache:clear

需要在生产环境重新生成配置缓存时,再执行:

php artisan config:cache

区分入站请求头与 SDK 的出站请求头

下面的代码只能查看浏览器或客户端发给 Laravel 的请求头:

request()->headers->all();

下面的函数通常只能查看 PHP 准备返回给浏览器的响应头:

headers_list();

这两种方式都无法证明 GoPay SDK 发出的 cURL 请求是否包含 Authorization、Accept 或 Content-Type。

GoPay SDK 创建的是一条独立的 HTTP 请求链:

浏览器 -> Laravel -> GoPay SDK/cURL -> GoPay API

Laravel 中间件处理左侧的请求。SDK 的请求头位于右侧,需要通过 SDK 日志、cURL verbose 输出、代理抓包或 GoPay 返回的错误信息来检查。

先确认 token 是否获取成功

支付请求中的 Authorization 通常依赖 SDK 事先取得 access token。如果认证请求失败,后续支付请求就无法携带有效的 Bearer token。

调用接口后,除了支付数据,还应记录完整的状态和响应体。具体方法取决于项目安装的 GoPay SDK 版本,例如:

$response = $gopay->createPayment($paymentData);

logger()->debug('GoPay response', [
    'status_code' => method_exists($response, 'statusCode')
        ? $response->statusCode()
        : null,
    'json' => method_exists($response, 'json')
        ? $response->json()
        : null,
]);

if (method_exists($response, 'hasSucceed') && !$response->hasSucceed()) {
    logger()->error('GoPay request failed', [
        'status_code' => $response->statusCode(),
        'response' => $response->json(),
    ]);
}

不要记录完整的 Authorization、clientSecret 或 access token。需要检查请求头时,应对敏感值进行脱敏:

Authorization: Bearer ********

认证阶段需要重点检查以下错误:

  • 401 Unauthorized
  • invalid_client
  • DNS 解析失败
  • SSL certificate problem
  • Connection timeout
  • 无法连接 GoPay sandbox
  • 请求被发送到错误的生产或测试地址

isProductionMode => false 表示使用测试环境,因此 client ID、secret 和 GoID 也必须属于对应的测试环境。混用生产凭据和测试地址会导致认证失败,很容易被误判为”请求头没有设置”。

比较 Web 与 CLI 的 PHP 环境

普通脚本经常通过命令行运行,而 Laravel controller 通常由 PHP-FPM 或 Apache PHP 模块执行,两者可能加载完全不同的配置。

先在命令行中检查:

php --ini
php -m | grep curl
php -i | grep -E "cURL support|curl.cainfo|openssl.cafile"
php -v

然后临时在 Laravel 路由或控制器中记录 Web 环境:

logger()->debug('PHP web environment', [
    'php_version' => PHP_VERSION,
    'php_sapi' => PHP_SAPI,
    'loaded_ini' => php_ini_loaded_file(),
    'curl_loaded' => extension_loaded('curl'),
    'curl_version' => extension_loaded('curl') ? curl_version() : null,
    'curl_cainfo' => ini_get('curl.cainfo'),
    'openssl_cafile' => ini_get('openssl.cafile'),
]);

比较以下项目:

  • PHP 版本
  • PHP_SAPI
  • 实际加载的 php.ini
  • cURL 扩展是否启用
  • libcurl 和 OpenSSL 版本
  • CA 证书路径
  • open_basedir
  • 网络代理相关环境变量
  • PHP-FPM 用户是否有权读取 CA 文件

如果 CLI 启用了 cURL 扩展,而 PHP-FPM 没有,就需要为 Web 使用的 PHP 安装或启用该扩展,并重启 PHP-FPM 或 Web Server。只重启 Artisan 命令不会生效。

检查 Composer 实际加载的 SDK

两个项目都调用了 GoPay\payments(),并不代表它们使用的 SDK 完全相同。可以执行以下命令进行比较:

composer show | grep -i gopay
composer show gopay/payments-sdk-php
composer why gopay/payments-sdk-php

包名应以项目的实际安装结果为准。

还要确认 Laravel 加载的是 Composer 提供的类,而不是项目中的同名 facade、辅助函数或旧版手动复制文件。可以通过反射确认实际加载的文件:

$reflection = new ReflectionClass(\GoPay\Api::class);

logger()->debug('Loaded GoPay class', [
    'file' => $reflection->getFileName(),
]);

这里的类名也应按照实际安装的 SDK 进行调整。需要确认的是运行时加载的文件路径和版本。

控制器顶部还应明确导入相关类型:

use GoPay\Definition\BankSwiftCode;
use GoPay\Definition\Currency;
use GoPay\Definition\Payment\PaymentInstrument;
use GoPay\Definition\Payment\Recurrence;

如果导入路径与当前 SDK 版本不一致,PHP 可能解析到错误的类,也可能在真正发起请求前就已经失败。

直接验证 Laravel 运行环境能否通过 cURL 发送请求头

要区分 SDK 问题和 PHP-FPM/cURL 环境问题,可以在同一个 Web 运行环境中进行最小化诊断。测试地址应使用自己控制的调试端点,不要将 GoPay secret 或真实 token 发送到第三方回显服务。

$ch = curl_init('https://your-controlled-endpoint.example/debug');

curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => json_encode(['test' => true]),
    CURLOPT_HTTPHEADER => [
        'Accept: application/json',
        'Content-Type: application/json',
        'Authorization: Bearer test-token',
    ],
    CURLINFO_HEADER_OUT => true,
]);

$body = curl_exec($ch);
$sentHeaders = curl_getinfo($ch, CURLINFO_HEADER_OUT);
$errorNumber = curl_errno($ch);
$errorMessage = curl_error($ch);
$statusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

curl_close($ch);

logger()->debug('cURL diagnostic', [
    'headers' => preg_replace(
        '/Authorization:.*\R/i',
        "Authorization: Bearer ********\r\n",
        $sentHeaders
    ),
    'status_code' => $statusCode,
    'curl_errno' => $errorNumber,
    'curl_error' => $errorMessage,
    'body' => $body,
]);

如果日志中能看到这三个请求头,就说明 Laravel 没有阻止 cURL 设置 header。接下来应检查 SDK 版本、认证流程和配置值。

如果这项测试也失败,则需要检查 PHP-FPM 的 cURL、SSL、CA 证书、代理设置和网络权限。

检查代理与重定向

Authorization 是敏感请求头。当请求跨主机重定向时,cURL、HTTP 客户端或中间代理可能不会将它继续发送到新主机。这是正常的安全策略。

需要检查以下项目:

  • GoPay URL 是否配置成旧地址。
  • HTTP 地址是否重定向到 HTTPS。
  • 测试环境是否重定向到另一个域名。
  • 服务器是否通过公司代理访问外网。
  • Nginx、Apache、WAF 或容器 sidecar 是否改写了请求。
  • 环境中是否设置了 HTTP_PROXY、HTTPS_PROXY 或 NO_PROXY。

可以记录最终 URL、HTTP 状态码和重定向次数,但不要输出凭据。如果出现 301、302、307 或 308,应直接使用最终的官方 HTTPS API 地址,不要依赖重定向。

Content-Type 并非所有请求都必须包含

不同请求阶段使用的 header 可能不同:

  • 获取 token 的请求可能使用 Basic Authentication 和表单编码。
  • 创建支付的请求通常发送 JSON,因此需要相应的 Content-Type。
  • 没有请求体的 GET 请求不一定设置 Content-Type。
  • Authorization 可能由 CURLOPT_USERPWD 生成,不一定直接出现在 SDK 的 header 数组中。

因此,只检查 SDK 内部传给 CURLOPT_HTTPHEADER 的数组,无法完整判断最终请求是否带有认证信息。应检查 cURL 实际发出的请求,或者服务端实际收到的请求。

建议的排查顺序

  1. 立即更换已经公开的 clientSecret。
  2. 记录 GoPay 返回的 HTTP 状态码、响应体和 cURL 错误,但不要记录敏感值。
  3. 确认 token 是否获取成功,并核对测试环境与测试凭据是否匹配。
  4. 比较 CLI 与 PHP-FPM 使用的 PHP、cURL、OpenSSL 和 php.ini。
  5. 比较普通脚本与 Laravel 项目安装的 GoPay SDK 版本。
  6. 清除 Laravel 配置缓存,确认 config() 读取到的值不是空值。
  7. 在 Laravel Web 环境中执行最小 cURL 诊断。
  8. 检查代理、重定向和最终请求 URL。

如果仍然无法定位问题,最有用的补充信息不是完整 token,而是脱敏后的 SDK 请求日志、GoPay 响应体、HTTP 状态码、curl_errno()、curl_error()、CLI 与 Web 分别加载的 php.ini 路径,以及两个项目实际安装的 SDK 版本。

备注:内容仅供参考。