跳至內容

Laravel Passport

簡介

Laravel Passport 在幾分鐘內為您的 Laravel 應用程式提供完整的 OAuth2 伺服器實作。Passport 建構於由 Andy Millington 和 Simon Hamp 維護的 League OAuth2 伺服器之上。

exclamation

本文件假設您已經熟悉 OAuth2。如果您對 OAuth2 一無所知,請在繼續之前先熟悉 OAuth2 的一般術語和功能。

Passport 或 Sanctum?

在開始之前,您可能需要確定您的應用程式使用 Laravel Passport 或 Laravel Sanctum 會更好。如果您的應用程式絕對需要支援 OAuth2,則應使用 Laravel Passport。

但是,如果您嘗試驗證單頁應用程式、行動應用程式或發行 API 令牌,則應使用 Laravel Sanctum。Laravel Sanctum 不支援 OAuth2;但是,它提供了更簡單的 API 身份驗證開發體驗。

安裝

您可以透過 install:api Artisan 命令安裝 Laravel Passport

php artisan install:api --passport

此命令將發布並執行建立應用程式儲存 OAuth2 客戶端和存取令牌所需的資料庫遷移。該命令還將建立生成安全存取令牌所需的加密金鑰。

此外,此命令會詢問您是否要使用 UUID 作為 Passport Client 模型的主鍵值,而不是自動遞增的整數。

執行 install:api 命令後,將 Laravel\Passport\HasApiTokens 特性新增至您的 App\Models\User 模型。此特性將為您的模型提供一些輔助方法,讓您可以檢查已驗證使用者的令牌和範圍

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Passport\HasApiTokens;
 
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
}

最後,在您的應用程式的 config/auth.php 設定檔中,您應該定義一個 api 身份驗證守衛,並將 driver 選項設定為 passport。這將指示您的應用程式在驗證傳入的 API 請求時使用 Passport 的 TokenGuard

'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
 
'api' => [
'driver' => 'passport',
'provider' => 'users',
],
],

部署 Passport

首次將 Passport 部署到應用程式的伺服器時,您可能需要執行 passport:keys 命令。此命令會產生 Passport 生成存取令牌所需的加密金鑰。產生的金鑰通常不會保留在原始碼控制中

php artisan passport:keys

如有必要,您可以定義應從何處載入 Passport 金鑰的路徑。您可以使用 Passport::loadKeysFrom 方法來完成此操作。通常,應從應用程式的 App\Providers\AppServiceProvider 類別的 boot 方法中呼叫此方法

/**
* Bootstrap any application services.
*/
public function boot(): void
{
Passport::loadKeysFrom(__DIR__.'/../secrets/oauth');
}

從環境載入金鑰

或者,您可以使用 vendor:publish Artisan 命令發布 Passport 的設定檔

php artisan vendor:publish --tag=passport-config

發布設定檔後,您可以將應用程式的加密金鑰定義為環境變數來載入

PASSPORT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
<private key here>
-----END RSA PRIVATE KEY-----"
 
PASSPORT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
<public key here>
-----END PUBLIC KEY-----"

升級 Passport

升級到新的 Passport 主要版本時,請務必仔細閱讀升級指南

設定

客戶端密碼雜湊

如果您希望在將客戶端的密碼儲存在資料庫中時進行雜湊處理,則應在 App\Providers\AppServiceProvider 類別的 boot 方法中呼叫 Passport::hashClientSecrets 方法

use Laravel\Passport\Passport;
 
Passport::hashClientSecrets();

啟用後,您的所有客戶端密碼只會在建立後立即顯示給使用者。由於純文字客戶端密碼值永遠不會儲存在資料庫中,因此如果遺失了密碼值,則無法恢復該密碼的值。

令牌生命週期

預設情況下,Passport 會發行長期存取令牌,這些令牌會在一年後過期。如果您想要設定更長 / 更短的令牌生命週期,可以使用 tokensExpireInrefreshTokensExpireInpersonalAccessTokensExpireIn 方法。這些方法應從應用程式的 App\Providers\AppServiceProvider 類別的 boot 方法中呼叫

/**
* Bootstrap any application services.
*/
public function boot(): void
{
Passport::tokensExpireIn(now()->addDays(15));
Passport::refreshTokensExpireIn(now()->addDays(30));
Passport::personalAccessTokensExpireIn(now()->addMonths(6));
}
exclamation

Passport 資料庫表格中的 expires_at 資料行是唯讀的,僅用於顯示目的。在發行令牌時,Passport 會將過期資訊儲存在已簽署和加密的令牌中。如果您需要使令牌失效,則應撤銷它

覆寫預設模型

您可以自由擴展 Passport 內部使用的模型,方法是定義您自己的模型並擴展對應的 Passport 模型

use Laravel\Passport\Client as PassportClient;
 
class Client extends PassportClient
{
// ...
}

定義模型後,您可以透過 Laravel\Passport\Passport 類別指示 Passport 使用您的自訂模型。通常,您應該在應用程式的 App\Providers\AppServiceProvider 類別的 boot 方法中告知 Passport 您的自訂模型

use App\Models\Passport\AuthCode;
use App\Models\Passport\Client;
use App\Models\Passport\PersonalAccessClient;
use App\Models\Passport\RefreshToken;
use App\Models\Passport\Token;
 
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Passport::useTokenModel(Token::class);
Passport::useRefreshTokenModel(RefreshToken::class);
Passport::useAuthCodeModel(AuthCode::class);
Passport::useClientModel(Client::class);
Passport::usePersonalAccessClientModel(PersonalAccessClient::class);
}

覆寫路由

有時您可能希望自訂 Passport 定義的路由。為此,您首先需要在應用程式的 AppServiceProviderregister 方法中新增 Passport::ignoreRoutes 來忽略 Passport 註冊的路由

use Laravel\Passport\Passport;
 
/**
* Register any application services.
*/
public function register(): void
{
Passport::ignoreRoutes();
}

然後,您可以將 Passport 在 其路由檔案 中定義的路由複製到應用程式的 routes/web.php 檔案中,並根據您的喜好修改它們

Route::group([
'as' => 'passport.',
'prefix' => config('passport.path', 'oauth'),
'namespace' => '\Laravel\Passport\Http\Controllers',
], function () {
// Passport routes...
});

發行存取令牌

透過授權碼使用 OAuth2 是大多數開發人員熟悉 OAuth2 的方式。使用授權碼時,用戶端應用程式會將使用者重新導向至您的伺服器,他們將在該伺服器上核准或拒絕向用戶端發行存取令牌的請求。

管理客戶端

首先,需要與您的應用程式 API 互動的應用程式開發人員需要透過建立「客戶端」在您的應用程式中註冊他們的應用程式。通常,這包括提供他們應用程式的名稱和一個 URL,在使用者核准他們的授權請求後,您的應用程式可以重新導向至該 URL。

passport:client 命令

建立客戶端最簡單的方法是使用 passport:client Artisan 命令。此命令可用於建立您自己的客戶端來測試 OAuth2 功能。當您執行 client 命令時,Passport 會提示您有關客戶端的更多資訊,並為您提供客戶端 ID 和密碼

php artisan passport:client

重新導向 URL

如果您希望允許您的客戶端使用多個重新導向 URL,您可以使用逗號分隔的清單指定它們,在 passport:client 命令提示 URL 時。任何包含逗號的 URL 都應該進行 URL 編碼

http://example.com/callback,http://examplefoo.com/callback

JSON API

由於您的應用程式使用者將無法使用 client 命令,Passport 提供了一個 JSON API,您可以使用它來建立用戶端。這省去了您必須手動編寫控制器來建立、更新和刪除用戶端的麻煩。

然而,您需要將 Passport 的 JSON API 與您自己的前端配對,以為您的使用者提供一個儀表板來管理他們的用戶端。以下,我們將檢視所有用於管理用戶端的 API 端點。為了方便起見,我們將使用 Axios 來示範如何向端點發出 HTTP 請求。

JSON API 受 webauth 中介軟體的保護;因此,它只能從您自己的應用程式中呼叫。它無法從外部來源呼叫。

GET /oauth/clients

此路由會傳回已驗證使用者的所有用戶端。這主要用於列出使用者所有的用戶端,以便他們可以編輯或刪除這些用戶端。

axios.get('/oauth/clients')
.then(response => {
console.log(response.data);
});

POST /oauth/clients

此路由用於建立新的用戶端。它需要兩項資料:用戶端的 name 和一個 redirect URL。redirect URL 是使用者在批准或拒絕授權請求後將被重新導向的位置。

建立用戶端時,將會發出一個用戶端 ID 和用戶端密鑰。這些值將在從您的應用程式請求存取令牌時使用。用戶端建立路由將傳回新的用戶端實例。

const data = {
name: 'Client Name',
redirect: 'http://example.com/callback'
};
 
axios.post('/oauth/clients', data)
.then(response => {
console.log(response.data);
})
.catch (response => {
// List errors on response...
});

PUT /oauth/clients/{client-id}

此路由用於更新用戶端。它需要兩項資料:用戶端的 name 和一個 redirect URL。redirect URL 是使用者在批准或拒絕授權請求後將被重新導向的位置。此路由將傳回更新後的用戶端實例。

const data = {
name: 'New Client Name',
redirect: 'http://example.com/callback'
};
 
axios.put('/oauth/clients/' + clientId, data)
.then(response => {
console.log(response.data);
})
.catch (response => {
// List errors on response...
});

DELETE /oauth/clients/{client-id}

此路由用於刪除用戶端。

axios.delete('/oauth/clients/' + clientId)
.then(response => {
// ...
});

請求令牌

重新導向以進行授權

建立用戶端後,開發人員可以使用其用戶端 ID 和密鑰從您的應用程式請求授權碼和存取令牌。首先,使用應用程式應向您應用程式的 /oauth/authorize 路由發出重新導向請求,如下所示:

use Illuminate\Http\Request;
use Illuminate\Support\Str;
 
Route::get('/redirect', function (Request $request) {
$request->session()->put('state', $state = Str::random(40));
 
$query = http_build_query([
'client_id' => 'client-id',
'redirect_uri' => 'http://third-party-app.com/callback',
'response_type' => 'code',
'scope' => '',
'state' => $state,
// 'prompt' => '', // "none", "consent", or "login"
]);
 
return redirect('http://passport-app.test/oauth/authorize?'.$query);
});

prompt 參數可用於指定 Passport 應用程式的驗證行為。

如果 prompt 的值為 none,如果使用者尚未通過 Passport 應用程式驗證,Passport 將始終拋出驗證錯誤。如果值為 consent,即使先前已將所有範圍授與使用應用程式,Passport 也將始終顯示授權批准畫面。當值為 login 時,即使使用者已經有現有的工作階段,Passport 應用程式也會始終提示使用者重新登入應用程式。

如果未提供 prompt 值,則只有在使用者先前未授權使用應用程式存取所請求的範圍時,才會提示使用者進行授權。

lightbulb

請記住,/oauth/authorize 路由已由 Passport 定義。您無需手動定義此路由。

批准請求

當接收授權請求時,Passport 將根據 prompt 參數的值(如果存在)自動回應,並可能會向使用者顯示一個範本,允許他們批准或拒絕授權請求。如果他們批准請求,他們將被重新導向回使用應用程式指定的 redirect_uriredirect_uri 必須與建立用戶端時指定的 redirect URL 相符。

如果您想自訂授權批准畫面,您可以使用 vendor:publish Artisan 命令發佈 Passport 的檢視畫面。發佈的檢視畫面將放置在 resources/views/vendor/passport 目錄中。

php artisan vendor:publish --tag=passport-views

有時您可能希望跳過授權提示,例如在授權第一方用戶端時。您可以透過擴充 Client 模型並定義 skipsAuthorization 方法來實現這一點。如果 skipsAuthorization 傳回 true,則將批准用戶端,並且使用者將立即重新導向回 redirect_uri,除非使用應用程式在重新導向進行授權時已明確設定 prompt 參數。

<?php
 
namespace App\Models\Passport;
 
use Laravel\Passport\Client as BaseClient;
 
class Client extends BaseClient
{
/**
* Determine if the client should skip the authorization prompt.
*/
public function skipsAuthorization(): bool
{
return $this->firstParty();
}
}

將授權碼轉換為存取令牌

如果使用者批准授權請求,他們將被重新導向回使用應用程式。消費者應首先針對重新導向前儲存的值驗證 state 參數。如果狀態參數相符,則消費者應向您的應用程式發出 POST 請求以請求存取令牌。該請求應包含您的應用程式在使用者批准授權請求時發出的授權碼。

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
 
Route::get('/callback', function (Request $request) {
$state = $request->session()->pull('state');
 
throw_unless(
strlen($state) > 0 && $state === $request->state,
InvalidArgumentException::class,
'Invalid state value.'
);
 
$response = Http::asForm()->post('http://passport-app.test/oauth/token', [
'grant_type' => 'authorization_code',
'client_id' => 'client-id',
'client_secret' => 'client-secret',
'redirect_uri' => 'http://third-party-app.com/callback',
'code' => $request->code,
]);
 
return $response->json();
});

/oauth/token 路由將傳回一個 JSON 回應,其中包含 access_tokenrefresh_tokenexpires_in 屬性。expires_in 屬性包含存取令牌過期前的秒數。

lightbulb

/oauth/authorize 路由一樣,/oauth/token 路由是由 Passport 為您定義的。無需手動定義此路由。

JSON API

Passport 還包括一個用於管理已授權存取令牌的 JSON API。您可以將其與您自己的前端配對,以為您的使用者提供一個儀表板來管理存取令牌。為了方便起見,我們將使用 Axios 來示範如何向端點發出 HTTP 請求。JSON API 受 webauth 中介軟體的保護;因此,它只能從您自己的應用程式中呼叫。

GET /oauth/tokens

此路由傳回已驗證使用者建立的所有已授權存取令牌。這主要用於列出使用者所有的令牌,以便他們可以撤銷這些令牌。

axios.get('/oauth/tokens')
.then(response => {
console.log(response.data);
});

DELETE /oauth/tokens/{token-id}

此路由可用於撤銷已授權的存取令牌及其相關的更新令牌。

axios.delete('/oauth/tokens/' + tokenId);

重新整理令牌

如果您的應用程式發出有效期短的存取令牌,使用者將需要透過發出存取令牌時提供給他們的更新令牌來更新他們的存取令牌。

use Illuminate\Support\Facades\Http;
 
$response = Http::asForm()->post('http://passport-app.test/oauth/token', [
'grant_type' => 'refresh_token',
'refresh_token' => 'the-refresh-token',
'client_id' => 'client-id',
'client_secret' => 'client-secret',
'scope' => '',
]);
 
return $response->json();

/oauth/token 路由將傳回一個 JSON 回應,其中包含 access_tokenrefresh_tokenexpires_in 屬性。expires_in 屬性包含存取令牌過期前的秒數。

撤銷令牌

您可以使用 Laravel\Passport\TokenRepository 上的 revokeAccessToken 方法來撤銷令牌。您可以使用 Laravel\Passport\RefreshTokenRepository 上的 revokeRefreshTokensByAccessTokenId 方法來撤銷令牌的更新令牌。可以使用 Laravel 的 服務容器來解析這些類別。

use Laravel\Passport\TokenRepository;
use Laravel\Passport\RefreshTokenRepository;
 
$tokenRepository = app(TokenRepository::class);
$refreshTokenRepository = app(RefreshTokenRepository::class);
 
// Revoke an access token...
$tokenRepository->revokeAccessToken($tokenId);
 
// Revoke all of the token's refresh tokens...
$refreshTokenRepository->revokeRefreshTokensByAccessTokenId($tokenId);

清除令牌

當令牌被撤銷或過期時,您可能需要將其從資料庫中清除。Passport 包含的 passport:purge Artisan 命令可以為您執行此操作。

# Purge revoked and expired tokens and auth codes...
php artisan passport:purge
 
# Only purge tokens expired for more than 6 hours...
php artisan passport:purge --hours=6
 
# Only purge revoked tokens and auth codes...
php artisan passport:purge --revoked
 
# Only purge expired tokens and auth codes...
php artisan passport:purge --expired

您也可以在應用程式的 routes/console.php 檔案中設定排程任務,以便在排程中自動修剪您的令牌。

use Illuminate\Support\Facades\Schedule;
 
Schedule::command('passport:purge')->hourly();

帶 PKCE 的授權碼授權

帶有「授權碼交換證明金鑰 (Proof Key for Code Exchange, PKCE)」的授權碼授權是驗證單頁應用程式或原生應用程式以存取您的 API 的安全方式。當您無法保證用戶端密鑰將被機密儲存,或者為了減輕授權碼被攻擊者攔截的風險時,應使用此授權。當交換授權碼以獲取存取令牌時,「程式碼驗證器」和「程式碼挑戰」的組合會取代用戶端密鑰。

建立客戶端

在您的應用程式可以透過帶有 PKCE 的授權碼授權發出令牌之前,您需要建立一個啟用 PKCE 的用戶端。您可以使用帶有 --public 選項的 passport:client Artisan 命令來執行此操作。

php artisan passport:client --public

請求令牌

程式碼驗證器和程式碼挑戰

由於此授權未提供用戶端密鑰,因此開發人員需要產生程式碼驗證器和程式碼挑戰的組合才能請求令牌。

程式碼驗證器應該是一個介於 43 到 128 個字元的隨機字串,其中包含字母、數字以及 "-"".""_""~" 字元,如 RFC 7636 規範中所定義。

程式碼挑戰應為帶有 URL 和檔案名稱安全字元的 Base64 編碼字串。應移除尾部的 '=' 字元,並且不應存在換行符、空白字元或其他附加字元。

$encoded = base64_encode(hash('sha256', $code_verifier, true));
 
$codeChallenge = strtr(rtrim($encoded, '='), '+/', '-_');

重新導向以進行授權

建立用戶端後,您可以使用用戶端 ID 和產生的程式碼驗證器和程式碼挑戰來從您的應用程式請求授權碼和存取令牌。首先,使用應用程式應向您應用程式的 /oauth/authorize 路由發出重新導向請求。

use Illuminate\Http\Request;
use Illuminate\Support\Str;
 
Route::get('/redirect', function (Request $request) {
$request->session()->put('state', $state = Str::random(40));
 
$request->session()->put(
'code_verifier', $code_verifier = Str::random(128)
);
 
$codeChallenge = strtr(rtrim(
base64_encode(hash('sha256', $code_verifier, true))
, '='), '+/', '-_');
 
$query = http_build_query([
'client_id' => 'client-id',
'redirect_uri' => 'http://third-party-app.com/callback',
'response_type' => 'code',
'scope' => '',
'state' => $state,
'code_challenge' => $codeChallenge,
'code_challenge_method' => 'S256',
// 'prompt' => '', // "none", "consent", or "login"
]);
 
return redirect('http://passport-app.test/oauth/authorize?'.$query);
});

將授權碼轉換為存取令牌

如果使用者批准授權請求,他們將被重新導向回使用應用程式。與標準授權碼授權中一樣,消費者應針對重新導向前儲存的值驗證 state 參數。

如果狀態參數相符,則消費者應向您的應用程式發出 POST 請求以請求存取令牌。該請求應包含您的應用程式在使用者批准授權請求時發出的授權碼,以及原始產生的程式碼驗證器。

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
 
Route::get('/callback', function (Request $request) {
$state = $request->session()->pull('state');
 
$codeVerifier = $request->session()->pull('code_verifier');
 
throw_unless(
strlen($state) > 0 && $state === $request->state,
InvalidArgumentException::class
);
 
$response = Http::asForm()->post('http://passport-app.test/oauth/token', [
'grant_type' => 'authorization_code',
'client_id' => 'client-id',
'redirect_uri' => 'http://third-party-app.com/callback',
'code_verifier' => $codeVerifier,
'code' => $request->code,
]);
 
return $response->json();
});

密碼授權令牌

exclamation

我們不再建議使用密碼授權令牌。相反,您應該選擇 OAuth2 伺服器目前建議的授權類型

OAuth2 密碼授權允許您的其他第一方用戶端(例如行動應用程式)使用電子郵件地址/使用者名稱和密碼來取得存取令牌。這允許您安全地向您的第一方用戶端發出存取令牌,而無需您的使用者經過整個 OAuth2 授權碼重新導向流程。

要啟用密碼授權,請在您的應用程式的 App\Providers\AppServiceProvider 類別的 boot 方法中呼叫 enablePasswordGrant 方法。

/**
* Bootstrap any application services.
*/
public function boot(): void
{
Passport::enablePasswordGrant();
}

建立密碼授權客戶端

在您的應用程式可以透過密碼授權發出令牌之前,您需要建立一個密碼授權用戶端。您可以使用帶有 --password 選項的 passport:client Artisan 命令來執行此操作。如果您已經執行了 passport:install 命令,則無需執行此命令:

php artisan passport:client --password

請求令牌

建立密碼授權用戶端後,您可以透過向 /oauth/token 路由發出 POST 請求,並提供使用者的電子郵件地址和密碼來請求存取令牌。請記住,此路由已由 Passport 註冊,因此無需手動定義它。如果請求成功,您將在伺服器傳回的 JSON 回應中收到 access_tokenrefresh_token

use Illuminate\Support\Facades\Http;
 
$response = Http::asForm()->post('http://passport-app.test/oauth/token', [
'grant_type' => 'password',
'client_id' => 'client-id',
'client_secret' => 'client-secret',
'username' => '[email protected]',
'password' => 'my-password',
'scope' => '',
]);
 
return $response->json();
lightbulb

請記住,預設情況下,存取令牌的有效期較長。但是,如果需要,您可以隨時設定最大存取令牌存留時間

請求所有範圍

使用密碼授權或用戶端憑證授權時,您可能希望授權令牌具有您的應用程式支援的所有範圍。您可以透過請求 * 範圍來執行此操作。如果您請求 * 範圍,則令牌實例上的 can 方法將始終傳回 true。此範圍只能指派給使用 passwordclient_credentials 授權發出的令牌。

use Illuminate\Support\Facades\Http;
 
$response = Http::asForm()->post('http://passport-app.test/oauth/token', [
'grant_type' => 'password',
'client_id' => 'client-id',
'client_secret' => 'client-secret',
'username' => '[email protected]',
'password' => 'my-password',
'scope' => '*',
]);

自訂使用者提供者

如果您的應用程式使用多個 驗證使用者提供者,您可以在透過 artisan passport:client --password 命令建立用戶端時,提供 --provider 選項來指定密碼授權用戶端使用的使用者提供者。給定的提供者名稱應與您的應用程式的 config/auth.php 設定檔中定義的有效提供者相符。然後,您可以使用中介軟體保護您的路由,以確保只有來自該保護所指定提供者的使用者才能獲得授權。

自訂使用者名稱欄位

當使用密碼授權進行身份驗證時,Passport 會使用您可驗證模型的 email 屬性作為「使用者名稱」。不過,您可以在您的模型上定義 findForPassport 方法來自訂此行為。

<?php
 
namespace App\Models;
 
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Passport\HasApiTokens;
 
class User extends Authenticatable
{
use HasApiTokens, Notifiable;
 
/**
* Find the user instance for the given username.
*/
public function findForPassport(string $username): User
{
return $this->where('username', $username)->first();
}
}

自訂密碼驗證

當使用密碼授權進行身份驗證時,Passport 會使用您模型的 password 屬性來驗證給定的密碼。如果您的模型沒有 password 屬性,或您希望自訂密碼驗證邏輯,您可以在您的模型上定義 validateForPassportPasswordGrant 方法。

<?php
 
namespace App\Models;
 
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Hash;
use Laravel\Passport\HasApiTokens;
 
class User extends Authenticatable
{
use HasApiTokens, Notifiable;
 
/**
* Validate the password of the user for the Passport password grant.
*/
public function validateForPassportPasswordGrant(string $password): bool
{
return Hash::check($password, $this->password);
}
}

隱式授權令牌

exclamation

我們不再建議使用隱式授權令牌。相反地,您應該選擇目前 OAuth2 Server 建議的授權類型

隱式授權與授權碼授權類似;但是,令牌會直接返回給客戶端,而不需要交換授權碼。此授權最常用於 JavaScript 或行動應用程式,在這些應用程式中,客戶端憑證無法安全地儲存。若要啟用此授權,請在您應用程式的 App\Providers\AppServiceProvider 類的 boot 方法中呼叫 enableImplicitGrant 方法。

/**
* Bootstrap any application services.
*/
public function boot(): void
{
Passport::enableImplicitGrant();
}

一旦授權啟用後,開發人員可以使用他們的客戶端 ID,向您的應用程式請求存取令牌。消費應用程式應該像這樣,對您應用程式的 /oauth/authorize 路由發出重新導向請求。

use Illuminate\Http\Request;
 
Route::get('/redirect', function (Request $request) {
$request->session()->put('state', $state = Str::random(40));
 
$query = http_build_query([
'client_id' => 'client-id',
'redirect_uri' => 'http://third-party-app.com/callback',
'response_type' => 'token',
'scope' => '',
'state' => $state,
// 'prompt' => '', // "none", "consent", or "login"
]);
 
return redirect('http://passport-app.test/oauth/authorize?'.$query);
});
lightbulb

請記住,/oauth/authorize 路由已由 Passport 定義。您無需手動定義此路由。

客戶端憑證授權令牌

客戶端憑證授權適用於機器對機器身份驗證。例如,您可能會在排程工作中,使用此授權透過 API 執行維護任務。

在您的應用程式可以透過客戶端憑證授權發行令牌之前,您需要建立一個客戶端憑證授權客戶端。您可以使用 passport:client Artisan 命令的 --client 選項來完成此操作。

php artisan passport:client --client

接下來,若要使用此授權類型,請為 CheckClientCredentials 中介軟體註冊一個中介軟體別名。您可以在您的應用程式的 bootstrap/app.php 檔案中定義中介軟體別名。

use Laravel\Passport\Http\Middleware\CheckClientCredentials;
 
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'client' => CheckClientCredentials::class
]);
})

然後,將中介軟體附加到路由。

Route::get('/orders', function (Request $request) {
...
})->middleware('client');

若要將路由的存取權限限制在特定範圍,您可以在將 client 中介軟體附加到路由時,提供以逗號分隔的所需範圍列表。

Route::get('/orders', function (Request $request) {
...
})->middleware('client:check-status,your-scope');

檢索令牌

若要使用此授權類型檢索令牌,請向 oauth/token 端點發出請求。

use Illuminate\Support\Facades\Http;
 
$response = Http::asForm()->post('http://passport-app.test/oauth/token', [
'grant_type' => 'client_credentials',
'client_id' => 'client-id',
'client_secret' => 'client-secret',
'scope' => 'your-scope',
]);
 
return $response->json()['access_token'];

個人存取令牌

有時候,您的使用者可能希望在不經過典型的授權碼重新導向流程的情況下,向他們自己發行存取令牌。允許使用者透過您應用程式的 UI 向他們自己發行令牌,對於讓使用者實驗您的 API 可能很有用,或者可以作為一般發行存取令牌的更簡單方法。

lightbulb

如果您的應用程式主要使用 Passport 發行個人存取令牌,請考慮使用 Laravel Sanctum,這是 Laravel 輕量級的第一方程式庫,用於發行 API 存取令牌。

建立個人存取客戶端

在您的應用程式可以發行個人存取令牌之前,您需要建立一個個人存取客戶端。您可以執行帶有 --personal 選項的 passport:client Artisan 命令來完成此操作。如果您已經執行了 passport:install 命令,則無需執行此命令。

php artisan passport:client --personal

建立個人存取客戶端後,請將客戶端的 ID 和純文字密碼值放入您應用程式的 .env 檔案中。

PASSPORT_PERSONAL_ACCESS_CLIENT_ID="client-id-value"
PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET="unhashed-client-secret-value"

管理個人存取令牌

建立個人存取客戶端後,您可以使用 App\Models\User 模型實例上的 createToken 方法,為給定使用者發行令牌。createToken 方法接受令牌的名稱作為其第一個引數,並接受可選的 範圍陣列作為其第二個引數。

use App\Models\User;
 
$user = User::find(1);
 
// Creating a token without scopes...
$token = $user->createToken('Token Name')->accessToken;
 
// Creating a token with scopes...
$token = $user->createToken('My Token', ['place-orders'])->accessToken;

JSON API

Passport 還包含一個 JSON API,用於管理個人存取令牌。您可以將其與您自己的前端配對,為您的使用者提供一個儀表板來管理個人存取令牌。以下,我們將檢閱所有用於管理個人存取令牌的 API 端點。為了方便起見,我們將使用 Axios 來示範對端點發出 HTTP 請求。

JSON API 受 webauth 中介軟體的保護;因此,它只能從您自己的應用程式中呼叫。它無法從外部來源呼叫。

GET /oauth/scopes

此路由會傳回為您的應用程式定義的所有範圍。您可以使用此路由來列出使用者可以指派給個人存取令牌的範圍。

axios.get('/oauth/scopes')
.then(response => {
console.log(response.data);
});

GET /oauth/personal-access-tokens

此路由會傳回已通過身份驗證的使用者建立的所有個人存取令牌。這主要用於列出所有使用者的令牌,以便他們可以編輯或撤銷它們。

axios.get('/oauth/personal-access-tokens')
.then(response => {
console.log(response.data);
});

POST /oauth/personal-access-tokens

此路由會建立新的個人存取令牌。它需要兩項資料:令牌的 name 和應該指派給令牌的 scopes

const data = {
name: 'Token Name',
scopes: []
};
 
axios.post('/oauth/personal-access-tokens', data)
.then(response => {
console.log(response.data.accessToken);
})
.catch (response => {
// List errors on response...
});

DELETE /oauth/personal-access-tokens/{token-id}

此路由可用於撤銷個人存取令牌。

axios.delete('/oauth/personal-access-tokens/' + tokenId);

保護路由

透過中介層

Passport 包含一個身份驗證守衛,它將驗證傳入請求上的存取令牌。一旦您將 api 守衛配置為使用 passport 驅動程式,您只需要在任何應該需要有效存取令牌的路由上指定 auth:api 中介軟體。

Route::get('/user', function () {
// ...
})->middleware('auth:api');
exclamation

如果您使用客戶端憑證授權,您應該使用client 中介軟體來保護您的路由,而不是 auth:api 中介軟體。

多個身份驗證守衛

如果您的應用程式驗證不同類型的使用者,他們可能使用完全不同的 Eloquent 模型,您可能需要為應用程式中的每個使用者提供者類型定義一個守衛配置。這允許您保護針對特定使用者提供者的請求。例如,給定以下 config/auth.php 配置檔案中的守衛配置:

'api' => [
'driver' => 'passport',
'provider' => 'users',
],
 
'api-customers' => [
'driver' => 'passport',
'provider' => 'customers',
],

以下路由將使用 api-customers 守衛,該守衛使用 customers 使用者提供者來驗證傳入的請求。

Route::get('/customer', function () {
// ...
})->middleware('auth:api-customers');
lightbulb

有關將多個使用者提供者與 Passport 一起使用的更多資訊,請參閱密碼授權文件

傳遞存取令牌

當呼叫受 Passport 保護的路由時,您的應用程式的 API 消費者應該在其請求的 Authorization 標頭中,將其存取令牌指定為 Bearer 令牌。例如,當使用 Guzzle HTTP 程式庫時:

use Illuminate\Support\Facades\Http;
 
$response = Http::withHeaders([
'Accept' => 'application/json',
'Authorization' => 'Bearer '.$accessToken,
])->get('https://passport-app.test/api/user');
 
return $response->json();

令牌範圍

當請求授權存取帳戶時,範圍允許您的 API 客戶端請求一組特定的權限。例如,如果您正在建立一個電子商務應用程式,並非所有 API 消費者都需要能夠下訂單。相反地,您可以允許消費者僅請求授權存取訂單的出貨狀態。換句話說,範圍允許您應用程式的使用者限制第三方應用程式可以代表他們執行的操作。

定義範圍

您可以使用您應用程式的 App\Providers\AppServiceProvider 類的 boot 方法中的 Passport::tokensCan 方法來定義您的 API 範圍。tokensCan 方法接受範圍名稱和範圍描述的陣列。範圍描述可以是您希望的任何內容,並且會顯示在授權核准畫面上。

/**
* Bootstrap any application services.
*/
public function boot(): void
{
Passport::tokensCan([
'place-orders' => 'Place orders',
'check-status' => 'Check order status',
]);
}

預設範圍

如果客戶端沒有請求任何特定範圍,您可以使用 setDefaultScope 方法,將預設範圍附加到令牌來配置您的 Passport 伺服器。通常,您應該從您應用程式的 App\Providers\AppServiceProvider 類的 boot 方法呼叫此方法。

use Laravel\Passport\Passport;
 
Passport::tokensCan([
'place-orders' => 'Place orders',
'check-status' => 'Check order status',
]);
 
Passport::setDefaultScope([
'check-status',
'place-orders',
]);
lightbulb

Passport 的預設範圍不適用於使用者產生的個人存取令牌。

將範圍分配給令牌

當請求授權碼時

當使用授權碼授權請求存取令牌時,消費者應該將他們所需的範圍指定為 scope 查詢字串參數。scope 參數應該是以空格分隔的範圍列表。

Route::get('/redirect', function () {
$query = http_build_query([
'client_id' => 'client-id',
'redirect_uri' => 'http://example.com/callback',
'response_type' => 'code',
'scope' => 'place-orders check-status',
]);
 
return redirect('http://passport-app.test/oauth/authorize?'.$query);
});

當發行個人存取令牌時

如果您使用 App\Models\User 模型的 createToken 方法發行個人存取令牌,您可以將所需範圍的陣列作為該方法的第二個引數傳遞。

$token = $user->createToken('My Token', ['place-orders'])->accessToken;

檢查範圍

Passport 包含兩個中介軟體,可用於驗證傳入的請求是否已使用已授與給定範圍的令牌進行身份驗證。若要開始,請在您的應用程式的 bootstrap/app.php 檔案中定義以下中介軟體別名。

use Laravel\Passport\Http\Middleware\CheckForAnyScope;
use Laravel\Passport\Http\Middleware\CheckScopes;
 
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'scopes' => CheckScopes::class,
'scope' => CheckForAnyScope::class,
]);
})

檢查所有範圍

可以將 scopes 中介軟體指派給路由,以驗證傳入請求的存取令牌是否具有所有列出的範圍。

Route::get('/orders', function () {
// Access token has both "check-status" and "place-orders" scopes...
})->middleware(['auth:api', 'scopes:check-status,place-orders']);

檢查任何範圍

可以將 scope 中介軟體指派給路由,以驗證傳入請求的存取令牌是否具有至少一個列出的範圍。

Route::get('/orders', function () {
// Access token has either "check-status" or "place-orders" scope...
})->middleware(['auth:api', 'scope:check-status,place-orders']);

檢查令牌實例上的範圍

一旦已通過身份驗證的存取令牌請求進入您的應用程式,您仍然可以使用已通過身份驗證的 App\Models\User 實例上的 tokenCan 方法,檢查令牌是否具有給定範圍。

use Illuminate\Http\Request;
 
Route::get('/orders', function (Request $request) {
if ($request->user()->tokenCan('place-orders')) {
// ...
}
});

其他範圍方法

scopeIds 方法會傳回所有已定義的 ID / 名稱的陣列。

use Laravel\Passport\Passport;
 
Passport::scopeIds();

scopes 方法會將所有已定義的範圍作為 Laravel\Passport\Scope 的實例傳回。

Passport::scopes();

scopesFor 方法會傳回與給定 ID / 名稱相符的 Laravel\Passport\Scope 實例陣列。

Passport::scopesFor(['place-orders', 'check-status']);

您可以使用 hasScope 方法來判斷是否已定義給定範圍。

Passport::hasScope('place-orders');

使用 JavaScript 取用您的 API

當建立 API 時,能夠從您的 JavaScript 應用程式使用自己的 API 會非常有用。這種 API 開發方法允許您自己的應用程式使用您與全世界共享的相同 API。相同的 API 可以由您的 Web 應用程式、行動應用程式、第三方應用程式以及您可能在各種套件管理員上發佈的任何 SDK 使用。

通常,如果您想從您的 JavaScript 應用程式使用您的 API,您需要手動將存取令牌傳送到應用程式,並在每次請求您的應用程式時傳遞它。但是,Passport 包含一個可以為您處理此問題的中介軟體。您只需要將 CreateFreshApiToken 中介軟體附加到您應用程式的 bootstrap/app.php 檔案中的 web 中介軟體群組即可。

use Laravel\Passport\Http\Middleware\CreateFreshApiToken;
 
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
CreateFreshApiToken::class,
]);
})
exclamation

您應該確保 CreateFreshApiToken 中介軟體是您的中介軟體堆疊中列出的最後一個中介軟體。

此中介軟體會將 laravel_token cookie 附加到您的傳出回應。此 cookie 包含一個加密的 JWT,Passport 將使用它來驗證來自您 JavaScript 應用程式的 API 請求。JWT 的存續期等於您的 session.lifetime 配置值。現在,由於瀏覽器會自動將 cookie 與所有後續請求一起傳送,因此您可以向您應用程式的 API 發出請求,而無需明確傳遞存取令牌。

axios.get('/api/user')
.then(response => {
console.log(response.data);
});

如果需要,您可以使用 Passport::cookie 方法自訂 laravel_token cookie 的名稱。通常,應該從您應用程式的 App\Providers\AppServiceProvider 類的 boot 方法呼叫此方法。

/**
* Bootstrap any application services.
*/
public function boot(): void
{
Passport::cookie('custom_name');
}

CSRF 保護

當使用這種身份驗證方法時,您需要確保在您的請求中包含有效的 CSRF 令牌標頭。預設的 Laravel JavaScript scaffolding 包含一個 Axios 實例,它將自動使用加密的 XSRF-TOKEN cookie 值,在相同來源的請求上傳送 X-XSRF-TOKEN 標頭。

lightbulb

如果您選擇傳送 X-CSRF-TOKEN 標頭而不是 X-XSRF-TOKEN,您將需要使用 csrf_token() 提供的未加密令牌。

事件

當發行存取令牌和重新整理令牌時,Passport 會引發事件。您可以監聽這些事件,以修剪或撤銷資料庫中的其他存取令牌。

事件名稱
Laravel\Passport\Events\AccessTokenCreated
Laravel\Passport\Events\RefreshTokenCreated

測試

Passport 的 actingAs 方法可用於指定目前已通過身份驗證的使用者及其範圍。傳遞給 actingAs 方法的第一個引數是使用者實例,第二個引數是應授與使用者令牌的範圍陣列。

use App\Models\User;
use Laravel\Passport\Passport;
 
test('servers can be created', function () {
Passport::actingAs(
User::factory()->create(),
['create-servers']
);
 
$response = $this->post('/api/create-server');
 
$response->assertStatus(201);
});
use App\Models\User;
use Laravel\Passport\Passport;
 
public function test_servers_can_be_created(): void
{
Passport::actingAs(
User::factory()->create(),
['create-servers']
);
 
$response = $this->post('/api/create-server');
 
$response->assertStatus(201);
}

Passport 的 actingAsClient 方法可用於指定目前已通過身份驗證的客戶端及其範圍。傳遞給 actingAsClient 方法的第一個引數是客戶端實例,第二個引數是應授與客戶端令牌的範圍陣列。

use Laravel\Passport\Client;
use Laravel\Passport\Passport;
 
test('orders can be retrieved', function () {
Passport::actingAsClient(
Client::factory()->create(),
['check-status']
);
 
$response = $this->get('/api/orders');
 
$response->assertStatus(200);
});
use Laravel\Passport\Client;
use Laravel\Passport\Passport;
 
public function test_orders_can_be_retrieved(): void
{
Passport::actingAsClient(
Client::factory()->create(),
['check-status']
);
 
$response = $this->get('/api/orders');
 
$response->assertStatus(200);
}