如何使用 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 请求头。
备注:内容仅供参考。
