OAuth2 伺服器
基於 league/oauth2-server 的 Hyperf 框架完整 OAuth2 伺服器實現。
功能特性
- 完整的 OAuth2 伺服器實現,支援:
- 客戶端憑證授權 (Client Credentials Grant)
- 密碼授權 (Password Grant)
- 重新整理令牌授權 (Refresh Token Grant)
- 授權碼授權 (Authorization Code Grant,支援 PKCE)
- 裝置碼授權 (Device Code Grant)
- 隱式授權 (Implicit Grant)
- 內建客戶端管理命令
- 多種儲存後端 (Eloquent ORM)
- 可自定義的令牌生命週期
- 作用域管理
- 事件驅動架構
- 工廠模式實現
- 型別安全的值物件和列舉
- 完整的錯誤處理和日誌記錄
安裝
1. 透過 Composer 安裝
composer require friendsofhyperf/oauth2-server
2. 釋出配置
php bin/hyperf.php vendor:publish friendsofhyperf/oauth2-server
3. 生成加密金鑰
# 生成私鑰/公鑰對
php bin/hyperf.php oauth2:generate-keypair
這將生成:
storage/oauth2/private.key
- 用於簽名令牌的私鑰storage/oauth2/public.key
- 用於驗證令牌的公鑰
4. 執行資料庫遷移
php bin/hyperf.php migrate
配置
在 config/autoload/oauth2-server.php
中配置 OAuth2 伺服器:
<?php
return [
'authorization_server' => [
'private_key' => env('OAUTH2_PRIVATE_KEY', 'storage/oauth2/private.key'),
'private_key_passphrase' => env('OAUTH2_PRIVATE_KEY_PASSPHRASE'),
'encryption_key' => env('OAUTH2_ENCRYPTION_KEY'),
'encryption_key_type' => EncryptionKeyType::from(env('OAUTH2_ENCRYPTION_KEY_TYPE', 'plain')),
'response_type' => BearerTokenResponse::class,
'revoke_refresh_tokens' => true,
'access_token_ttl' => new DateInterval('PT1H'),
'auth_code_ttl' => new DateInterval('PT10M'),
'refresh_token_ttl' => new DateInterval('P1M'),
'enable_client_credentials_grant' => true,
'enable_password_grant' => true,
'enable_refresh_token_grant' => true,
'enable_auth_code_grant' => true,
'enable_implicit_grant' => false,
'require_code_challenge_for_public_clients' => true,
'persist_access_tokens' => true,
],
'resource_server' => [
'public_key' => env('OAUTH2_PUBLIC_KEY', 'storage/oauth2/public.key'),
'jwt_leeway' => null,
],
'scopes' => [
'available' => ['read', 'write', 'admin'],
'default' => ['read'],
],
];
環境變數
在 .env
檔案中設定以下環境變數:
# OAuth2 金鑰
OAUTH2_PRIVATE_KEY=storage/oauth2/private.key
OAUTH2_PUBLIC_KEY=storage/oauth2/public.key
OAUTH2_PRIVATE_KEY_PASSPHRASE=
OAUTH2_ENCRYPTION_KEY=your-encryption-key-here
# 可選
OAUTH2_ENCRYPTION_KEY_TYPE=plain
可用命令
命令 | 描述 |
---|---|
oauth2:clear-expired-tokens | 清除過期的訪問/重新整理令牌 |
oauth2:create-client | 建立新的 OAuth2 客戶端 |
oauth2:delete-client | 刪除 OAuth2 客戶端 |
oauth2:generate-keypair | 生成私鑰/公鑰對 |
oauth2:list-clients | 列出所有 OAuth2 客戶端 |
oauth2:update-client | 更新 OAuth2 客戶端 |
建立客戶端
建立授權碼授權的客戶端:
php bin/hyperf.php oauth2:create-client \
--name="我的網頁應用" \
--redirect-uri="https://myapp.com/callback" \
--grant-type="authorization_code" \
--grant-type="refresh_token"
建立密碼授權的客戶端:
php bin/hyperf.php oauth2:create-client \
--name="我的移動應用" \
--grant-type="password" \
--grant-type="refresh_token"
建立客戶端憑證授權的客戶端:
php bin/hyperf.php oauth2:create-client \
--name="我的API服務" \
--grant-type="client_credentials"
API 端點
授權端點
GET /oauth/authorize
用於授權碼授權流程。引數:
response_type
: 必須是code
client_id
: 客戶端 IDredirect_uri
: 必須與註冊的回撥 URI 匹配scope
: 空格分隔的作用域列表state
: CSRF 保護令牌code_challenge
: PKCE 程式碼挑戰code_challenge_method
: PKCE 方法(通常是S256
)
令牌端點
POST /oauth/token
用於交換授權碼獲取訪問令牌或使用其他授權型別。
受保護資源
使用 ResourceServerMiddleware
保護路由:
use FriendsOfHyperf\Oauth2\Server\Middleware\ResourceServerMiddleware;
Router::addGroup('/api', function () {
Router::get('user', [UserController::class, 'index']);
Router::post('posts', [PostController::class, 'store']);
})->add(ResourceServerMiddleware::class);
授權型別
1. 客戶端憑證授權 (Client Credentials Grant)
用於伺服器到伺服器認證:
curl -X POST http://your-server/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "client_credentials",
"client_id": "your-client-id",
"client_secret": "your-client-secret",
"scope": "read write"
}'
適用場景:
- 微服務間通訊
- API 金鑰認證
- 系統整合
2. 密碼授權 (Password Grant)
用於可信應用(移動應用、SPA):
curl -X POST http://your-server/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "password",
"client_id": "your-client-id",
"client_secret": "your-client-secret",
"username": "user@example.com",
"password": "password",
"scope": "read write"
}'
適用場景:
- 移動應用
- 單頁應用 (SPA)
- 可信的第三方應用
注意: 此授權方式需要高度信任客戶端,謹慎使用。
3. 授權碼授權 (Authorization Code Grant)
用於需要使用者互動的網頁應用:
步驟1:重定向使用者到授權端點
https://your-server/oauth/authorize?response_type=code&client_id=your-client-id&redirect_uri=https://myapp.com/callback&scope=read&state=random-state&code_challenge=challenge&code_challenge_method=S256
步驟2:交換授權碼獲取令牌
curl -X POST http://your-server/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "authorization_code",
"client_id": "your-client-id",
"client_secret": "your-client-secret",
"redirect_uri": "https://myapp.com/callback",
"code_verifier": "verifier",
"code": "authorization-code-from-redirect"
}'
PKCE 支援:
code_challenge
: 生成的程式碼挑戰code_challenge_method
: S256 或 plaincode_verifier
: 用於驗證的原始程式碼
適用場景:
- 網頁應用
- 需要使用者授權的應用
- 安全要求高的場景
4. 重新整理令牌授權 (Refresh Token Grant)
獲取新的訪問令牌:
curl -X POST http://your-server/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "refresh_token",
"client_id": "your-client-id",
"client_secret": "your-client-secret",
"refresh_token": "your-refresh-token",
"scope": "read write"
}'
適用場景:
- 延長使用者會話
- 避免頻繁重新登入
- 移動應用後臺服務
5. 裝置碼授權 (Device Code Grant)
用於無輸入裝置(如智慧電視、IoT裝置):
步驟1:請求裝置碼
curl -X POST http://your-server/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "device_code",
"client_id": "your-client-id",
"scope": "read"
}'
步驟2:使用者在其他裝置上授權
使用者需要在手機或電腦上訪問顯示的授權URL完成授權。
步驟3:輪詢令牌
curl -X POST http://your-server/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "device_code",
"client_id": "your-client-id",
"device_code": "device-code-from-step1"
}'
適用場景:
- 智慧電視應用
- IoT 裝置
- 無輸入裝置的應用
6. 隱式授權 (Implicit Grant)
已不推薦使用,但在某些舊系統中仍可能遇到:
https://your-server/oauth/authorize?response_type=token&client_id=your-client-id&redirect_uri=https://myapp.com/callback&scope=read&state=random-state
注意: 此授權方式已被 OAuth 2.1 棄用,建議使用授權碼授權 + PKCE。
工廠模式實現
元件使用工廠模式建立伺服器例項,提供了更好的靈活性和可測試性:
授權伺服器工廠
use FriendsOfHyperf\Oauth2\Server\Factory\AuthorizationServerFactory;
use Hyperf\Di\Annotation\Inject;
class YourController
{
#[Inject]
private AuthorizationServerFactory $authorizationServerFactory;
public function handleAuthorization()
{
// 構建授權伺服器
$authorizationServer = $this->authorizationServerFactory->build();
// 使用授權伺服器處理請求
// ...
}
}
資源伺服器工廠
use FriendsOfHyperf\Oauth2\Server\Factory\ResourceServerFactory;
use Hyperf\Di\Annotation\Inject;
class YourController
{
#[Inject]
private ResourceServerFactory $resourceServerFactory;
public function getProtectedData()
{
// 構建資源伺服器
$resourceServer = $this->resourceServerFactory->build();
// 驗證訪問令牌
$accessToken = $resourceServer->validateAuthenticatedRequest($request);
// 獲取令牌資訊
$tokenId = $accessToken->getAttribute('oauth_access_token_id');
$userId = $accessToken->getAttribute('oauth_user_id');
// 返回受保護的資料
return ['user_id' => $userId];
}
}
配置工廠
use FriendsOfHyperf\Oauth2\Server\Factory\ConfigFactory;
use Hyperf\Di\Annotation\Inject;
class YourController
{
#[Inject]
private ConfigFactory $configFactory;
public function getConfig()
{
// 獲取 OAuth2 配置
$config = $this->configFactory->create();
// 訪問配置項
$accessTokenTtl = $config->get('authorization_server.access_token_ttl');
$encryptionKey = $config->get('authorization_server.encryption_key');
return $config;
}
}
發起認證請求
在 Authorization 頭中包含訪問令牌:
curl -X GET http://your-server/api/user \
-H "Authorization: Bearer your-access-token"
事件系統
元件提供了完整的事件系統,允許您自定義 OAuth2 流程的各個方面:
可用事件
事件類 | 描述 | 使用場景 |
---|---|---|
AuthorizationRequestResolveEvent | 當授權請求需要使用者批准時觸發 | 實現自定義授權邏輯、顯示授權頁面 |
UserResolveEvent | 當為密碼授權解析使用者時觸發 | 實現自定義使用者認證邏輯 |
ScopeResolveEvent | 當解析作用域時觸發 | 實現自定義作用域驗證和過濾 |
TokenRequestResolveEvent | 當處理令牌請求時觸發 | 記錄令牌發放、新增自定義響應頭 |
PreSaveClientEvent | 在儲存客戶端之前觸發 | 驗證客戶端資料、新增預設值 |
事件監聽器示例
1. 自定義使用者認證
<?php
namespace App\Listener;
use FriendsOfHyperf\Oauth2\Server\Event\UserResolveEvent;
use FriendsOfHyperf\Oauth2\Server\Model\UserInterface;
use Hyperf\Event\Annotation\Listener;
use Hyperf\Di\Annotation\Inject;
use App\Service\UserService;
#[Listener]
class UserResolveListener
{
#[Inject]
private UserService $userService;
public function listen(): array
{
return [
UserResolveEvent::class,
];
}
public function process(object $event): void
{
if (!$event instanceof UserResolveEvent) {
return;
}
// 驗證使用者憑據
$user = $this->userService->authenticate($event->getUsername(), $event->getPassword());
if ($user) {
// 設定使用者實體
$userEntity = new UserInterface();
$userEntity->setIdentifier($user->id);
$event->setUser($userEntity);
}
}
}
2. 自定義授權處理
<?php
namespace App\Listener;
use FriendsOfHyperf\Oauth2\Server\Event\AuthorizationRequestResolveEvent;
use FriendsOfHyperf\Oauth2\Server\Model\ClientInterface;
use Hyperf\Event\Annotation\Listener;
use Hyperf\HttpServer\Contract\ResponseInterface;
#[Listener]
class AuthorizationRequestResolveListener
{
public function listen(): array
{
return [
AuthorizationRequestResolveEvent::class,
];
}
public function process(object $event): void
{
if (!$event instanceof AuthorizationRequestResolveEvent) {
return;
}
// 檢查客戶端是否被允許訪問請求的作用域
if (!$this->isScopeAllowed($event->getClient(), $event->getScopes())) {
// 返回自定義錯誤響應
$response = $this->createErrorResponse('invalid_scope', 'Requested scope is not allowed');
$event->setResponse($response);
return;
}
// 自動批准可信客戶端的請求
if ($this->isTrustedClient($event->getClient())) {
$event->resolveAuthorization(AuthorizationRequestResolveEvent::AUTHORIZATION_APPROVED);
}
}
private function isScopeAllowed(ClientInterface $client, array $scopes): bool
{
// 實現作用域驗證邏輯
return true;
}
private function isTrustedClient(ClientInterface $client): bool
{
// 實現可信客戶端檢查邏輯
return $client->getName() === 'Trusted App';
}
}
3. 自定義作用域解析
<?php
namespace App\Listener;
use FriendsOfHyperf\Oauth2\Server\Event\ScopeResolveEvent;
use FriendsOfHyperf\Oauth2\Server\ValueObject\Scope;
use Hyperf\Event\Annotation\Listener;
#[Listener]
class ScopeResolveListener
{
public function listen(): array
{
return [
ScopeResolveEvent::class,
];
}
public function process(object $event): void
{
if (!$event instanceof ScopeResolveEvent) {
return;
}
// 根據使用者角色動態調整作用域
$userScopes = $this->getUserScopes($event->getUserId());
$filteredScopes = [];
foreach ($event->getRequestedScopes() as $scope) {
if (in_array((string) $scope, $userScopes)) {
$filteredScopes[] = $scope;
}
}
$event->setResolvedScopes($filteredScopes);
}
private function getUserScopes(string $userId): array
{
// 實現使用者作用域獲取邏輯
return ['read', 'write'];
}
}
4. 令牌請求記錄
<?php
namespace App\Listener;
use FriendsOfHyperf\Oauth2\Server\Event\TokenRequestResolveEvent;
use Hyperf\Event\Annotation\Listener;
use Hyperf\Di\Annotation\Inject;
use App\Service\AuditService;
#[Listener]
class TokenRequestResolveListener
{
#[Inject]
private AuditService $auditService;
public function listen(): array
{
return [
TokenRequestResolveEvent::class,
];
}
public function process(object $event): void
{
if (!$event instanceof TokenRequestResolveEvent) {
return;
}
// 記錄令牌發放
$response = $event->getResponse();
$responseData = json_decode((string) $response->getBody(), true);
if (isset($responseData['access_token'])) {
$this->auditService->logTokenIssued([
'access_token' => $responseData['access_token'],
'token_type' => $responseData['token_type'] ?? 'Bearer',
'expires_in' => $responseData['expires_in'] ?? null,
'scope' => $responseData['scope'] ?? null,
'client_id' => $responseData['client_id'] ?? null,
'user_id' => $responseData['user_id'] ?? null,
]);
}
}
}
資料庫表
該包建立以下表:
oauth_clients
: OAuth2 客戶端oauth_access_tokens
: 訪問令牌oauth_refresh_tokens
: 重新整理令牌oauth_auth_codes
: 授權碼oauth_personal_access_clients
: 個人訪問客戶端
自定義
自定義使用者提供程式
透過監聽 UserResolveEvent
實現您自己的使用者解析邏輯。
自定義作用域管理
監聽 ScopeResolveEvent
以實現自定義作用域邏輯。
自定義令牌儲存
擴充套件儲存庫類以實現自定義儲存後端。
自定義客戶端驗證
透過監聽 PreSaveClientEvent
實現客戶端資料驗證和預設值設定:
<?php
namespace App\Listener;
use FriendsOfHyperf\Oauth2\Server\Event\PreSaveClientEvent;
use FriendsOfHyperf\Oauth2\Server\ValueObject\Grant;
use FriendsOfHyperf\Oauth2\Server\ValueObject\RedirectUri;
use FriendsOfHyperf\Oauth2\Server\ValueObject\Scope;
use Hyperf\Event\Annotation\Listener;
#[Listener]
class PreSaveClientListener
{
public function listen(): array
{
return [
PreSaveClientEvent::class,
];
}
public function process(object $event): void
{
if (!$event instanceof PreSaveClientEvent) {
return;
}
$client = $event->getClient();
// 設定預設作用域
if (empty($client->getScopes())) {
$client->setScopes(new Scope('read'));
}
// 驗證重定向URI
foreach ($client->getRedirectUris() as $uri) {
if (!$this->isValidRedirectUri($uri)) {
throw new \InvalidArgumentException('Invalid redirect URI');
}
}
// 設定預設授權型別
if (empty($client->getGrants())) {
$client->setGrants(new Grant('client_credentials'));
}
}
private function isValidRedirectUri(RedirectUri $uri): bool
{
// 實現URI驗證邏輯
return str_starts_with((string) $uri, 'https://');
}
}
自定義錯誤處理
<?php
namespace App\Listener;
use FriendsOfHyperf\Oauth2\Server\Event\TokenRequestResolveEvent;
use League\OAuth2\Server\Exception\OAuthServerException;
use Hyperf\Event\Annotation\Listener;
use Hyperf\HttpMessage\Stream\SwooleStream;
#[Listener]
class CustomErrorHandler
{
public function listen(): array
{
return [
TokenRequestResolveEvent::class,
];
}
public function process(object $event): void
{
if (!$event instanceof TokenRequestResolveEvent) {
return;
}
$response = $event->getResponse();
$statusCode = $response->getStatusCode();
// 自定義錯誤響應格式
if ($statusCode >= 400) {
$body = json_decode((string) $response->getBody(), true);
$customError = [
'error' => $body['error'] ?? 'server_error',
'error_description' => $body['error_description'] ?? 'An error occurred',
'error_code' => $this->getErrorCode($body['error'] ?? 'server_error'),
'timestamp' => time(),
'request_id' => $this->generateRequestId(),
];
$newResponse = $response->withBody(new SwooleStream(json_encode($customError)));
$event->setResponse($newResponse);
}
}
private function getErrorCode(string $error): string
{
// 實現錯誤程式碼對映
return match($error) {
'invalid_client' => 'AUTH_001',
'invalid_grant' => 'AUTH_002',
'invalid_scope' => 'AUTH_003',
default => 'AUTH_999',
};
}
private function generateRequestId(): string
{
// 生成請求ID
return uniqid('oauth_', true);
}
}
自定義令牌響應
<?php
namespace App\Listener;
use FriendsOfHyperf\Oauth2\Server\Event\TokenRequestResolveEvent;
use Hyperf\Event\Annotation\Listener;
use Hyperf\HttpMessage\Stream\SwooleStream;
#[Listener]
class CustomTokenResponseListener
{
public function listen(): array
{
return [
TokenRequestResolveEvent::class,
];
}
public function process(object $event): void
{
if (!$event instanceof TokenRequestResolveEvent) {
return;
}
$response = $event->getResponse();
$body = json_decode((string) $response->getBody(), true);
if (isset($body['access_token'])) {
// 新增自定義欄位
$customResponse = array_merge($body, [
'token_type' => 'Bearer',
'expires_in' => $body['expires_in'] ?? 3600,
'issued_at' => time(),
'user_info' => $this->getUserInfo($body),
'permissions' => $this->getPermissions($body['scope'] ?? ''),
]);
$newResponse = $response->withBody(new SwooleStream(json_encode($customResponse)));
$event->setResponse($newResponse);
}
}
private function getUserInfo(array $tokenData): array
{
// 獲取使用者資訊
return [
'id' => $tokenData['user_id'] ?? null,
'name' => 'John Doe',
'email' => 'john@example.com',
];
}
private function getPermissions(string $scope): array
{
// 根據作用域獲取許可權
$permissions = [];
$scopes = explode(' ', $scope);
foreach ($scopes as $scope) {
$permissions = array_merge($permissions, $this->scopeToPermissions($scope));
}
return array_unique($permissions);
}
private function scopeToPermissions(string $scope): array
{
// 作用域到許可權的對映
return match($scope) {
'read' => ['user:read', 'post:read'],
'write' => ['user:write', 'post:write'],
'admin' => ['*'],
default => [],
};
}
}
安全最佳實踐
- 在生產環境中始終使用 HTTPS
- 使用適當的檔案許可權安全儲存私鑰
- 使用強加密金鑰
- 為授權流程實施適當的 CSRF 保護
- 嚴格驗證回撥 URI
- 使用短生命週期的訪問令牌和重新整理令牌
- 在令牌端點上實施速率限制
- 記錄和監控令牌使用情況
測試
在開發過程中,您可以使用內建命令測試 OAuth2 流程:
# 建立測試客戶端
php bin/hyperf.php oauth2:create-client \
--name="測試客戶端" \
--redirect-uri="http://localhost:3000/callback" \
--grant-type="authorization_code" \
--grant-type="password" \
--grant-type="refresh_token"
# 列出所有客戶端
php bin/hyperf.php oauth2:list-clients
# 清除過期令牌
php bin/hyperf.php oauth2:clear-expired-tokens
錯誤處理
常見錯誤響應:
invalid_client
: 客戶端認證失敗invalid_grant
: 無效的授權許可invalid_request
: 缺少必需引數invalid_scope
: 請求的作用域無效unsupported_grant_type
: 不支援的授權型別server_error
: 內部伺服器錯誤
許可證
MIT