OAuth2.0 是關于授權的開放網絡標準,它允許用戶已第三方應用獲取該用戶在某一網站的私密資源,而無需提供用戶名與密碼,目前已在全世界得到廣泛應用。
league/oauth2-server 是一個輕量級并且功能強大的符合 OAuth2.0 協議的 PHP 庫,使用它可以構建出標準的 OAuth2.0 授權服務器。
本文通過對 PHP 庫:league/oauth2-server 進行實踐的同時,理解 OAuth2.0 的工作流程與設計思路。
術語
了解 OAuth2.0 與 oauth2-server 的專用術語,對于理解后面內容很有幫助。
OAuth2.0 定義了四個角色
- Client:客戶端,第三方應用程序。
- Resource Owner:資源所有者,授權 Client 訪問其帳戶的用戶。
- Authorization server:授權服務器,服務商專用于處理用戶授權認證的服務器。
- Resource server:資源服務器,服務商用于存放用戶受保護資源的服務器,它可以與授權服務器是同一臺服務器,也可以是不同的服務器。
oauth2-server
- Access token:用于訪問受保護資源的令牌。
- Authorization code:發放給應用程序的中間令牌,客戶端應用使用此令牌交換 access token。
- Scope:授予應用程序的權限范圍。
- JWT:Json Web Token 是一種用于安全傳輸的數據傳輸格式。
運行流程
安裝
推薦使用 Composer 進行安裝:
composer require league/oauth2-server
根據授權模式的不同,oauth2-server 提供了不同的 Interface 與 Triat 幫助實現。
本文發布時,版本號為7.3.1。
生成公鑰與私鑰
公鑰與私鑰用于簽名和驗證傳輸的 JWT,授權服務器使用私鑰簽名 JWT,資源服務器擁有公鑰驗證 JWT。
oauth2-server 使用 JWT 傳輸訪問令牌(access token),方便資源服務器獲取其中內容,所以需要使用非對稱加密。
生成私鑰,在終端中運行:
openssl genrsa -out private.key 2048
使用私鑰提取私鑰:
openssl rsa -in private.key -pubout -out public.key
私鑰必須保密于授權服務器中,并將公鑰分發給資源服務器。
生成加密密鑰
加密密鑰用于加密授權碼(auth code)與刷新令牌(refesh token),AuthorizationServer(授權服務器啟動類)接受兩種加密密鑰,string
或 defuse/php-encryption
庫的對象。
加密授權碼(auth code)與刷新令牌(refesh token)只有授權權服務器使用,所以使用對稱加密。
生成字符串密鑰,在終端中輸入:
php -r 'echo base64_encode(random_bytes(32)), PHP_EOL;'
生成對象,在項目根目錄的終端中輸入:
vendor/bin/generate-defuse-key
將獲得的內容,傳入 AuthorizationServer:
use \Defuse\Crypto\Key;
$server = new AuthorizationServer(
$clientRepository,
$accessTokenRepository,
$scopeRepository,
$privateKeyPath,
Key::loadFromAsciiSafeString($encryptionKey) //傳入加密密鑰
);
PHP版本支持
- PHP 7.0
- PHP 7.1
- PHP 7.2
授權模式
OAuth2.0 定義了四種授權模式,以應對不同情況時的授權。
- 授權碼模式
- 隱式授權模式
- 密碼模式
- 客戶端模式
客戶端類型
- 保密的:
- 客戶端可以安全的存儲自己與用戶的憑據(例如:有所屬的服務器端)
- 公開的:
- 客戶端無法安全的存儲自己與用戶的憑據(例如:運行在瀏覽器的單頁應用)
選用哪種授權模式?
如果客戶端是保密的,應使用授權碼模式。
如果客戶端是公開的,應使用隱式授權模式。
如果用戶對于此客戶端高度信任(例如:第一方應用程序或操作系統程序),應使用密碼模式。
如果客戶端是以自己的名義,不與用戶產生關系,應使用客戶端模式。
預先注冊
客戶端需要預先在授權服務器進行注冊,用以獲取 client_id
與 client_secret
,也可以在注冊是預先設定好 redirect_uri
,以便于之后可以使用默認的 redirect_uri
。
授權碼模式
授權碼模式是 OAuth2.0 種功能最完整,流程最嚴密的一種模式,如果你使用過 Google 或 QQ 登錄過第三方應用程序,應該會對這個流程的第一部分很熟悉。
流程
第一部分(用戶可見)
用戶訪問客戶端,客戶端將用戶導向授權服務器時,將以下參數通過 GET query
傳入:
-
response_type
:授權類型,必選項,值固定為:code
-
client_id
:客戶端ID,必選項 -
redirect_uri
:重定向URI,可選項,不填寫時默認預先注冊的重定向URI -
scope
:權限范圍,可選項,以空格分隔 -
state
:CSRF令牌,可選項,但強烈建議使用,應將該值存儲與用戶會話中,以便在返回時驗證
用戶選擇是否給予客戶端授權
假設用戶給予授權,授權服務器將用戶導向客戶端事先指定的 redirect_uri
,并將以下參數通過 GET query
傳入:
-
code
:授權碼(Authorization code) -
state
:請求中發送的state
,原樣返回。客戶端將此值與用戶會話中的值進行對比,以確保授權碼響應的是此客戶端而非其他客戶端程序
第二部分(用戶不可見)
客戶端已得到授權,通過 POST
請求向授權服務器獲取訪問令牌(access token):
-
grant_type
:授權模式,值固定為:authorization_code
-
client_id
:客戶端ID -
client_secret
:客戶端 secret -
redirect_uri
:使用與第一部分請求相同的 URI -
code
:第一部分所獲的的授權碼,要注意URL解碼
授權服務器核對授權碼與重定向 URI,確認無誤后,向客戶端響應下列內容:
token_type
:令牌類型,值固定為:Bearer
expires_in
:訪問令牌的存活時間access_token
:訪問令牌refresh_token
:刷新令牌,訪問令牌過期后,使用刷新令牌重新獲取
使用 oauth2-server 實現
初始化
OAuth2.0 只是協議,在實現上需要聯系到用戶與數據庫存儲,oauth2-server 的新版本并沒有指定某種數據庫,但它提供了 Interfaces 與 Traits 幫助我們實現,這讓我們可以方便的使用任何形式的數據存儲方式,這種方便的代價就是需要我們自行創建 Repositories 與 Entities。
初始化 server
// 初始化存儲庫
$clientRepository = new ClientRepository(); // Interface: ClientRepositoryInterface
$scopeRepository = new ScopeRepository(); // Interface: ScopeRepositoryInterface
$accessTokenRepository = new AccessTokenRepository(); // Interface: AccessTokenRepositoryInterface
$authCodeRepository = new AuthCodeRepository(); // Interface: AuthCodeRepositoryInterface
$refreshTokenRepository = new RefreshTokenRepository(); // Interface: RefreshTokenRepositoryInterface
$userRepository = new UserRepository(); //Interface: UserRepositoryInterface
// 私鑰與加密密鑰
$privateKey = 'file://path/to/private.key';
//$privateKey = new CryptKey('file://path/to/private.key', 'passphrase'); // 如果私鑰文件有密碼
$encryptionKey = 'lxZFUEsBCJ2Yb14IF2ygAHI5N4+ZAUXXaSeeJm6+twsUmIen'; // 加密密鑰字符串
// $encryptionKey = Key::loadFromAsciiSafeString($encryptionKey); //如果通過 generate-defuse-key 腳本生成的字符串,可使用此方法傳入
// 初始化 server
$server = new \League\OAuth2\Server\AuthorizationServer(
$clientRepository,
$accessTokenRepository,
$scopeRepository,
$privateKey,
$encryptionKey
);
初始化授權碼類型
// 授權碼授權類型初始化
$grant = new \League\OAuth2\Server\Grant\AuthCodeGrant(
$authCodeRepository,
$refreshTokenRepository,
new \DateInterval('PT10M') // 設置授權碼過期時間為10分鐘
);
$grant->setRefreshTokenTTL(new \DateInterval('P1M')); // 設置刷新令牌過期時間1個月
// 將授權碼授權類型添加進 server
$server->enableGrantType(
$grant,
new \DateInterval('PT1H') // 設置訪問令牌過期時間1小時
);
使用
注意:這里的示例演示的是 Slim Framework 的用法,Slim 不是這個庫的必要條件,只需要請求與響應符合PSR-7規范即可。
用戶向客戶端提出 OAuth 登錄請求,客戶端將用戶重定向授權服務器的地址(例如:https://example.com/authorize?response_type=code&client_id={client_id}&redirect_uri={redirect_uri}&scope{scope}&state={state}):
$app->get('/authorize', function (ServerRequestInterface $request, ResponseInterface $response) use ($server) {
try {
// 驗證 HTTP 請求,并返回 authRequest 對象
$authRequest = $server->validateAuthorizationRequest($request);
// 此時應將 authRequest 對象序列化后存在當前會話(session)中
$_SESSION['authRequest'] = serialize($authRequest);
// 然后將用戶重定向至登錄入口或在當前地址直接響應登錄頁面
return $response->getBody()->write(file_get_contents("login.html"));
} catch (OAuthServerException $exception) {
// 可以捕獲 OAuthServerException,將其轉為 HTTP 響應
return $exception->generateHttpResponse($response);
} catch (\Exception $exception) {
// 其他異常
$body = new Stream(fopen('php://temp', 'r+'));
$body->write($exception->getMessage());
return $response->withStatus(500)->withBody($body);
}
});
此時展示給用戶的是這樣的頁面:
用戶提交登錄后,設置好用戶實體(userEntity):
$app->post('/login', function (ServerRequestInterface $request, ResponseInterface $response) use ($server) {
try {
// 在會話(session)中取出 authRequest 對象
$authRequest = unserialize($_SESSION['authRequest']);
// 設置用戶實體(userEntity)
$authRequest->setUser(new UserEntity(1));
// 設置權限范圍
$authRequest->setScopes(['basic'])
// true = 批準,false = 拒絕
$authRequest->setAuthorizationApproved(true);
// 完成后重定向至客戶端請求重定向地址
return $server->completeAuthorizationRequest($authRequest, $response);
} catch (OAuthServerException $exception) {
// 可以捕獲 OAuthServerException,將其轉為 HTTP 響應
return $exception->generateHttpResponse($response);
} catch (\Exception $exception) {
// 其他異常
$body = new Stream(fopen('php://temp', 'r+'));
$body->write($exception->getMessage());
return $response->withStatus(500)->withBody($body);
}
});
客戶端通過授權碼請求訪問令牌:
$app->post('/access_token', function (ServerRequestInterface $request, ResponseInterface $response) use ($server) {
try {
// 這里只需要這一行就可以,具體的判斷在 Repositories 中
return $server->respondToAccessTokenRequest($request, $response);
} catch (\League\OAuth2\Server\Exception\OAuthServerException $exception) {
return $exception->generateHttpResponse($response);
} catch (\Exception $exception) {
$body = new Stream(fopen('php://temp', 'r+'));
$body->write($exception->getMessage());
return $response->withStatus(500)->withBody($body);
}
});
隱式授權模式
隱式授權相當于是授權碼模式的簡化版本:
流程(用戶可見)
用戶訪問客戶端,客戶端將用戶導向授權服務器時,將以下參數通過 GET query
傳入:
-
response_type
:授權類型,必選項,值固定為:token
-
client_id
:客戶端ID,必選項 -
redirect_uri
:重定向URI,可選項,不填寫時默認預先注冊的重定向URI -
scope
:權限范圍,可選項,以空格分隔 -
state
:CSRF令牌,可選項,但強烈建議使用,應將該值存儲與用戶會話中,以便在返回時驗證
用戶選擇是否給予客戶端授權
假設用戶給予授權,授權服務器將用戶導向客戶端事先指定的 redirect_uri
,并將以下參數通過 GET query
傳入:
-
token_type
:令牌類型,值固定為:Bearer
-
expires_in
:訪問令牌的存活時間 -
access_token
:訪問令牌 -
state
:請求中發送的state
,原樣返回。客戶端將此值與用戶會話中的值進行對比,以確保授權碼響應的是此應用程序而非其他應用程序
整個流程與授權碼模式的第一部分類似,只是授權服務器直接響應了訪問令牌,跳過了授權碼的步驟。它適用于沒有服務器,完全運行在前端的應用程序。
此模式下沒有刷新令牌(refresh token)的返回。
使用 oauth2-server 實現
初始化授權碼類型
// 將隱式授權類型添加進 server
$server->enableGrantType(
new ImplicitGrant(new \DateInterval('PT1H')),
new \DateInterval('PT1H') // 設置訪問令牌過期時間1小時
);
使用
注意:這里的示例演示的是 Slim Framework 的用法,Slim 不是這個庫的必要條件,只需要請求與響應符合PSR-7規范即可。
$app->post('/login', function (ServerRequestInterface $request, ResponseInterface $response) use ($server) {
try {
// 在會話(session)中取出 authRequest 對象
$authRequest = unserialize($_SESSION['authRequest']);
// 設置用戶實體(userEntity)
$authRequest->setUser(new UserEntity(1));
// 設置權限范圍
$authRequest->setScopes(['basic'])
// true = 批準,false = 拒絕
$authRequest->setAuthorizationApproved(true);
// 完成后重定向至客戶端請求重定向地址
return $server->completeAuthorizationRequest($authRequest, $response);
} catch (OAuthServerException $exception) {
// 可以捕獲 OAuthServerException,將其轉為 HTTP 響應
return $exception->generateHttpResponse($response);
} catch (\Exception $exception) {
// 其他異常
$body = new Stream(fopen('php://temp', 'r+'));
$body->write($exception->getMessage());
return $response->withStatus(500)->withBody($body);
}
});
此時展示給用戶的是這樣的頁面:
用戶提交登錄后,設置好用戶實體(userEntity):
$app->post('/login', function (ServerRequestInterface $request, ResponseInterface $response) use ($server) {
try {
// 在會話(session)中取出 authRequest 對象
$authRequest = unserialize($_SESSION['authRequest']);
// 設置用戶實體(userEntity)
$authRequest->setUser(new UserEntity(1));
// 設置權限范圍
$authRequest->setScopes(['basic'])
// true = 批準,false = 拒絕
$authRequest->setAuthorizationApproved(true);
// 完成后重定向至客戶端請求重定向地址
return $server->completeAuthorizationRequest($authRequest, $response);
} catch (OAuthServerException $exception) {
// 可以捕獲 OAuthServerException,將其轉為 HTTP 響應
return $exception->generateHttpResponse($response);
} catch (\Exception $exception) {
// 其他異常
$body = new Stream(fopen('php://temp', 'r+'));
$body->write($exception->getMessage());
return $response->withStatus(500)->withBody($body);
}
});
密碼模式
密碼模式是由用戶提供給客戶端賬號密碼來獲取訪問令牌,這屬于危險行為,所以此模式只適用于高度信任的客戶端(例如第一方應用程序)。客戶端不應存儲用戶的賬號密碼。
OAuth2 協議規定此模式不需要傳 client_id
& client_secret
,但 oauth-server 庫需要
流程
客戶端要求用戶提供授權憑據,通常是賬號密碼
然后,客戶端發送 POST
請求至授權服務器,攜帶以下參數:
-
grant_type
:授權類型,必選項,值固定為:password
-
client_id
:客戶端ID,必選項 -
client_secret
:客戶端 secret -
scope
:權限范圍,可選項,以空格分隔 -
username
:用戶賬號 -
password
:用戶密碼
授權服務器響應以下內容:
-
token_type
:令牌類型,值固定為:Bearer
-
expires_in
:訪問令牌的存活時間 -
access_token
:訪問令牌 -
refresh_token
:刷新令牌,訪問令牌過期后,使用刷新令牌重新獲取
使用 oauth2-server 實現
初始化授權碼類型
$grant = new \League\OAuth2\Server\Grant\PasswordGrant(
$userRepository,
$refreshTokenRepository
);
$grant->setRefreshTokenTTL(new \DateInterval('P1M')); // 設置刷新令牌過期時間1個月
// 將密碼授權類型添加進 server
$server->enableGrantType(
$grant,
new \DateInterval('PT1H') // 設置訪問令牌過期時間1小時
);
使用
注意:這里的示例演示的是 Slim Framework 的用法,Slim 不是這個庫的必要條件,只需要請求與響應符合PSR-7規范即可。
$app->post('/access_token', function (ServerRequestInterface $request, ResponseInterface $response) use ($server) {
try {
// 這里只需要這一行就可以,具體的判斷在 Repositories 中
return $server->respondToAccessTokenRequest($request, $response);
} catch (\League\OAuth2\Server\Exception\OAuthServerException $exception) {
return $exception->generateHttpResponse($response);
} catch (\Exception $exception) {
$body = new Stream(fopen('php://temp', 'r+'));
$body->write($exception->getMessage());
return $response->withStatus(500)->withBody($body);
}
});
客戶端模式
客戶端模式是指以客戶端的名義,而不是用戶的名義,向授權服務器獲取認證。在這個模式下,用戶與授權服務器不產生關系,用戶只能感知到的客戶端,所產生的資源也都由客戶端處理。
流程
客戶端發送 POST
請求至授權服務器,攜帶以下參數:
-
grant_type
:授權類型,必選項,值固定為:client_credentials
-
client_id
:客戶端ID,必選項 -
client_secret
:客戶端 secret -
scope
:權限范圍,可選項,以空格分隔
授權服務器響應以下內容:
-
token_type
:令牌類型,值固定為:Bearer
-
expires_in
:訪問令牌的存活時間 -
access_token
:訪問令牌
此模式下無需刷新令牌(refresh token)的返回。
使用 oauth2-server 實現
初始化授權碼類型
// 將客戶端授權類型添加進 server
$server->enableGrantType(
new \League\OAuth2\Server\Grant\ClientCredentialsGrant(),
new \DateInterval('PT1H') // 設置訪問令牌過期時間1小時
);
使用
注意:這里的示例演示的是 Slim Framework 的用法,Slim 不是這個庫的必要條件,只需要請求與響應符合PSR-7規范即可。
$app->post('/access_token', function (ServerRequestInterface $request, ResponseInterface $response) use ($server) {
try {
// 這里只需要這一行就可以,具體的判斷在 Repositories 中
return $server->respondToAccessTokenRequest($request, $response);
} catch (\League\OAuth2\Server\Exception\OAuthServerException $exception) {
return $exception->generateHttpResponse($response);
} catch (\Exception $exception) {
$body = new Stream(fopen('php://temp', 'r+'));
$body->write($exception->getMessage());
return $response->withStatus(500)->withBody($body);
}
});
刷新訪問令牌(access token)
訪問令牌有一個較短的存活時間,在過期后,客戶端通過刷新令牌來獲得新的訪問令牌與刷新令牌。當用戶長時間不活躍,刷新令牌也過期后,就需要重新獲取授權。
流程
客戶端發送 POST
請求至授權服務器,攜帶以下參數:
-
grant_type
:授權類型,必選項,值固定為:refresh_token
-
client_id
:客戶端ID,必選項 -
client_secret
:客戶端 secret -
scope
:權限范圍,可選項,以空格分隔 -
refresh_token
:刷新令牌
授權服務器響應以下內容:
-
token_type
:令牌類型,值固定為:Bearer
-
expires_in
:訪問令牌的存活時間 -
access_token
:訪問令牌 -
refresh_token
:刷新令牌,訪問令牌過期后,使用刷新令牌重新獲取
使用 oauth2-server 實現
初始化授權碼類型
$grant = new \League\OAuth2\Server\Grant\RefreshTokenGrant($refreshTokenRepository);
$grant->setRefreshTokenTTL(new \DateInterval('P1M')); // 新的刷新令牌過期時間1個月
// 將刷新訪問令牌添加進 server
$server->enableGrantType(
$grant,
new \DateInterval('PT1H') // 新的訪問令牌過期時間1小時
);
使用
$app->post('/access_token', function (ServerRequestInterface $request, ResponseInterface $response) use ($server) {
try {
// 這里只需要這一行就可以,具體的判斷在 Repositories 中
return $server->respondToAccessTokenRequest($request, $response);
} catch (\League\OAuth2\Server\Exception\OAuthServerException $exception) {
return $exception->generateHttpResponse($response);
} catch (\Exception $exception) {
$body = new Stream(fopen('php://temp', 'r+'));
$body->write($exception->getMessage());
return $response->withStatus(500)->withBody($body);
}
});
資源服務器驗證訪問令牌
oauth2-server 為資源服務器提供了一個中間件用于驗證訪問令牌。
客戶端需要在 HTTP Header
中使用 Authorization
傳入訪問令牌,如果通過,中間件將會在 request
中加入對應數據:
-
oauth_access_token_id
:訪問令牌 id -
oauth_client_id
: 客戶端id -
oauth_user_id
:用戶id -
oauth_scopes
:權限范圍
授權不通過,則拋出 OAuthServerException::accessDenied
異常。
// 初始化
$accessTokenRepository = new AccessTokenRepository(); // Interface: AccessTokenRepositoryInterface
// 授權服務器分發的公鑰
$publicKeyPath = 'file://path/to/public.key';
// 創建 ResourceServer
$server = new \League\OAuth2\Server\ResourceServer(
$accessTokenRepository,
$publicKeyPath
);
// 中間件
new \League\OAuth2\Server\Middleware\ResourceServerMiddleware($server);
如果所用路由不支持中間件,可自行實現,符合PSR-7規范即可 :
try {
$request = $server->validateAuthenticatedRequest($request);
} catch (OAuthServerException $exception) {
return $exception->generateHttpResponse($response);
} catch (\Exception $exception) {
return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500))->generateHttpResponse($response);
}
oauth2-server 實現
oauth2-server 的實現需要我們手動創建 Repositories 與 Entities,下面展示一個項目目錄示例:
- Entities
- AccessTokenEntity.php
- AuthCodeEntity.php
- ClientEntity.php
- RefreshTokenEntity.php
- ScopeEntity.php
- UserEntity.php
- Repositories
- AccessTokenRepository.php
- AuthCodeRepository.php
- ClientRepository.php
- RefreshTokenRepository.php
- ScopeRepository.php
- UserRepository.php
Repositories
Repositories 里主要是處理關于授權碼、訪問令牌等數據的存儲邏輯,oauth2-server 提供了 Interfaces 來定義所需要實現的方法。
class AccessTokenRepository implements AccessTokenRepositoryInterface
{
/**
* @return AccessTokenEntityInterface
*/
public function getNewToken(ClientEntityInterface $clientEntity, array $scopes, $userIdentifier = null)
{
// 創建新訪問令牌時調用方法
// 需要返回 AccessTokenEntityInterface 對象
// 需要在返回前,向 AccessTokenEntity 傳入參數中對應屬性
// 示例代碼:
$accessToken = new AccessTokenEntity();
$accessToken->setClient($clientEntity);
foreach ($scopes as $scope) {
$accessToken->addScope($scope);
}
$accessToken->setUserIdentifier($userIdentifier);
return $accessToken;
}
public function persistNewAccessToken(AccessTokenEntityInterface $accessTokenEntity)
{
// 創建新訪問令牌時調用此方法
// 可以用于持久化存儲訪問令牌,持久化數據庫自行選擇
// 可以使用參數中的 AccessTokenEntityInterface 對象,獲得有價值的信息:
// $accessTokenEntity->getIdentifier(); // 獲得令牌唯一標識符
// $accessTokenEntity->getExpiryDateTime(); // 獲得令牌過期時間
// $accessTokenEntity->getUserIdentifier(); // 獲得用戶標識符
// $accessTokenEntity->getScopes(); // 獲得權限范圍
// $accessTokenEntity->getClient()->getIdentifier(); // 獲得客戶端標識符
}
public function revokeAccessToken($tokenId)
{
// 使用刷新令牌創建新的訪問令牌時調用此方法
// 參數為原訪問令牌的唯一標識符
// 可將其在持久化存儲中過期
}
public function isAccessTokenRevoked($tokenId)
{
// 資源服務器驗證訪問令牌時將調用此方法
// 用于驗證訪問令牌是否已被刪除
// return true 已刪除,false 未刪除
return false;
}
}
class AuthCodeRepository implements AuthCodeRepositoryInterface
{
/**
* @return AuthCodeEntityInterface
*/
public function getNewAuthCode()
{
// 創建新授權碼時調用方法
// 需要返回 AuthCodeEntityInterface 對象
return new AuthCodeEntity();
}
public function persistNewAuthCode(AuthCodeEntityInterface $authCodeEntity)
{
// 創建新授權碼時調用此方法
// 可以用于持久化存儲授權碼,持久化數據庫自行選擇
// 可以使用參數中的 AuthCodeEntityInterface 對象,獲得有價值的信息:
// $authCodeEntity->getIdentifier(); // 獲得授權碼唯一標識符
// $authCodeEntity->getExpiryDateTime(); // 獲得授權碼過期時間
// $authCodeEntity->getUserIdentifier(); // 獲得用戶標識符
// $authCodeEntity->getScopes(); // 獲得權限范圍
// $authCodeEntity->getClient()->getIdentifier(); // 獲得客戶端標識符
}
public function revokeAuthCode($codeId)
{
// 當使用授權碼獲取訪問令牌時調用此方法
// 可以在此時將授權碼從持久化數據庫中刪除
// 參數為授權碼唯一標識符
}
public function isAuthCodeRevoked($codeId)
{
// 當使用授權碼獲取訪問令牌時調用此方法
// 用于驗證授權碼是否已被刪除
// return true 已刪除,false 未刪除
return false;
}
}
class ClientRepository implements ClientRepositoryInterface
{
/**
* @return ClientEntityInterface
*/
public function getClientEntity($clientIdentifier, $grantType = null, $clientSecret = null, $mustValidateSecret = true)
{
// 獲取客戶端對象時調用方法,用于驗證客戶端
// 需要返回 ClientEntityInterface 對象
// $clientIdentifier 客戶端唯一標識符
// $grantType 代表授權類型,根據類型不同,驗證方式也不同
// $clientSecret 代表客戶端密鑰,是客戶端事先在授權服務器中注冊時得到的
// $mustValidateSecret 代表是否需要驗證客戶端密鑰
$client = new ClientEntity();
$client->setIdentifier($clientIdentifier);
return $client;
}
}
class RefreshTokenRepository implements RefreshTokenRepositoryInterface
{
/**
* @return RefreshTokenEntityInterface
*/
public function getNewRefreshToken()
{
// 創建新授權碼時調用方法
// 需要返回 RefreshTokenEntityInterface 對象
return new RefreshTokenEntity();
}
public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity)
{
// 創建新刷新令牌時調用此方法
// 用于持久化存儲授刷新令牌
// 可以使用參數中的 RefreshTokenEntityInterface 對象,獲得有價值的信息:
// $refreshTokenEntity->getIdentifier(); // 獲得刷新令牌唯一標識符
// $refreshTokenEntity->getExpiryDateTime(); // 獲得刷新令牌過期時間
// $refreshTokenEntity->getAccessToken()->getIdentifier(); // 獲得訪問令牌標識符
}
public function revokeRefreshToken($tokenId)
{
// 當使用刷新令牌獲取訪問令牌時調用此方法
// 原刷新令牌將刪除,創建新的刷新令牌
// 參數為原刷新令牌唯一標識
// 可在此刪除原刷新令牌
}
public function isRefreshTokenRevoked($tokenId)
{
// 當使用刷新令牌獲取訪問令牌時調用此方法
// 用于驗證刷新令牌是否已被刪除
// return true 已刪除,false 未刪除
return false;
}
}
class ScopeRepository implements ScopeRepositoryInterface
{
/**
* @return ScopeEntityInterface
*/
public function getScopeEntityByIdentifier($identifier)
{
// 驗證權限是否在權限范圍中會調用此方法
// 參數為單個權限標識符
// ......
// 驗證成功則返回 ScopeEntityInterface 對象
$scope = new ScopeEntity();
$scope->setIdentifier($identifier);
return $scope;
}
public function finalizeScopes(
array $scopes,
$grantType,
ClientEntityInterface $clientEntity,
$userIdentifier = null
) {
// 在創建授權碼與訪問令牌前會調用此方法
// 用于驗證權限范圍、授權類型、客戶端、用戶是否匹配
// 可整合進項目自身的權限控制中
// 必須返回 ScopeEntityInterface 對象可用的 scope 數組
// 示例:
// $scope = new ScopeEntity();
// $scope->setIdentifier('example');
// $scopes[] = $scope;
return $scopes;
}
}
class UserRepository implements UserRepositoryInterface
{
/**
* @return UserEntityInterface
*/
public function getUserEntityByUserCredentials(
$username,
$password,
$grantType,
ClientEntityInterface $clientEntity
) {
// 驗證用戶時調用此方法
// 用于驗證用戶信息是否符合
// 可以驗證是否為用戶可使用的授權類型($grantType)與客戶端($clientEntity)
// 驗證成功返回 UserEntityInterface 對象
$user = new UserEntity();
$user->setIdentifier(1);
return $user;
}
}
Entities
Entities 里是 oauth2-server 處理授權與認證邏輯的類,它為我們提供了 Interfaces 來定義需要實現的方法,同時提供了 Traits 幫助我們實現,可以選擇使用,有需要時也可以重寫。
class AccessTokenEntity implements AccessTokenEntityInterface
{
use AccessTokenTrait, TokenEntityTrait, EntityTrait;
}
class AuthCodeEntity implements AuthCodeEntityInterface
{
use EntityTrait, TokenEntityTrait, AuthCodeTrait;
}
class ClientEntity implements ClientEntityInterface
{
use EntityTrait, ClientTrait;
}
class RefreshTokenEntity implements RefreshTokenEntityInterface
{
use RefreshTokenTrait, EntityTrait;
}
class ScopeEntity implements ScopeEntityInterface
{
use EntityTrait;
// 沒有 Trait 實現這個方法,需要自行實現
// oauth2-server 項目的測試代碼的實現例子
public function jsonSerialize()
{
return $this->getIdentifier();
}
}
class UserEntity implements UserEntityInterface
{
use EntityTrait;
}
Interfaces
Repositories
League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface.php
League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface.php
League\OAuth2\Server\Repositories\ClientRepositoryInterface.php
League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface.php
League\OAuth2\Server\Repositories\ScopeRepositoryInterface.php
League\OAuth2\Server\Repositories\UserRepositoryInterface.php
Entities
- League\OAuth2\Server\Entities\AccessTokenEntityInterface.php
- League\OAuth2\Server\Entities\AuthCodeEntityInterface.php
- League\OAuth2\Server\Entities\ClientEntityInterface.php
- League\OAuth2\Server\Entities\RefreshTokenEntityInterface.php
- League\OAuth2\Server\Entities\ScopeEntityInterface.php
- League\OAuth2\Server\Entities\TokenInterface.php
- League\OAuth2\Server\Entities\UserEntityInterface.php
Traits
- League\OAuth2\Server\Entities\Traits\AccessTokenTrait.php
- League\OAuth2\Server\Entities\Traits\AuthCodeTrait.php
- League\OAuth2\Server\Entities\Traits\ClientTrait.php
- League\OAuth2\Server\Entities\Traits\EntityTrait.php
- League\OAuth2\Server\Entities\Traits\RefreshTokenTrait.php
- League\OAuth2\Server\Entities\Traits\ScopeTrait.php
- League\OAuth2\Server\Entities\Traits\TokenEntityTrait.php
事件
oauth2-server 預設了一些事件,目前官方文檔中只有兩個,余下的可以在 RequestEvent.php 文件中查看。
client.authentication.failed
$server->getEmitter()->addListener(
'client.authentication.failed',
function (\League\OAuth2\Server\RequestEvent $event) {
// do something
}
);
客戶端身份驗證未通過時觸發此事件。你可以在客戶端嘗試 n
次失敗后禁止它一段時間內的再次嘗試。
user.authentication.failed
$server->getEmitter()->addListener(
'user.authentication.failed',
function (\League\OAuth2\Server\RequestEvent $event) {
// do something
}
);
用戶身份驗證未通過時觸發此事件。你可以通過這里提醒用戶重置密碼,或嘗試 n
次后禁止用戶再次嘗試。
參考文章
《oauth2-server 官方文檔》(https://oauth2.thephpleague.com/)
《理解OAuth 2.0》-阮一峰(http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html)