PAYATHON 2026

如何使用 PHP 集成 OAuth 2.0 支付 API

支付小周

结论

使用 Client Credentials 获取 access token 时,不要发送空的 Authorization: Bearer 请求头。Bearer 用于携带已经取得的 access token,不适用于首次申请 token。

token 请求通常需要满足以下要求:

  • 向 token endpoint 发送 POST 请求;
  • 使用 application/x-www-form-urlencoded,不要使用 application/json;
  • 通过 HTTP Basic Authentication 传递 client_id 和 client_secret;
  • 在请求体中传入 grant_type=client_credentials;
  • 多个 scope 通常以空格分隔,其名称和大小写必须与 API 文档完全一致。

Client Credentials 通常不会返回 refresh_token。access token 过期后,再次发送相同的 client credentials 请求即可取得新 token。

获取 access token

如果该支付 API 遵循标准的 OAuth 2.0 Client Credentials 流程,可以按下面的方式请求:

<?php

$clientId = 'YOUR_CLIENT_ID';
$clientSecret = 'YOUR_CLIENT_SECRET';

$tokenUrl = 'https://test.auth.alphacredit.be/api/oauth/token';

$postData = http_build_query([
    'grant_type' => 'client_credentials',
    'scope' => 'Resource.WRITE resource.READ',
]);

$curl = curl_init();

curl_setopt_array($curl, [
    CURLOPT_URL => $tokenUrl,
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,

    // 等价于 Authorization: Basic base64(client_id:client_secret)
    CURLOPT_USERPWD => $clientId . ':' . $clientSecret,
    CURLOPT_HTTPAUTH => CURLAUTH_BASIC,

    CURLOPT_HTTPHEADER => [
        'Accept: application/json',
        'Content-Type: application/x-www-form-urlencoded',
    ],

    CURLOPT_POSTFIELDS => $postData,
]);

$response = curl_exec($curl);

if ($response === false) {
    $error = curl_error($curl);
    curl_close($curl);

    throw new RuntimeException('Token request failed: ' . $error);
}

$statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);

$data = json_decode($response, true);

if ($statusCode < 200 || $statusCode >= 300) {
    $errorDescription = $data['error_description'] ?? $response;

    throw new RuntimeException(
        'Token endpoint returned HTTP ' . $statusCode . ': ' . $errorDescription
    );
}

if (!isset($data['access_token'])) {
    throw new RuntimeException('Response does not contain access_token.');
}

$accessToken = $data['access_token'];
$tokenType = $data['token_type'] ?? 'Bearer';
$expiresIn = isset($data['expires_in']) ? (int) $data['expires_in'] : null;

成功响应可能如下:

{
  "access_token": "eyJ...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "Resource.WRITE resource.READ"
}

实际字段以服务商的响应为准,不要假定响应中一定有 expires_in 或 scope。

为什么原代码无法正常工作

原代码有以下几个问题。

1. 错误地发送了 Bearer 请求头

Authorization: Bearer

这个请求头没有包含 token,而且申请 token 时也不应使用 Bearer 认证。只有在取得 access token 并调用支付资源接口时,才需要发送:

Authorization: Bearer ACCESS_TOKEN

2. Content-Type 与请求体不匹配

代码声明了:

Content-Type: application/json

但传给 CURLOPT_POSTFIELDS 的是 PHP 数组。OAuth 2.0 token endpoint 通常要求:

Content-Type: application/x-www-form-urlencoded

因此,请使用 http_build_query() 构造请求体。

3. scope 格式和名称可能有误

配置中的 scope 是:

Resource.WRITE, resource.READ

OAuth 2.0 请求中的多个 scope 通常以空格分隔,而不是使用逗号:

Resource.WRITE resource.READ

scope 往往区分大小写,不要自行改成:

read, write

最终值仍须以该支付 API 的文档或服务商提供的示例为准。如果文档明确要求使用逗号分隔,就应遵循其非标准约定。

4. cURL 配置数组的结构不正确

curl_setopt() 是函数调用,不能直接放进 curl_setopt_array() 的配置数组中。请求体应写成一个数组项:

CURLOPT_POSTFIELDS => $postData,

如果服务商要求把凭据放在请求体中

OAuth 2.0 更推荐通过 HTTP Basic Authentication 传递客户端凭据,但有些服务商会要求把 client_id 和 client_secret 放入请求体。

只有 API 文档明确提出这一要求时,才使用下面的写法:

<?php

$clientId = 'YOUR_CLIENT_ID';
$clientSecret = 'YOUR_CLIENT_SECRET';

$curl = curl_init('https://test.auth.alphacredit.be/api/oauth/token');

$postData = http_build_query([
    'client_id' => $clientId,
    'client_secret' => $clientSecret,
    'grant_type' => 'client_credentials',
    'scope' => 'Resource.WRITE resource.READ',
]);

curl_setopt_array($curl, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTPHEADER => [
        'Accept: application/json',
        'Content-Type: application/x-www-form-urlencoded',
    ],
    CURLOPT_POSTFIELDS => $postData,
]);

$response = curl_exec($curl);
$statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);

if ($response === false) {
    throw new RuntimeException(curl_error($curl));
}

curl_close($curl);

$data = json_decode($response, true);

if ($statusCode < 200 || $statusCode >= 300 || !isset($data['access_token'])) {
    throw new RuntimeException('Unable to obtain access token: ' . $response);
}

$accessToken = $data['access_token'];

除非服务商文档明确要求,否则不要同时通过 Basic Authentication 和请求体重复发送客户端密钥。

使用 access token 调用支付 API

取得 token 后,把它放入资源请求的 Authorization 请求头:

<?php

$curl = curl_init('https://example.com/api/payments');

curl_setopt_array($curl, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Accept: application/json',
        'Authorization: Bearer ' . $accessToken,
    ],
]);

$response = curl_exec($curl);
$statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);

if ($response === false) {
    throw new RuntimeException(curl_error($curl));
}

curl_close($curl);

只有这里需要 Bearer 前缀,而且前缀与 token 之间必须保留一个空格。

token 过期后如何处理

标准的 Client Credentials 流程通常没有“刷新现有 token”这一步,也不会返回 refresh_token。当 token 即将过期或已经过期时,客户端应再次调用 token endpoint:

grant_type=client_credentials

取得新 token 后,用它替换旧 token。

一种稳妥的处理方式是记录 token 的取得时间和 expires_in,并提前一小段时间重新申请,例如提前 60 秒:

$expiresAt = time() + $expiresIn;
$shouldRenew = time() >= ($expiresAt - 60);

如果接口没有返回 expires_in,不要自行推断有效期。应查看 API 文档,或者在资源接口返回 401 Unauthorized 后重新申请 token,再谨慎地重试一次原请求。

安全和排错注意事项

  • client_secret 只能保存在服务器端,不能写入浏览器 JavaScript、移动端代码或公开仓库。
  • 生产环境不要记录完整的 client_secret、access token 或支付请求中的敏感数据。
  • 不建议显式设置 Accept-Encoding: gzip, deflate。如需自动解压,可以使用:
CURLOPT_ENCODING => '',
  • 保留 token endpoint 返回的 HTTP 状态码和错误响应,便于区分 invalid_client、invalid_scope、unsupported_grant_type 等问题。
  • 不要通过关闭 TLS 验证来绕过证书错误。
  • 测试环境和生产环境通常使用不同的 endpoint、client_id 与 client_secret,不要混用。
  • 如果 Basic Authentication 返回 invalid_client,先核对服务商文档,确认是否需要在请求体中发送客户端凭据,不要反复修改 Bearer 请求头。

备注:内容仅供参考。