PHP开放平台与OAuth认证服务
PHP开放平台与OAuth认证服务
OAuth是开放平台的标准授权协议。PHP可以实现完整的OAuth服务端和客户端。今天说说PHP中OAuth认证的实现。
OAuth的核心流程包括授权码模式、密码模式和客户端模式。授权码模式是最安全的。
```php
class OAuthServer
{
private PDO $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
$this->initSchema();
}
private function initSchema(): void
{
$this->pdo->exec("
CREATE TABLE IF NOT EXISTS oauth_clients (
id INT AUTO_INCREMENT PRIMARY KEY,
client_id VARCHAR(80) NOT NULL UNIQUE,
client_secret VARCHAR(200) NOT NULL,
redirect_uri TEXT,
grant_types VARCHAR(200),
name VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
");
$this->pdo->exec("
CREATE TABLE IF NOT EXISTS oauth_access_tokens (
id INT AUTO_INCREMENT PRIMARY KEY,
access_token VARCHAR(200) NOT NULL UNIQUE,
client_id VARCHAR(80) NOT NULL,
user_id VARCHAR(80),
scope VARCHAR(200),
expires_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
");
$this->pdo->exec("
CREATE TABLE IF NOT EXISTS oauth_refresh_tokens (
id INT AUTO_INCREMENT PRIMARY KEY,
refresh_token VARCHAR(200) NOT NULL UNIQUE,
client_id VARCHAR(80) NOT NULL,
user_id VARCHAR(80),
expires_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
");
$this->pdo->exec("
CREATE TABLE IF NOT EXISTS oauth_authorization_codes (
id INT AUTO_INCREMENT PRIMARY KEY,
authorization_code VARCHAR(200) NOT NULL UNIQUE,
client_id VARCHAR(80) NOT NULL,
user_id VARCHAR(80),
redirect_uri TEXT,
expires_at TIMESTAMP,
scope VARCHAR(200)
)
");
}
public function registerClient(string $name, string $redirectUri): array
{
$clientId = bin2hex(random_bytes(16));
$clientSecret = bin2hex(random_bytes(32));
$stmt = $this->pdo->prepare("
INSERT INTO oauth_clients (client_id, client_secret, redirect_uri, grant_types, name)
VALUES (?, ?, ?, ?, ?)
");
$stmt->execute([$clientId, $clientSecret, $redirectUri, 'authorization_code,refresh_token', $name]);
return ['client_id' => $clientId, 'client_secret' => $clientSecret];
}
public function createAuthorizationCode(string $clientId, string $userId, string $redirectUri): string
{
$code = bin2hex(random_bytes(20));
$stmt = $this->pdo->prepare("
INSERT INTO oauth_authorization_codes (authorization_code, client_id, user_id, redirect_uri, expires_at, scope)
VALUES (?, ?, ?, ?, DATE_ADD(NOW(), INTERVAL 10 MINUTE), 'basic')
");
$stmt->execute([$code, $clientId, $userId, $redirectUri]);
return $code;
}
public function issueAccessToken(string $code, string $clientId, string $clientSecret): array
{
// 验证授权码
$stmt = $this->pdo->prepare("
SELECT * FROM oauth_authorization_codes
WHERE authorization_code = ? AND client_id = ? AND expires_at > NOW()
");
$stmt->execute([$code, $clientId]);
$authCode = $stmt->fetch();
if (!$authCode) {
throw new \RuntimeException("无效的授权码");
}
// 验证客户端
$client = $this->getClient($clientId);
if (!$client || !hash_equals($client['client_secret'], $clientSecret)) {
throw new \RuntimeException("客户端验证失败");
}
// 生成令牌
$accessToken = bin2hex(random_bytes(40));
$refreshToken = bin2hex(random_bytes(40));
$this->pdo->prepare("
INSERT INTO oauth_access_tokens (access_token, client_id, user_id, scope, expires_at)
VALUES (?, ?, ?, 'basic', DATE_ADD(NOW(), INTERVAL 1 HOUR))
")->execute([$accessToken, $clientId, $authCode['user_id']]);
$this->pdo->prepare("
INSERT INTO oauth_refresh_tokens (refresh_token, client_id, user_id, expires_at)
VALUES (?, ?, ?, DATE_ADD(NOW(), INTERVAL 30 DAY))
")->execute([$refreshToken, $clientId, $authCode['user_id']]);
// 删除已使用的授权码
$this->pdo->prepare("DELETE FROM oauth_authorization_codes WHERE authorization_code = ?")->execute([$code]);
return [
'access_token' => $accessToken,
'token_type' => 'Bearer',
'expires_in' => 3600,
'refresh_token' => $refreshToken,
'scope' => 'basic',
];
}
public function refreshAccessToken(string $refreshToken, string $clientId, string $clientSecret): array
{
$stmt = $this->pdo->prepare("
SELECT * FROM oauth_refresh_tokens
WHERE refresh_token = ? AND client_id = ? AND expires_at > NOW()
");
$stmt->execute([$refreshToken, $clientId]);
$token = $stmt->fetch();
if (!$token) {
throw new \RuntimeException("无效的刷新令牌");
}
// 验证客户端
$client = $this->getClient($clientId);
if (!$client || !hash_equals($client['client_secret'], $clientSecret)) {
throw new \RuntimeException("客户端验证失败");
}
$newAccessToken = bin2hex(random_bytes(40));
$this->pdo->prepare("
INSERT INTO oauth_access_tokens (access_token, client_id, user_id, scope, expires_at)
VALUES (?, ?, ?, 'basic', DATE_ADD(NOW(), INTERVAL 1 HOUR))
")->execute([$newAccessToken, $clientId, $token['user_id']]);
// 删除旧的刷新令牌
$this->pdo->prepare("DELETE FROM oauth_refresh_tokens WHERE refresh_token = ?")->execute([$refreshToken]);
return [
'access_token' => $newAccessToken,
'token_type' => 'Bearer',
'expires_in' => 3600,
'scope' => 'basic',
];
}
public function validateAccessToken(string $accessToken): ?array
{
$stmt = $this->pdo->prepare("
SELECT * FROM oauth_access_tokens
WHERE access_token = ? AND expires_at > NOW()
");
$stmt->execute([$accessToken]);
$token = $stmt->fetch(PDO::FETCH_ASSOC);
return $token ?: null;
}
public function getClient(string $clientId): ?array
{
$stmt = $this->pdo->prepare("SELECT * FROM oauth_clients WHERE client_id = ?");
$stmt->execute([$clientId]);
$client = $stmt->fetch(PDO::FETCH_ASSOC);
return $client ?: null;
}
}
$pdo = new PDO('mysql:host=localhost;dbname=oauth', 'root', '');
$oauth = new OAuthServer($pdo);
$client = $oauth->registerClient('我的应用', 'https://myapp.com/callback');
echo "客户端注册成功: " . json_encode($client) . "\n";
$code = $oauth->createAuthorizationCode($client['client_id'], 'user_123', 'https://myapp.com/callback');
echo "授权码: {$code}\n";
$token = $oauth->issueAccessToken($code, $client['client_id'], $client['secret']);
echo "访问令牌: " . json_encode($token, JSON_PRETTY_PRINT) . "\n";
$valid = $oauth->validateAccessToken($token['access_token']);
echo "令牌有效: " . ($valid ? '是' : '否') . "\n";
?>
>
OAuth中间件可以保护API端点:
```php
class OAuthMiddleware
{
private OAuthServer $server;
public function __construct(OAuthServer $server)
{
$this->server = $server;
}
public function authenticate(): ?array
{
$header = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
$token = str_replace('Bearer ', '', $header);
if (empty($token)) {
http_response_code(401);
echo json_encode(['error' => 'missing_token', 'message' => '缺少访问令牌']);
exit;
}
$tokenData = $this->server->validateAccessToken($token);
if ($tokenData === null) {
http_response_code(401);
echo json_encode(['error' => 'invalid_token', 'message' => '令牌无效或已过期']);
exit;
}
return $tokenData;
}
}
?>
OAuth是开放平台的认证基础设施。授权码模式是最安全的流程,适用于服务端应用。密码模式适用于信任的客户端,客户端模式适用于服务间通信。实现OAuth服务端要特别注意安全,包括CSRF防护、重定向URI验证和令牌的加密存储。
