OAuth2.0 與 oauth2-server 庫的使用

OAuth2.0 是關(guān)于授權(quán)的開放網(wǎng)絡(luò)標(biāo)準(zhǔn),它允許用戶已第三方應(yīng)用獲取該用戶在某一網(wǎng)站的私密資源,而無需提供用戶名與密碼,目前已在全世界得到廣泛應(yīng)用。

league/oauth2-server 是一個輕量級并且功能強(qiáng)大的符合 OAuth2.0 協(xié)議的 PHP 庫,使用它可以構(gòu)建出標(biāo)準(zhǔn)的 OAuth2.0 授權(quán)服務(wù)器。

本文通過對 PHP 庫:league/oauth2-server 進(jìn)行實(shí)踐的同時,理解 OAuth2.0 的工作流程與設(shè)計思路。

術(shù)語

了解 OAuth2.0 與 oauth2-server 的專用術(shù)語,對于理解后面內(nèi)容很有幫助。

OAuth2.0 定義了四個角色

  1. Client:客戶端,第三方應(yīng)用程序。
  2. Resource Owner:資源所有者,授權(quán) Client 訪問其帳戶的用戶。
  3. Authorization server:授權(quán)服務(wù)器,服務(wù)商專用于處理用戶授權(quán)認(rèn)證的服務(wù)器。
  4. Resource server:資源服務(wù)器,服務(wù)商用于存放用戶受保護(hù)資源的服務(wù)器,它可以與授權(quán)服務(wù)器是同一臺服務(wù)器,也可以是不同的服務(wù)器。

oauth2-server

  1. Access token:用于訪問受保護(hù)資源的令牌。
  2. Authorization code:發(fā)放給應(yīng)用程序的中間令牌,客戶端應(yīng)用使用此令牌交換 access token。
  3. Scope:授予應(yīng)用程序的權(quán)限范圍。
  4. JWTJson Web Token 是一種用于安全傳輸?shù)臄?shù)據(jù)傳輸格式。

運(yùn)行流程

flowchart.png

安裝

推薦使用 Composer 進(jìn)行安裝:

composer require league/oauth2-server

根據(jù)授權(quán)模式的不同,oauth2-server 提供了不同的 Interface 與 Triat 幫助實(shí)現(xiàn)。

本文發(fā)布時,版本號為7.3.1。

生成公鑰與私鑰

公鑰與私鑰用于簽名和驗(yàn)證傳輸?shù)?JWT,授權(quán)服務(wù)器使用私鑰簽名 JWT,資源服務(wù)器擁有公鑰驗(yàn)證 JWT。

oauth2-server 使用 JWT 傳輸訪問令牌(access token),方便資源服務(wù)器獲取其中內(nèi)容,所以需要使用非對稱加密。

生成私鑰,在終端中運(yùn)行:

openssl genrsa -out private.key 2048

使用私鑰提取私鑰:

openssl rsa -in private.key -pubout -out public.key

私鑰必須保密于授權(quán)服務(wù)器中,并將公鑰分發(fā)給資源服務(wù)器。

生成加密密鑰

加密密鑰用于加密授權(quán)碼(auth code)與刷新令牌(refesh token),AuthorizationServer(授權(quán)服務(wù)器啟動類)接受兩種加密密鑰,stringdefuse/php-encryption 庫的對象。

加密授權(quán)碼(auth code)與刷新令牌(refesh token)只有授權(quán)權(quán)服務(wù)器使用,所以使用對稱加密。

生成字符串密鑰,在終端中輸入:

php -r 'echo base64_encode(random_bytes(32)), PHP_EOL;'

生成對象,在項目根目錄的終端中輸入:

vendor/bin/generate-defuse-key

將獲得的內(nèi)容,傳入 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

授權(quán)模式

OAuth2.0 定義了四種授權(quán)模式,以應(yīng)對不同情況時的授權(quán)。

  1. 授權(quán)碼模式
  2. 隱式授權(quán)模式
  3. 密碼模式
  4. 客戶端模式

客戶端類型

  • 保密的:
    • 客戶端可以安全的存儲自己與用戶的憑據(jù)(例如:有所屬的服務(wù)器端)
  • 公開的:
    • 客戶端無法安全的存儲自己與用戶的憑據(jù)(例如:運(yùn)行在瀏覽器的單頁應(yīng)用)

選用哪種授權(quán)模式?

如果客戶端是保密的,應(yīng)使用授權(quán)碼模式。

如果客戶端是公開的,應(yīng)使用隱式授權(quán)模式。

如果用戶對于此客戶端高度信任(例如:第一方應(yīng)用程序或操作系統(tǒng)程序),應(yīng)使用密碼模式。

如果客戶端是以自己的名義,不與用戶產(chǎn)生關(guān)系,應(yīng)使用客戶端模式

預(yù)先注冊

客戶端需要預(yù)先在授權(quán)服務(wù)器進(jìn)行注冊,用以獲取 client_idclient_secret,也可以在注冊是預(yù)先設(shè)定好 redirect_uri,以便于之后可以使用默認(rèn)的 redirect_uri。

授權(quán)碼模式

授權(quán)碼模式是 OAuth2.0 種功能最完整,流程最嚴(yán)密的一種模式,如果你使用過 Google 或 QQ 登錄過第三方應(yīng)用程序,應(yīng)該會對這個流程的第一部分很熟悉。

流程

第一部分(用戶可見)

用戶訪問客戶端,客戶端將用戶導(dǎo)向授權(quán)服務(wù)器時,將以下參數(shù)通過 GET query 傳入:

  • response_type:授權(quán)類型,必選項,值固定為:code
  • client_id:客戶端ID,必選項
  • redirect_uri:重定向URI,可選項,不填寫時默認(rèn)預(yù)先注冊的重定向URI
  • scope:權(quán)限范圍,可選項,以空格分隔
  • stateCSRF令牌,可選項,但強(qiáng)烈建議使用,應(yīng)將該值存儲與用戶會話中,以便在返回時驗(yàn)證

用戶選擇是否給予客戶端授權(quán)

假設(shè)用戶給予授權(quán),授權(quán)服務(wù)器將用戶導(dǎo)向客戶端事先指定的 redirect_uri,并將以下參數(shù)通過 GET query 傳入:

  • code:授權(quán)碼(Authorization code)
  • state:請求中發(fā)送的 state,原樣返回??蛻舳藢⒋酥蹬c用戶會話中的值進(jìn)行對比,以確保授權(quán)碼響應(yīng)的是此客戶端而非其他客戶端程序

第二部分(用戶不可見)

客戶端已得到授權(quán),通過 POST 請求向授權(quán)服務(wù)器獲取訪問令牌(access token):

  • grant_type:授權(quán)模式,值固定為:authorization_code
  • client_id:客戶端ID
  • client_secret:客戶端 secret
  • redirect_uri:使用與第一部分請求相同的 URI
  • code:第一部分所獲的的授權(quán)碼,要注意URL解碼

授權(quán)服務(wù)器核對授權(quán)碼與重定向 URI,確認(rèn)無誤后,向客戶端響應(yīng)下列內(nèi)容:

  • token_type:令牌類型,值固定為:Bearer

  • expires_in:訪問令牌的存活時間

  • access_token:訪問令牌

  • refresh_token:刷新令牌,訪問令牌過期后,使用刷新令牌重新獲取

使用 oauth2-server 實(shí)現(xiàn)

初始化

OAuth2.0 只是協(xié)議,在實(shí)現(xiàn)上需要聯(lián)系到用戶與數(shù)據(jù)庫存儲,oauth2-server 的新版本并沒有指定某種數(shù)據(jù)庫,但它提供了 InterfacesTraits 幫助我們實(shí)現(xiàn),這讓我們可以方便的使用任何形式的數(shù)據(jù)存儲方式,這種方便的代價就是需要我們自行創(chuàng)建 RepositoriesEntities

初始化 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
);
初始化授權(quán)碼類型
// 授權(quán)碼授權(quán)類型初始化
$grant = new \League\OAuth2\Server\Grant\AuthCodeGrant(
    $authCodeRepository,
    $refreshTokenRepository,
    new \DateInterval('PT10M') // 設(shè)置授權(quán)碼過期時間為10分鐘
);
$grant->setRefreshTokenTTL(new \DateInterval('P1M')); // 設(shè)置刷新令牌過期時間1個月

// 將授權(quán)碼授權(quán)類型添加進(jìn) server
$server->enableGrantType(
    $grant,
    new \DateInterval('PT1H') // 設(shè)置訪問令牌過期時間1小時
);

DateInterval

使用

注意:這里的示例演示的是 Slim Framework 的用法,Slim 不是這個庫的必要條件,只需要請求與響應(yīng)符合PSR-7規(guī)范即可。

用戶向客戶端提出 OAuth 登錄請求,客戶端將用戶重定向授權(quán)服務(wù)器的地址(例如: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 {
        // 驗(yàn)證 HTTP 請求,并返回 authRequest 對象
        $authRequest = $server->validateAuthorizationRequest($request);
        // 此時應(yīng)將 authRequest 對象序列化后存在當(dāng)前會話(session)中
        $_SESSION['authRequest'] = serialize($authRequest);
        // 然后將用戶重定向至登錄入口或在當(dāng)前地址直接響應(yīng)登錄頁面
        return $response->getBody()->write(file_get_contents("login.html"));
        
    } catch (OAuthServerException $exception) {
        // 可以捕獲 OAuthServerException,將其轉(zhuǎn)為 HTTP 響應(yīng)
        return $exception->generateHttpResponse($response);
        
    } catch (\Exception $exception) {
        // 其他異常
        $body = new Stream(fopen('php://temp', 'r+'));
        $body->write($exception->getMessage());
        return $response->withStatus(500)->withBody($body);
        
    }
});

此時展示給用戶的是這樣的頁面:


qq-oauth.png

用戶提交登錄后,設(shè)置好用戶實(shí)體(userEntity):

$app->post('/login', function (ServerRequestInterface $request, ResponseInterface $response) use ($server) {
    try {
        // 在會話(session)中取出 authRequest 對象
        $authRequest = unserialize($_SESSION['authRequest']);
        // 設(shè)置用戶實(shí)體(userEntity)
        $authRequest->setUser(new UserEntity(1));
        // 設(shè)置權(quán)限范圍
        $authRequest->setScopes(['basic'])
        // true = 批準(zhǔn),false = 拒絕
        $authRequest->setAuthorizationApproved(true);
        // 完成后重定向至客戶端請求重定向地址
        return $server->completeAuthorizationRequest($authRequest, $response);
    } catch (OAuthServerException $exception) {
        // 可以捕獲 OAuthServerException,將其轉(zhuǎn)為 HTTP 響應(yīng)
        return $exception->generateHttpResponse($response);
    } catch (\Exception $exception) {
        // 其他異常
        $body = new Stream(fopen('php://temp', 'r+'));
        $body->write($exception->getMessage());
        return $response->withStatus(500)->withBody($body);
    }
});

客戶端通過授權(quán)碼請求訪問令牌:

$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);
    }
});

隱式授權(quán)模式

隱式授權(quán)相當(dāng)于是授權(quán)碼模式的簡化版本:

流程(用戶可見)

用戶訪問客戶端,客戶端將用戶導(dǎo)向授權(quán)服務(wù)器時,將以下參數(shù)通過 GET query 傳入:

  • response_type:授權(quán)類型,必選項,值固定為:token
  • client_id:客戶端ID,必選項
  • redirect_uri:重定向URI,可選項,不填寫時默認(rèn)預(yù)先注冊的重定向URI
  • scope:權(quán)限范圍,可選項,以空格分隔
  • stateCSRF令牌,可選項,但強(qiáng)烈建議使用,應(yīng)將該值存儲與用戶會話中,以便在返回時驗(yàn)證

用戶選擇是否給予客戶端授權(quán)

假設(shè)用戶給予授權(quán),授權(quán)服務(wù)器將用戶導(dǎo)向客戶端事先指定的 redirect_uri,并將以下參數(shù)通過 GET query 傳入:

  • token_type:令牌類型,值固定為:Bearer
  • expires_in:訪問令牌的存活時間
  • access_token:訪問令牌
  • state:請求中發(fā)送的 state,原樣返回??蛻舳藢⒋酥蹬c用戶會話中的值進(jìn)行對比,以確保授權(quán)碼響應(yīng)的是此應(yīng)用程序而非其他應(yīng)用程序

整個流程與授權(quán)碼模式的第一部分類似,只是授權(quán)服務(wù)器直接響應(yīng)了訪問令牌,跳過了授權(quán)碼的步驟。它適用于沒有服務(wù)器,完全運(yùn)行在前端的應(yīng)用程序。

此模式下沒有刷新令牌(refresh token)的返回。

使用 oauth2-server 實(shí)現(xiàn)

初始化 server

初始化授權(quán)碼類型
// 將隱式授權(quán)類型添加進(jìn) server
$server->enableGrantType(
    new ImplicitGrant(new \DateInterval('PT1H')),
    new \DateInterval('PT1H') // 設(shè)置訪問令牌過期時間1小時
);

DateInterval

使用

注意:這里的示例演示的是 Slim Framework 的用法,Slim 不是這個庫的必要條件,只需要請求與響應(yīng)符合PSR-7規(guī)范即可。

$app->post('/login', function (ServerRequestInterface $request, ResponseInterface $response) use ($server) {
    try {
        // 在會話(session)中取出 authRequest 對象
        $authRequest = unserialize($_SESSION['authRequest']);
        // 設(shè)置用戶實(shí)體(userEntity)
        $authRequest->setUser(new UserEntity(1));
        // 設(shè)置權(quán)限范圍
        $authRequest->setScopes(['basic'])
        // true = 批準(zhǔn),false = 拒絕
        $authRequest->setAuthorizationApproved(true);
        // 完成后重定向至客戶端請求重定向地址
        return $server->completeAuthorizationRequest($authRequest, $response);
    } catch (OAuthServerException $exception) {
        // 可以捕獲 OAuthServerException,將其轉(zhuǎn)為 HTTP 響應(yīng)
        return $exception->generateHttpResponse($response);
    } catch (\Exception $exception) {
        // 其他異常
        $body = new Stream(fopen('php://temp', 'r+'));
        $body->write($exception->getMessage());
        return $response->withStatus(500)->withBody($body);
    }
});

此時展示給用戶的是這樣的頁面:


qq-oauth.png

用戶提交登錄后,設(shè)置好用戶實(shí)體(userEntity):

$app->post('/login', function (ServerRequestInterface $request, ResponseInterface $response) use ($server) {
   
    try {
          // 在會話(session)中取出 authRequest 對象
        $authRequest = unserialize($_SESSION['authRequest']);
                // 設(shè)置用戶實(shí)體(userEntity)
            $authRequest->setUser(new UserEntity(1));
                // 設(shè)置權(quán)限范圍
                $authRequest->setScopes(['basic'])
                // true = 批準(zhǔn),false = 拒絕
            $authRequest->setAuthorizationApproved(true);
                // 完成后重定向至客戶端請求重定向地址
            return $server->completeAuthorizationRequest($authRequest, $response);
    } catch (OAuthServerException $exception) {
        // 可以捕獲 OAuthServerException,將其轉(zhuǎn)為 HTTP 響應(yīng)
        return $exception->generateHttpResponse($response);
    } catch (\Exception $exception) {
        // 其他異常
        $body = new Stream(fopen('php://temp', 'r+'));
        $body->write($exception->getMessage());
        return $response->withStatus(500)->withBody($body);
    }
});

密碼模式

密碼模式是由用戶提供給客戶端賬號密碼來獲取訪問令牌,這屬于危險行為,所以此模式只適用于高度信任的客戶端(例如第一方應(yīng)用程序)。客戶端不應(yīng)存儲用戶的賬號密碼。

OAuth2 協(xié)議規(guī)定此模式不需要傳 client_id & client_secret,但 oauth-server 庫需要

流程

客戶端要求用戶提供授權(quán)憑據(jù),通常是賬號密碼

然后,客戶端發(fā)送 POST 請求至授權(quán)服務(wù)器,攜帶以下參數(shù):

  • grant_type:授權(quán)類型,必選項,值固定為:password
  • client_id:客戶端ID,必選項
  • client_secret:客戶端 secret
  • scope:權(quán)限范圍,可選項,以空格分隔
  • username:用戶賬號
  • password:用戶密碼

授權(quán)服務(wù)器響應(yīng)以下內(nèi)容:

  • token_type:令牌類型,值固定為:Bearer
  • expires_in:訪問令牌的存活時間
  • access_token:訪問令牌
  • refresh_token:刷新令牌,訪問令牌過期后,使用刷新令牌重新獲取

使用 oauth2-server 實(shí)現(xiàn)

初始化 server

初始化授權(quán)碼類型
$grant = new \League\OAuth2\Server\Grant\PasswordGrant(
     $userRepository,
     $refreshTokenRepository
);

$grant->setRefreshTokenTTL(new \DateInterval('P1M')); // 設(shè)置刷新令牌過期時間1個月

// 將密碼授權(quán)類型添加進(jìn) server
$server->enableGrantType(
    $grant,
    new \DateInterval('PT1H') // 設(shè)置訪問令牌過期時間1小時
);

DateInterval

使用

注意:這里的示例演示的是 Slim Framework 的用法,Slim 不是這個庫的必要條件,只需要請求與響應(yīng)符合PSR-7規(guī)范即可。

$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);
    }
});

客戶端模式

客戶端模式是指以客戶端的名義,而不是用戶的名義,向授權(quán)服務(wù)器獲取認(rèn)證。在這個模式下,用戶與授權(quán)服務(wù)器不產(chǎn)生關(guān)系,用戶只能感知到的客戶端,所產(chǎn)生的資源也都由客戶端處理。

流程

客戶端發(fā)送 POST 請求至授權(quán)服務(wù)器,攜帶以下參數(shù):

  • grant_type:授權(quán)類型,必選項,值固定為:client_credentials
  • client_id:客戶端ID,必選項
  • client_secret:客戶端 secret
  • scope:權(quán)限范圍,可選項,以空格分隔

授權(quán)服務(wù)器響應(yīng)以下內(nèi)容:

  • token_type:令牌類型,值固定為:Bearer
  • expires_in:訪問令牌的存活時間
  • access_token:訪問令牌

此模式下無需刷新令牌(refresh token)的返回。

使用 oauth2-server 實(shí)現(xiàn)

初始化 server

初始化授權(quán)碼類型
// 將客戶端授權(quán)類型添加進(jìn) server
$server->enableGrantType(
    new \League\OAuth2\Server\Grant\ClientCredentialsGrant(),
    new \DateInterval('PT1H') // 設(shè)置訪問令牌過期時間1小時
);

DateInterval

使用

注意:這里的示例演示的是 Slim Framework 的用法,Slim 不是這個庫的必要條件,只需要請求與響應(yīng)符合PSR-7規(guī)范即可。

$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)

訪問令牌有一個較短的存活時間,在過期后,客戶端通過刷新令牌來獲得新的訪問令牌與刷新令牌。當(dāng)用戶長時間不活躍,刷新令牌也過期后,就需要重新獲取授權(quán)。

流程

客戶端發(fā)送 POST 請求至授權(quán)服務(wù)器,攜帶以下參數(shù):

  • grant_type:授權(quán)類型,必選項,值固定為:refresh_token
  • client_id:客戶端ID,必選項
  • client_secret:客戶端 secret
  • scope:權(quán)限范圍,可選項,以空格分隔
  • refresh_token:刷新令牌

授權(quán)服務(wù)器響應(yīng)以下內(nèi)容:

  • token_type:令牌類型,值固定為:Bearer
  • expires_in:訪問令牌的存活時間
  • access_token:訪問令牌
  • refresh_token:刷新令牌,訪問令牌過期后,使用刷新令牌重新獲取

使用 oauth2-server 實(shí)現(xiàn)

初始化 server

初始化授權(quán)碼類型
$grant = new \League\OAuth2\Server\Grant\RefreshTokenGrant($refreshTokenRepository);
$grant->setRefreshTokenTTL(new \DateInterval('P1M')); // 新的刷新令牌過期時間1個月

// 將刷新訪問令牌添加進(jìn) server
$server->enableGrantType(
    $grant,
    new \DateInterval('PT1H') // 新的訪問令牌過期時間1小時
);

DateInterval

使用
$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);
    }
});

資源服務(wù)器驗(yàn)證訪問令牌

oauth2-server 為資源服務(wù)器提供了一個中間件用于驗(yàn)證訪問令牌。

客戶端需要在 HTTP Header 中使用 Authorization 傳入訪問令牌,如果通過,中間件將會在 request 中加入對應(yīng)數(shù)據(jù):

  • oauth_access_token_id:訪問令牌 id
  • oauth_client_id: 客戶端id
  • oauth_user_id:用戶id
  • oauth_scopes:權(quán)限范圍

授權(quán)不通過,則拋出 OAuthServerException::accessDenied 異常。

// 初始化
$accessTokenRepository = new AccessTokenRepository(); // Interface: AccessTokenRepositoryInterface

// 授權(quán)服務(wù)器分發(fā)的公鑰
$publicKeyPath = 'file://path/to/public.key';
        
// 創(chuàng)建 ResourceServer
$server = new \League\OAuth2\Server\ResourceServer(
    $accessTokenRepository,
    $publicKeyPath
);

// 中間件
new \League\OAuth2\Server\Middleware\ResourceServerMiddleware($server);

如果所用路由不支持中間件,可自行實(shí)現(xiàn),符合PSR-7規(guī)范即可 :

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 實(shí)現(xiàn)

oauth2-server 的實(shí)現(xiàn)需要我們手動創(chuàng)建 RepositoriesEntities,下面展示一個項目目錄示例:

- 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 里主要是處理關(guān)于授權(quán)碼、訪問令牌等數(shù)據(jù)的存儲邏輯,oauth2-server 提供了 Interfaces 來定義所需要實(shí)現(xiàn)的方法。

class AccessTokenRepository implements AccessTokenRepositoryInterface
{
    /**
     * @return AccessTokenEntityInterface
     */
    public function getNewToken(ClientEntityInterface $clientEntity, array $scopes, $userIdentifier = null)
    {
        // 創(chuàng)建新訪問令牌時調(diào)用方法
        // 需要返回 AccessTokenEntityInterface 對象
        // 需要在返回前,向 AccessTokenEntity 傳入?yún)?shù)中對應(yīng)屬性
        // 示例代碼:
        $accessToken = new AccessTokenEntity();
        $accessToken->setClient($clientEntity);
        foreach ($scopes as $scope) {
            $accessToken->addScope($scope);
        }
        $accessToken->setUserIdentifier($userIdentifier);

        return $accessToken;
    }

    public function persistNewAccessToken(AccessTokenEntityInterface $accessTokenEntity)
    {
        // 創(chuàng)建新訪問令牌時調(diào)用此方法
        // 可以用于持久化存儲訪問令牌,持久化數(shù)據(jù)庫自行選擇
        // 可以使用參數(shù)中的 AccessTokenEntityInterface 對象,獲得有價值的信息:
        // $accessTokenEntity->getIdentifier(); // 獲得令牌唯一標(biāo)識符
        // $accessTokenEntity->getExpiryDateTime(); // 獲得令牌過期時間
        // $accessTokenEntity->getUserIdentifier(); // 獲得用戶標(biāo)識符
        // $accessTokenEntity->getScopes(); // 獲得權(quán)限范圍
        // $accessTokenEntity->getClient()->getIdentifier(); // 獲得客戶端標(biāo)識符
    }

    public function revokeAccessToken($tokenId)
    {
        // 使用刷新令牌創(chuàng)建新的訪問令牌時調(diào)用此方法
        // 參數(shù)為原訪問令牌的唯一標(biāo)識符
        // 可將其在持久化存儲中過期
    }

    public function isAccessTokenRevoked($tokenId)
    {
        // 資源服務(wù)器驗(yàn)證訪問令牌時將調(diào)用此方法
        // 用于驗(yàn)證訪問令牌是否已被刪除
        // return true 已刪除,false 未刪除
        return false;
    }
}
class AuthCodeRepository implements AuthCodeRepositoryInterface
{
    /**
     * @return AuthCodeEntityInterface
     */
    public function getNewAuthCode()
    {
        // 創(chuàng)建新授權(quán)碼時調(diào)用方法
        // 需要返回 AuthCodeEntityInterface 對象
        return new AuthCodeEntity();
    }

    public function persistNewAuthCode(AuthCodeEntityInterface $authCodeEntity)
    {
        // 創(chuàng)建新授權(quán)碼時調(diào)用此方法
        // 可以用于持久化存儲授權(quán)碼,持久化數(shù)據(jù)庫自行選擇
        // 可以使用參數(shù)中的 AuthCodeEntityInterface 對象,獲得有價值的信息:
        // $authCodeEntity->getIdentifier(); // 獲得授權(quán)碼唯一標(biāo)識符
        // $authCodeEntity->getExpiryDateTime(); // 獲得授權(quán)碼過期時間
        // $authCodeEntity->getUserIdentifier(); // 獲得用戶標(biāo)識符
        // $authCodeEntity->getScopes(); // 獲得權(quán)限范圍
        // $authCodeEntity->getClient()->getIdentifier(); // 獲得客戶端標(biāo)識符
    }

    public function revokeAuthCode($codeId)
    {
        // 當(dāng)使用授權(quán)碼獲取訪問令牌時調(diào)用此方法
        // 可以在此時將授權(quán)碼從持久化數(shù)據(jù)庫中刪除
        // 參數(shù)為授權(quán)碼唯一標(biāo)識符
    }

    public function isAuthCodeRevoked($codeId)
    {
        // 當(dāng)使用授權(quán)碼獲取訪問令牌時調(diào)用此方法
        // 用于驗(yàn)證授權(quán)碼是否已被刪除
        // return true 已刪除,false 未刪除
        return false;
    }
}
class ClientRepository implements ClientRepositoryInterface
{
    /**
     * @return ClientEntityInterface
     */
    public function getClientEntity($clientIdentifier, $grantType = null, $clientSecret = null, $mustValidateSecret = true)
    {
        // 獲取客戶端對象時調(diào)用方法,用于驗(yàn)證客戶端
        // 需要返回 ClientEntityInterface 對象
        // $clientIdentifier 客戶端唯一標(biāo)識符
        // $grantType 代表授權(quán)類型,根據(jù)類型不同,驗(yàn)證方式也不同
        // $clientSecret 代表客戶端密鑰,是客戶端事先在授權(quán)服務(wù)器中注冊時得到的
        // $mustValidateSecret 代表是否需要驗(yàn)證客戶端密鑰
        $client = new ClientEntity();
        $client->setIdentifier($clientIdentifier);

        return $client;
    }
}
class RefreshTokenRepository implements RefreshTokenRepositoryInterface
{
    /**
     * @return RefreshTokenEntityInterface
     */
    public function getNewRefreshToken()
    {
        // 創(chuàng)建新授權(quán)碼時調(diào)用方法
        // 需要返回 RefreshTokenEntityInterface 對象
        return new RefreshTokenEntity();
    }

    public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity)
    {
        // 創(chuàng)建新刷新令牌時調(diào)用此方法
        // 用于持久化存儲授刷新令牌
        // 可以使用參數(shù)中的 RefreshTokenEntityInterface 對象,獲得有價值的信息:
        // $refreshTokenEntity->getIdentifier(); // 獲得刷新令牌唯一標(biāo)識符
        // $refreshTokenEntity->getExpiryDateTime(); // 獲得刷新令牌過期時間
        // $refreshTokenEntity->getAccessToken()->getIdentifier(); // 獲得訪問令牌標(biāo)識符
    }

    public function revokeRefreshToken($tokenId)
    {
        // 當(dāng)使用刷新令牌獲取訪問令牌時調(diào)用此方法
        // 原刷新令牌將刪除,創(chuàng)建新的刷新令牌
        // 參數(shù)為原刷新令牌唯一標(biāo)識
        // 可在此刪除原刷新令牌
    }

    public function isRefreshTokenRevoked($tokenId)
    {
        // 當(dāng)使用刷新令牌獲取訪問令牌時調(diào)用此方法
        // 用于驗(yàn)證刷新令牌是否已被刪除
        // return true 已刪除,false 未刪除
        return false;
    }
}
class ScopeRepository implements ScopeRepositoryInterface
{
    /**
     * @return ScopeEntityInterface
     */
    public function getScopeEntityByIdentifier($identifier)
    {
        // 驗(yàn)證權(quán)限是否在權(quán)限范圍中會調(diào)用此方法
        // 參數(shù)為單個權(quán)限標(biāo)識符
        // ......
        // 驗(yàn)證成功則返回 ScopeEntityInterface 對象
        $scope = new ScopeEntity();
        $scope->setIdentifier($identifier);

        return $scope;
    }

    public function finalizeScopes(
        array $scopes,
        $grantType,
        ClientEntityInterface $clientEntity,
        $userIdentifier = null
    ) {
        // 在創(chuàng)建授權(quán)碼與訪問令牌前會調(diào)用此方法
        // 用于驗(yàn)證權(quán)限范圍、授權(quán)類型、客戶端、用戶是否匹配
        // 可整合進(jìn)項目自身的權(quán)限控制中
        // 必須返回 ScopeEntityInterface 對象可用的 scope 數(shù)組
        // 示例:
        // $scope = new ScopeEntity();
        // $scope->setIdentifier('example');
        // $scopes[] = $scope;

        return $scopes;
    }
}
class UserRepository implements UserRepositoryInterface
{
    /**
     * @return UserEntityInterface
     */
    public function getUserEntityByUserCredentials(
        $username,
        $password,
        $grantType,
        ClientEntityInterface $clientEntity
    ) {
        // 驗(yàn)證用戶時調(diào)用此方法
        // 用于驗(yàn)證用戶信息是否符合
        // 可以驗(yàn)證是否為用戶可使用的授權(quán)類型($grantType)與客戶端($clientEntity)
        // 驗(yàn)證成功返回 UserEntityInterface 對象
        $user = new UserEntity();
        $user->setIdentifier(1);

        return $user;
    }
}

Entities

Entities 里是 oauth2-server 處理授權(quán)與認(rèn)證邏輯的類,它為我們提供了 Interfaces 來定義需要實(shí)現(xiàn)的方法,同時提供了 Traits 幫助我們實(shí)現(xiàn),可以選擇使用,有需要時也可以重寫。

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 實(shí)現(xiàn)這個方法,需要自行實(shí)現(xiàn)
    // oauth2-server 項目的測試代碼的實(shí)現(xiàn)例子
    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 預(yù)設(shè)了一些事件,目前官方文檔中只有兩個,余下的可以在 RequestEvent.php 文件中查看。

client.authentication.failed
$server->getEmitter()->addListener(
    'client.authentication.failed',
    function (\League\OAuth2\Server\RequestEvent $event) {
        // do something
    }
);

客戶端身份驗(yàn)證未通過時觸發(fā)此事件。你可以在客戶端嘗試 n 次失敗后禁止它一段時間內(nèi)的再次嘗試。

user.authentication.failed
$server->getEmitter()->addListener(
    'user.authentication.failed',
    function (\League\OAuth2\Server\RequestEvent $event) {
        // do something
    }
);

用戶身份驗(yàn)證未通過時觸發(fā)此事件。你可以通過這里提醒用戶重置密碼,或嘗試 n 次后禁止用戶再次嘗試。

參考文章

《oauth2-server 官方文檔》(https://oauth2.thephpleague.com/)

《理解OAuth 2.0》-阮一峰(http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容