本文目录导读:

- 方法一:使用 cURL(最常用,无需额外依赖)
- 方法二:使用 Guzzle(适合复杂项目,依赖 Composer)
- 方法三:PHP 原生 (file_get_contents) — 简单场景
- 注意事项与最佳实践
- 完整示例:带缓存和 Basic Auth
在 PHP 中实现 OAuth 2.0 的客户端凭证(Client Credentials)授权模式,通常用于服务器到服务器(M2M)的通信,比如服务间调用 API。
下面提供几种常见且实用的实现方法,从简单到专业:
使用 cURL(最常用,无需额外依赖)
这是最推荐的方式,因为 PHP 环境默认支持 cURL 扩展。
获取 Token
<?php
function getAccessToken($tokenUrl, $clientId, $clientSecret) {
$ch = curl_init();
// 设置请求头(Basic Auth 或 表单参数,二选一)
// 方式 A:将凭证放在请求体中
$postFields = [
'grant_type' => 'client_credentials',
'client_id' => $clientId,
'client_secret' => $clientSecret,
];
// 方式 B:使用 HTTP Basic Auth(更符合规范,推荐)
// 不需要在 body 中传 client_id/secret
// $headers = [
// 'Authorization: Basic ' . base64_encode($clientId . ':' . $clientSecret),
// 'Content-Type: application/x-www-form-urlencoded',
// ];
// $postFields = ['grant_type' => 'client_credentials'];
curl_setopt_array($ch, [
CURLOPT_URL => $tokenUrl,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($postFields),
// CURLOPT_HTTPHEADER => $headers, // 如果用方式B,取消此行注释
CURLOPT_TIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => true, // 生产环境建议开启
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode >= 200 && $httpCode < 300) {
$data = json_decode($response, true);
if (isset($data['access_token'])) {
return $data; // 通常包含 access_token, expires_in, token_type
}
}
throw new Exception('Failed to get token: ' . $response);
}
// 使用示例
try {
$tokenData = getAccessToken(
'https://api.example.com/oauth/token',
'your_client_id',
'your_client_secret'
);
$accessToken = $tokenData['access_token'];
echo "Token 获取成功: " . $accessToken;
} catch (Exception $e) {
echo "错误: " . $e->getMessage();
}
使用 Token 调用 API
function callApi($apiUrl, $accessToken) {
$ch = curl_init();
$headers = [
'Authorization: Bearer ' . $accessToken,
'Accept: application/json',
];
curl_setopt_array($ch, [
CURLOPT_URL => $apiUrl,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_TIMEOUT => 15,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return [
'status_code' => $httpCode,
'body' => json_decode($response, true)
];
}
// 使用示例
$result = callApi('https://api.example.com/v1/users', $accessToken);
if ($result['status_code'] === 200) {
print_r($result['body']);
} else {
echo "API 调用失败: " . $result['status_code'];
}
使用 Guzzle(适合复杂项目,依赖 Composer)
如果你已经在使用 Guzzle HTTP 客户端,这是更优雅的方案。
composer require guzzlehttp/guzzle
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
$client = new Client();
// 1. 获取 Token
try {
$response = $client->post('https://api.example.com/oauth/token', [
'form_params' => [
'grant_type' => 'client_credentials',
'client_id' => 'your_client_id',
'client_secret' => 'your_client_secret',
],
'timeout' => 10,
]);
$tokenData = json_decode($response->getBody(), true);
$accessToken = $tokenData['access_token'];
echo "Token: " . $accessToken;
// 2. 调用 API
$apiResponse = $client->get('https://api.example.com/v1/resource', [
'headers' => [
'Authorization' => 'Bearer ' . $accessToken,
],
'timeout' => 15,
]);
if ($apiResponse->getStatusCode() === 200) {
$data = json_decode($apiResponse->getBody(), true);
print_r($data);
}
} catch (\GuzzleHttp\Exception\RequestException $e) {
echo "HTTP 错误: " . $e->getMessage();
if ($e->hasResponse()) {
echo $e->getResponse()->getBody();
}
}
PHP 原生 (file_get_contents) — 简单场景
适用于没有 cURL 的环境(例如某些云函数),但功能有限。
<?php
$tokenUrl = 'https://api.example.com/oauth/token';
$options = [
'http' => [
'method' => 'POST',
'header' => "Content-Type: application/x-www-form-urlencoded\r\n",
'content' => http_build_query([
'grant_type' => 'client_credentials',
'client_id' => 'your_client_id',
'client_secret' => 'your_client_secret',
]),
'timeout' => 10,
'ignore_errors' => true, // 要拿到错误响应体
],
];
$context = stream_context_create($options);
$result = file_get_contents($tokenUrl, false, $context);
if ($result !== false) {
$data = json_decode($result, true);
if (isset($data['access_token'])) {
echo "Token 获取成功";
// 继续调用 API...
} else {
echo "错误: " . $result;
}
} else {
echo "请求失败";
}
注意事项与最佳实践
-
凭证安全:
- 绝对不要将 client_id 和 client_secret 硬编码在代码中或提交到 Git。
- 建议使用环境变量(
getenv('CLIENT_ID'))或专门的配置管理服务。
-
Token 缓存:
- 客户端凭证 Token 通常有时效(如 1 小时),且不涉及用户,务必缓存 Token,避免每次都请求。
- 简单做法:将 Token 存入 Redis/Memcached,设置过期时间(比
expires_in提前 60 秒)。
function getCachedToken($cache, $tokenUrl, $clientId, $clientSecret) { $cacheKey = 'oauth_token_' . $clientId; $tokenData = $cache->get($cacheKey); if ($tokenData === false) { $tokenData = getAccessToken($tokenUrl, $clientId, $clientSecret); $cache->set($cacheKey, $tokenData, $tokenData['expires_in'] - 60); } return $tokenData['access_token']; } -
错误处理:
- 检查 HTTP 状态码。
- 检查响应的
scope字段,确保返回的权限符合预期。
-
请求头选择:
- 大多数 OAuth 服务器(如 Auth0, Okta, Azure AD)支持将凭证放在 Body 中,但推荐使用 HTTP Basic Auth(
Authorization: Basic ...),这是 OAuth 2.0 规范推荐的方式,安全性更高(避免在日志中泄露)。
- 大多数 OAuth 服务器(如 Auth0, Okta, Azure AD)支持将凭证放在 Body 中,但推荐使用 HTTP Basic Auth(
完整示例:带缓存和 Basic Auth
<?php
class OAuthClientCredentials {
private $tokenUrl;
private $clientId;
private $clientSecret;
private $cache;
public function __construct($tokenUrl, $clientId, $clientSecret, $cache = null) {
$this->tokenUrl = $tokenUrl;
$this->clientId = $clientId;
$this->clientSecret = $clientSecret;
$this->cache = $cache; // 可以是 Redis/APCu 等
}
public function getAccessToken() {
if ($this->cache !== null) {
$cached = $this->cache->get('access_token');
if ($cached !== false) {
return $cached;
}
}
$ch = curl_init();
$headers = [
'Authorization: Basic ' . base64_encode($this->clientId . ':' . $this->clientSecret),
'Content-Type: application/x-www-form-urlencoded',
'Accept: application/json',
];
curl_setopt_array($ch, [
CURLOPT_URL => $this->tokenUrl,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => 'grant_type=client_credentials',
CURLOPT_HTTPHEADER => $headers,
CURLOPT_TIMEOUT => 10,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status >= 400) {
throw new Exception("Token 请求失败 (HTTP $status): $response");
}
$data = json_decode($response, true);
if (!isset($data['access_token'])) {
throw new Exception("无效的 Token 响应: " . $response);
}
// 缓存 token,提前 60 秒过期
if ($this->cache !== null) {
$this->cache->set('access_token', $data['access_token'], $data['expires_in'] - 60);
}
return $data['access_token'];
}
}
这样你就有了一个可复用的客户端凭证认证类,可以安全高效地调用受保护的 API 了。