跳轉至內容

Laravel Passport

簡介

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

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

Passport 還是 Sanctum?

在開始之前,您可能需要確定您的應用程式是更適合使用 Laravel Passport 還是 Laravel Sanctum。如果您的應用程式絕對需要支援 OAuth2,那麼您應該使用 Laravel Passport。

然而,如果您只是嘗試認證單頁應用程式、移動應用程式或簽發 API 令牌,您應該使用 Laravel Sanctum。Laravel Sanctum 不支援 OAuth2;但它提供了更簡單的 API 認證開發體驗。

安裝

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

1php artisan install:api --passport

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

執行 install:api 命令後,將 Laravel\Passport\HasApiTokens trait 和 Laravel\Passport\Contracts\OAuthenticatable 介面新增到您的 App\Models\User 模型中。此 trait 將為您的模型提供一些輔助方法,允許您檢查已認證使用者的令牌和作用域。

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Factories\HasFactory;
6use Illuminate\Foundation\Auth\User as Authenticatable;
7use Illuminate\Notifications\Notifiable;
8use Laravel\Passport\Contracts\OAuthenticatable;
9use Laravel\Passport\HasApiTokens;
10 
11class User extends Authenticatable implements OAuthenticatable
12{
13 use HasApiTokens, HasFactory, Notifiable;
14}

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

1'guards' => [
2 'web' => [
3 'driver' => 'session',
4 'provider' => 'users',
5 ],
6 
7 'api' => [
8 'driver' => 'passport',
9 'provider' => 'users',
10 ],
11],

部署 Passport

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

1php artisan passport:keys

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

1/**
2 * Bootstrap any application services.
3 */
4public function boot(): void
5{
6 Passport::loadKeysFrom(__DIR__.'/../secrets/oauth');
7}

從環境變數載入金鑰

或者,您可以使用 vendor:publish Artisan 命令釋出 Passport 的配置檔案

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

釋出配置檔案後,您可以透過將它們定義為環境變數來載入應用程式的加密金鑰。

1PASSPORT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
2<private key here>
3-----END RSA PRIVATE KEY-----"
4 
5PASSPORT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
6<public key here>
7-----END PUBLIC KEY-----"

升級 Passport

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

配置

令牌有效期

預設情況下,Passport 簽發的訪問令牌有效期為一年。如果您想配置更長或更短的令牌有效期,可以使用 tokensExpireInrefreshTokensExpireInpersonalAccessTokensExpireIn 方法。這些方法應在您應用程式的 App\Providers\AppServiceProvider 類的 boot 方法中呼叫。

1use Carbon\CarbonInterval;
2 
3/**
4 * Bootstrap any application services.
5 */
6public function boot(): void
7{
8 Passport::tokensExpireIn(CarbonInterval::days(15));
9 Passport::refreshTokensExpireIn(CarbonInterval::days(30));
10 Passport::personalAccessTokensExpireIn(CarbonInterval::months(6));
11}

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

覆蓋預設模型

您可以自由地透過定義自己的模型並擴充套件相應的 Passport 模型來擴充套件 Passport 內部使用的模型。

1use Laravel\Passport\Client as PassportClient;
2 
3class Client extends PassportClient
4{
5 // ...
6}

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

1use App\Models\Passport\AuthCode;
2use App\Models\Passport\Client;
3use App\Models\Passport\DeviceCode;
4use App\Models\Passport\RefreshToken;
5use App\Models\Passport\Token;
6use Laravel\Passport\Passport;
7 
8/**
9 * Bootstrap any application services.
10 */
11public function boot(): void
12{
13 Passport::useTokenModel(Token::class);
14 Passport::useRefreshTokenModel(RefreshToken::class);
15 Passport::useAuthCodeModel(AuthCode::class);
16 Passport::useClientModel(Client::class);
17 Passport::useDeviceCodeModel(DeviceCode::class);
18}

覆蓋路由

有時您可能希望自定義 Passport 定義的路由。要實現這一點,您首先需要透過在應用程式的 AppServiceProviderregister 方法中新增 Passport::ignoreRoutes 來忽略 Passport 註冊的路由。

1use Laravel\Passport\Passport;
2 
3/**
4 * Register any application services.
5 */
6public function register(): void
7{
8 Passport::ignoreRoutes();
9}

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

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

授權碼模式 (Authorization Code Grant)

透過授權碼使用 OAuth2 是大多數開發者熟悉 OAuth2 的方式。使用授權碼時,客戶端應用程式會將使用者重定向到您的伺服器,使用者將在那裡批准或拒絕向客戶端簽發訪問令牌的請求。

首先,我們需要指示 Passport 如何返回我們的“授權”檢視。

所有授權檢視的渲染邏輯都可以透過 Laravel\Passport\Passport 類提供的適當方法進行自定義。通常,您應該從應用程式的 App\Providers\AppServiceProvider 類的 boot 方法中呼叫此方法。

1use Inertia\Inertia;
2use Laravel\Passport\Passport;
3 
4/**
5 * Bootstrap any application services.
6 */
7public function boot(): void
8{
9 // By providing a view name...
10 Passport::authorizationView('auth.oauth.authorize');
11 
12 // By providing a closure...
13 Passport::authorizationView(
14 fn ($parameters) => Inertia::render('Auth/OAuth/Authorize', [
15 'request' => $parameters['request'],
16 'authToken' => $parameters['authToken'],
17 'client' => $parameters['client'],
18 'user' => $parameters['user'],
19 'scopes' => $parameters['scopes'],
20 ])
21 );
22}

Passport 會自動定義返回此檢視的 /oauth/authorize 路由。您的 auth.oauth.authorize 模板應包含一個向 passport.authorizations.approve 路由傳送 POST 請求以批准授權的表單,以及一個向 passport.authorizations.deny 路由傳送 DELETE 請求以拒絕授權的表單。passport.authorizations.approvepassport.authorizations.deny 路由期望包含 stateclient_idauth_token 欄位。

管理客戶端

構建需要與您的 API 互動的應用程式的開發者,需要透過建立“客戶端”來向您的應用程式註冊他們的應用。通常,這包括提供他們的應用程式名稱和一個重定向 URI,以便在使用者批准其授權請求後,您的應用程式可以重定向到該 URI。

第一方客戶端

建立客戶端最簡單的方法是使用 passport:client Artisan 命令。此命令可用於建立第一方客戶端或測試您的 OAuth2 功能。當您執行 passport:client 命令時,Passport 將提示您輸入有關客戶端的更多資訊,並將為您提供客戶端 ID 和金鑰。

1php artisan passport:client

如果您想為客戶端允許多個重定向 URI,可以在 passport:client 命令提示輸入 URI 時,使用逗號分隔的列表指定它們。任何包含逗號的 URI 都應進行 URI 編碼。

1https://third-party-app.com/callback,https://example.com/oauth/redirect

第三方客戶端

由於您應用程式的使用者將無法使用 passport:client 命令,您可以使用 Laravel\Passport\ClientRepository 類的 createAuthorizationCodeGrantClient 方法為特定使用者註冊客戶端。

1use App\Models\User;
2use Laravel\Passport\ClientRepository;
3 
4$user = User::find($userId);
5 
6// Creating an OAuth app client that belongs to the given user...
7$client = app(ClientRepository::class)->createAuthorizationCodeGrantClient(
8 user: $user,
9 name: 'Example App',
10 redirectUris: ['https://third-party-app.com/callback'],
11 confidential: false,
12 enableDeviceFlow: true
13);
14 
15// Retrieving all the OAuth app clients that belong to the user...
16$clients = $user->oauthApps()->get();

createAuthorizationCodeGrantClient 方法返回 Laravel\Passport\Client 的例項。您可以向用戶顯示 $client->id 作為客戶端 ID,顯示 $client->plainSecret 作為客戶端金鑰。

請求令牌

重定向進行授權

一旦建立了客戶端,開發者就可以使用他們的客戶端 ID 和金鑰從您的應用程式請求授權碼和訪問令牌。首先,消費應用程式應向您應用程式的 /oauth/authorize 路由發出重定向請求,如下所示:

1use Illuminate\Http\Request;
2use Illuminate\Support\Str;
3 
4Route::get('/redirect', function (Request $request) {
5 $request->session()->put('state', $state = Str::random(40));
6 
7 $query = http_build_query([
8 'client_id' => 'your-client-id',
9 'redirect_uri' => 'https://third-party-app.com/callback',
10 'response_type' => 'code',
11 'scope' => 'user:read orders:create',
12 'state' => $state,
13 // 'prompt' => '', // "none", "consent", or "login"
14 ]);
15 
16 return redirect('https://passport-app.test/oauth/authorize?'.$query);
17});

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

如果 prompt 值為 none,那麼如果使用者未在 Passport 應用程式中認證,Passport 將始終丟擲認證錯誤。如果值為 consent,即使之前已授予消費應用程式所有作用域,Passport 也將始終顯示授權批准介面。當值為 login 時,即使 Passport 應用程式已有現有會話,它也會始終提示使用者重新登入。

如果不提供 prompt 值,則僅當用戶之前未授權消費應用程式訪問所請求的作用域時,才會提示使用者進行授權。

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

批准請求

接收授權請求時,Passport 會自動根據 prompt 引數(如果存在)的值進行響應,並可能向用戶顯示一個允許他們批准或拒絕授權請求的模板。如果他們批准請求,他們將被重定向回消費應用程式指定的 redirect_uriredirect_uri 必須與建立客戶端時指定的 redirect URL 匹配。

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

1<?php
2 
3namespace App\Models\Passport;
4 
5use Illuminate\Contracts\Auth\Authenticatable;
6use Laravel\Passport\Client as BaseClient;
7 
8class Client extends BaseClient
9{
10 /**
11 * Determine if the client should skip the authorization prompt.
12 *
13 * @param \Laravel\Passport\Scope[] $scopes
14 */
15 public function skipsAuthorization(Authenticatable $user, array $scopes): bool
16 {
17 return $this->firstParty();
18 }
19}

將授權碼轉換為訪問令牌

如果使用者批准了授權請求,他們將被重定向回消費應用程式。消費者應首先根據重定向前儲存的值驗證 state 引數。如果 state 引數匹配,那麼消費者應向您的應用程式發出 POST 請求以請求訪問令牌。請求應包含使用者批准授權請求時您的應用程式簽發的授權碼。

1use Illuminate\Http\Request;
2use Illuminate\Support\Facades\Http;
3 
4Route::get('/callback', function (Request $request) {
5 $state = $request->session()->pull('state');
6 
7 throw_unless(
8 strlen($state) > 0 && $state === $request->state,
9 InvalidArgumentException::class,
10 'Invalid state value.'
11 );
12 
13 $response = Http::asForm()->post('https://passport-app.test/oauth/token', [
14 'grant_type' => 'authorization_code',
15 'client_id' => 'your-client-id',
16 'client_secret' => 'your-client-secret',
17 'redirect_uri' => 'https://third-party-app.com/callback',
18 'code' => $request->code,
19 ]);
20 
21 return $response->json();
22});

/oauth/token 路由將返回一個包含 access_tokenrefresh_tokenexpires_in 屬性的 JSON 響應。expires_in 屬性包含訪問令牌過期前的秒數。

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

管理令牌

您可以使用 Laravel\Passport\HasApiTokens trait 的 tokens 方法檢索使用者的授權令牌。例如,這可用於為您的使用者提供一個儀表板,以跟蹤他們與第三方應用程式的連線。

1use App\Models\User;
2use Illuminate\Database\Eloquent\Collection;
3use Illuminate\Support\Facades\Date;
4use Laravel\Passport\Token;
5 
6$user = User::find($userId);
7 
8// Retrieving all of the valid tokens for the user...
9$tokens = $user->tokens()
10 ->where('revoked', false)
11 ->where('expires_at', '>', Date::now())
12 ->get();
13 
14// Retrieving all the user's connections to third-party OAuth app clients...
15$connections = $tokens->load('client')
16 ->reject(fn (Token $token) => $token->client->firstParty())
17 ->groupBy('client_id')
18 ->map(fn (Collection $tokens) => [
19 'client' => $tokens->first()->client,
20 'scopes' => $tokens->pluck('scopes')->flatten()->unique()->values()->all(),
21 'tokens_count' => $tokens->count(),
22 ])
23 ->values();

重新整理令牌

如果您的應用程式簽發短期訪問令牌,使用者將需要透過簽發訪問令牌時提供給他們的重新整理令牌來重新整理他們的訪問令牌。

1use Illuminate\Support\Facades\Http;
2 
3$response = Http::asForm()->post('https://passport-app.test/oauth/token', [
4 'grant_type' => 'refresh_token',
5 'refresh_token' => 'the-refresh-token',
6 'client_id' => 'your-client-id',
7 'client_secret' => 'your-client-secret', // Required for confidential clients only...
8 'scope' => 'user:read orders:create',
9]);
10 
11return $response->json();

/oauth/token 路由將返回一個包含 access_tokenrefresh_tokenexpires_in 屬性的 JSON 響應。expires_in 屬性包含訪問令牌過期前的秒數。

撤銷令牌

您可以使用 Laravel\Passport\Token 模型上的 revoke 方法撤銷令牌。您可以使用 Laravel\Passport\RefreshToken 模型上的 revoke 方法撤銷令牌的重新整理令牌。

1use Laravel\Passport\Passport;
2use Laravel\Passport\Token;
3 
4$token = Passport::token()->find($tokenId);
5 
6// Revoke an access token...
7$token->revoke();
8 
9// Revoke the token's refresh token...
10$token->refreshToken?->revoke();
11 
12// Revoke all of the user's tokens...
13User::find($userId)->tokens()->each(function (Token $token) {
14 $token->revoke();
15 $token->refreshToken?->revoke();
16});

清理令牌

當令牌被撤銷或過期時,您可能希望將它們從資料庫中清除。Passport 隨附的 passport:purge Artisan 命令可以為您完成此操作。

1# Purge revoked and expired tokens, auth codes, and device codes...
2php artisan passport:purge
3 
4# Only purge tokens expired for more than 6 hours...
5php artisan passport:purge --hours=6
6 
7# Only purge revoked tokens, auth codes, and device codes...
8php artisan passport:purge --revoked
9 
10# Only purge expired tokens, auth codes, and device codes...
11php artisan passport:purge --expired

您還可以在應用程式的 routes/console.php 檔案中配置計劃任務,按計劃自動清理令牌。

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

帶有 PKCE 的授權碼模式

帶有“授權碼交換證明金鑰”(PKCE) 的授權碼模式是一種安全的方法,可用於認證單頁應用程式或移動應用程式以訪問您的 API。當您無法保證客戶端金鑰被安全儲存,或者為了降低授權碼被攻擊者攔截的威脅時,應使用此模式。當使用“程式碼驗證器”和“程式碼挑戰”組合時,可以替代客戶端金鑰來將授權碼交換為訪問令牌。

建立客戶端

在您的應用程式能夠透過帶有 PKCE 的授權碼模式簽發令牌之前,您需要建立一個支援 PKCE 的客戶端。您可以使用帶有 --public 選項的 passport:client Artisan 命令來完成此操作。

1php artisan passport:client --public

請求令牌

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

由於此授權模式不提供客戶端金鑰,開發者需要生成程式碼驗證器和程式碼挑戰的組合,以便請求令牌。

根據 RFC 7636 規範,程式碼驗證器應為 43 到 128 個字元之間的隨機字串,包含字母、數字以及 "-"".""_""~" 字元。

程式碼挑戰應為 Base64 編碼的字串,且包含 URL 和檔名安全的字元。尾部的 '=' 字元應移除,且不得包含換行符、空格或其他額外字元。

1$encoded = base64_encode(hash('sha256', $codeVerifier, true));
2 
3$codeChallenge = strtr(rtrim($encoded, '='), '+/', '-_');

重定向進行授權

建立客戶端後,您可以使用客戶端 ID 以及生成的程式碼驗證器和程式碼挑戰從您的應用程式請求授權碼和訪問令牌。首先,消費應用程式應向您應用程式的 /oauth/authorize 路由發出重定向請求。

1use Illuminate\Http\Request;
2use Illuminate\Support\Str;
3 
4Route::get('/redirect', function (Request $request) {
5 $request->session()->put('state', $state = Str::random(40));
6 
7 $request->session()->put(
8 'code_verifier', $codeVerifier = Str::random(128)
9 );
10 
11 $codeChallenge = strtr(rtrim(
12 base64_encode(hash('sha256', $codeVerifier, true))
13 , '='), '+/', '-_');
14 
15 $query = http_build_query([
16 'client_id' => 'your-client-id',
17 'redirect_uri' => 'https://third-party-app.com/callback',
18 'response_type' => 'code',
19 'scope' => 'user:read orders:create',
20 'state' => $state,
21 'code_challenge' => $codeChallenge,
22 'code_challenge_method' => 'S256',
23 // 'prompt' => '', // "none", "consent", or "login"
24 ]);
25 
26 return redirect('https://passport-app.test/oauth/authorize?'.$query);
27});

將授權碼轉換為訪問令牌

如果使用者批准了授權請求,他們將被重定向回消費應用程式。消費者應像標準授權碼模式一樣,根據重定向前儲存的值驗證 state 引數。

如果 state 引數匹配,消費者應向您的應用程式發出 POST 請求以請求訪問令牌。請求應包含使用者批准授權請求時您的應用程式簽發的授權碼,以及最初生成的程式碼驗證器。

1use Illuminate\Http\Request;
2use Illuminate\Support\Facades\Http;
3 
4Route::get('/callback', function (Request $request) {
5 $state = $request->session()->pull('state');
6 
7 $codeVerifier = $request->session()->pull('code_verifier');
8 
9 throw_unless(
10 strlen($state) > 0 && $state === $request->state,
11 InvalidArgumentException::class
12 );
13 
14 $response = Http::asForm()->post('https://passport-app.test/oauth/token', [
15 'grant_type' => 'authorization_code',
16 'client_id' => 'your-client-id',
17 'redirect_uri' => 'https://third-party-app.com/callback',
18 'code_verifier' => $codeVerifier,
19 'code' => $request->code,
20 ]);
21 
22 return $response->json();
23});

裝置授權模式 (Device Authorization Grant)

OAuth2 裝置授權模式允許電視、遊戲機等無瀏覽器或輸入受限的裝置透過交換“裝置碼”來獲取訪問令牌。使用裝置流程時,裝置客戶端將引導使用者使用輔助裝置(如電腦或智慧手機)連線到您的伺服器,並在那裡輸入提供的“使用者碼”,從而批准或拒絕訪問請求。

首先,我們需要指示 Passport 如何返回我們的“使用者碼”和“授權”檢視。

所有授權檢視的渲染邏輯都可以透過 Laravel\Passport\Passport 類提供的適當方法進行自定義。通常,您應該從應用程式的 App\Providers\AppServiceProvider 類的 boot 方法中呼叫此方法。

1use Inertia\Inertia;
2use Laravel\Passport\Passport;
3 
4/**
5 * Bootstrap any application services.
6 */
7public function boot(): void
8{
9 // By providing a view name...
10 Passport::deviceUserCodeView('auth.oauth.device.user-code');
11 Passport::deviceAuthorizationView('auth.oauth.device.authorize');
12 
13 // By providing a closure...
14 Passport::deviceUserCodeView(
15 fn ($parameters) => Inertia::render('Auth/OAuth/Device/UserCode')
16 );
17 
18 Passport::deviceAuthorizationView(
19 fn ($parameters) => Inertia::render('Auth/OAuth/Device/Authorize', [
20 'request' => $parameters['request'],
21 'authToken' => $parameters['authToken'],
22 'client' => $parameters['client'],
23 'user' => $parameters['user'],
24 'scopes' => $parameters['scopes'],
25 ])
26 );
27 
28 // ...
29}

Passport 會自動定義返回這些檢視的路由。您的 auth.oauth.device.user-code 模板應包含一個向 passport.device.authorizations.authorize 路由傳送 GET 請求的表單。passport.device.authorizations.authorize 路由期望包含一個 user_code 查詢引數。

您的 auth.oauth.device.authorize 模板應包含一個向 passport.device.authorizations.approve 路由傳送 POST 請求以批准授權的表單,以及一個向 passport.device.authorizations.deny 路由傳送 DELETE 請求以拒絕授權的表單。passport.device.authorizations.approvepassport.device.authorizations.deny 路由期望包含 stateclient_idauth_token 欄位。

建立裝置授權模式客戶端

在您的應用程式能夠透過裝置授權模式簽發令牌之前,您需要建立一個啟用裝置流程的客戶端。您可以使用帶有 --device 選項的 passport:client Artisan 命令來完成此操作。此命令將建立一個第一方裝置流程啟用的客戶端,併為您提供客戶端 ID 和金鑰。

1php artisan passport:client --device

此外,您可以使用 ClientRepository 類上的 createDeviceAuthorizationGrantClient 方法來註冊屬於特定使用者的第三方客戶端。

1use App\Models\User;
2use Laravel\Passport\ClientRepository;
3 
4$user = User::find($userId);
5 
6$client = app(ClientRepository::class)->createDeviceAuthorizationGrantClient(
7 user: $user,
8 name: 'Example Device',
9 confidential: false,
10);

請求令牌

請求裝置碼

建立客戶端後,開發者可以使用他們的客戶端 ID 從您的應用程式請求裝置碼。首先,消費裝置應向您應用程式的 /oauth/device/code 路由發出 POST 請求以請求裝置碼。

1use Illuminate\Support\Facades\Http;
2 
3$response = Http::asForm()->post('https://passport-app.test/oauth/device/code', [
4 'client_id' => 'your-client-id',
5 'scope' => 'user:read orders:create',
6]);
7 
8return $response->json();

這將返回一個 JSON 響應,包含 device_codeuser_codeverification_uriintervalexpires_in 屬性。expires_in 屬性包含裝置碼過期前的秒數。interval 屬性包含消費裝置在輪詢 /oauth/token 路由時應等待的秒數,以避免速率限制錯誤。

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

顯示驗證 URI 和使用者碼

獲得裝置碼請求後,消費裝置應指示使用者使用另一臺裝置並訪問提供的 verification_uri,輸入 user_code 以批准授權請求。

輪詢令牌請求

由於使用者將使用單獨的裝置授予(或拒絕)訪問許可權,消費裝置應輪詢您應用程式的 /oauth/token 路由以確定使用者何時響應了請求。消費裝置應使用請求裝置碼時 JSON 響應中提供的最小輪詢 interval,以避免速率限制錯誤。

1use Illuminate\Support\Facades\Http;
2use Illuminate\Support\Sleep;
3 
4$interval = 5;
5 
6do {
7 Sleep::for($interval)->seconds();
8 
9 $response = Http::asForm()->post('https://passport-app.test/oauth/token', [
10 'grant_type' => 'urn:ietf:params:oauth:grant-type:device_code',
11 'client_id' => 'your-client-id',
12 'client_secret' => 'your-client-secret', // Required for confidential clients only...
13 'device_code' => 'the-device-code',
14 ]);
15 
16 if ($response->json('error') === 'slow_down') {
17 $interval += 5;
18 }
19} while (in_array($response->json('error'), ['authorization_pending', 'slow_down']));
20 
21return $response->json();

如果使用者批准了授權請求,將返回一個 JSON 響應,包含 access_tokenrefresh_tokenexpires_in 屬性。expires_in 屬性包含訪問令牌過期前的秒數。

密碼模式 (Password Grant)

我們不再建議使用密碼模式令牌。相反,您應該選擇 OAuth2 伺服器當前推薦的授權型別

OAuth2 密碼模式允許您的其他第一方客戶端(如移動應用程式)使用電子郵件地址/使用者名稱和密碼獲取訪問令牌。這使您可以安全地向第一方客戶端簽發訪問令牌,而無需使用者經歷整個 OAuth2 授權碼重定向流程。

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

1/**
2 * Bootstrap any application services.
3 */
4public function boot(): void
5{
6 Passport::enablePasswordGrant();
7}

建立密碼模式客戶端

在您的應用程式能夠透過密碼模式簽發令牌之前,您需要建立一個密碼模式客戶端。您可以使用帶有 --password 選項的 passport:client Artisan 命令來完成此操作。

1php artisan passport:client --password

請求令牌

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

1use Illuminate\Support\Facades\Http;
2 
3$response = Http::asForm()->post('https://passport-app.test/oauth/token', [
4 'grant_type' => 'password',
5 'client_id' => 'your-client-id',
6 'client_secret' => 'your-client-secret', // Required for confidential clients only...
7 'username' => '[email protected]',
8 'password' => 'my-password',
9 'scope' => 'user:read orders:create',
10]);
11 
12return $response->json();

請記住,訪問令牌預設是長期有效的。但是,如有必要,您可以自由配置您的最大訪問令牌有效期

請求所有作用域

使用密碼模式或客戶端憑證模式時,您可能希望授權該令牌訪問您應用程式支援的所有作用域。您可以透過請求 * 作用域來實現這一點。如果您請求 * 作用域,令牌例項上的 can 方法將始終返回 true。此作用域只能分配給使用 passwordclient_credentials 模式簽發的令牌。

1use Illuminate\Support\Facades\Http;
2 
3$response = Http::asForm()->post('https://passport-app.test/oauth/token', [
4 'grant_type' => 'password',
5 'client_id' => 'your-client-id',
6 'client_secret' => 'your-client-secret', // Required for confidential clients only...
7 'username' => '[email protected]',
8 'password' => 'my-password',
9 'scope' => '*',
10]);

自定義使用者提供程式

如果您的應用程式使用多個認證使用者提供程式,您可以在透過 artisan passport:client --password 命令建立客戶端時提供 --provider 選項,從而指定密碼模式客戶端使用哪個使用者提供程式。提供的提供程式名稱應與您應用程式 config/auth.php 配置檔案中定義的有效提供程式匹配。然後,您可以使用中介軟體保護您的路由,以確保僅有來自守衛指定提供程式的使用者獲得授權。

自定義使用者名稱欄位

使用密碼模式進行認證時,Passport 將使用您的認證模型中的 email 屬性作為“使用者名稱”。但是,您可以透過在模型上定義 findForPassport 方法來自定義此行為。

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Foundation\Auth\User as Authenticatable;
6use Illuminate\Notifications\Notifiable;
7use Laravel\Passport\Bridge\Client;
8use Laravel\Passport\Contracts\OAuthenticatable;
9use Laravel\Passport\HasApiTokens;
10 
11class User extends Authenticatable implements OAuthenticatable
12{
13 use HasApiTokens, Notifiable;
14 
15 /**
16 * Find the user instance for the given username.
17 */
18 public function findForPassport(string $username, Client $client): User
19 {
20 return $this->where('username', $username)->first();
21 }
22}

自定義密碼驗證

使用密碼模式進行認證時,Passport 將使用您模型的 password 屬性來驗證給定的密碼。如果您的模型沒有 password 屬性,或者您希望自定義密碼驗證邏輯,則可以在模型上定義 validateForPassportPasswordGrant 方法。

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Foundation\Auth\User as Authenticatable;
6use Illuminate\Notifications\Notifiable;
7use Illuminate\Support\Facades\Hash;
8use Laravel\Passport\Contracts\OAuthenticatable;
9use Laravel\Passport\HasApiTokens;
10 
11class User extends Authenticatable implements OAuthenticatable
12{
13 use HasApiTokens, Notifiable;
14 
15 /**
16 * Validate the password of the user for the Passport password grant.
17 */
18 public function validateForPassportPasswordGrant(string $password): bool
19 {
20 return Hash::check($password, $this->password);
21 }
22}

隱式模式 (Implicit Grant)

我們不再建議使用隱式模式令牌。相反,您應該選擇 OAuth2 伺服器當前推薦的授權型別

隱式模式類似於授權碼模式;但是,令牌在不交換授權碼的情況下返回給客戶端。此模式最常用於無法安全儲存客戶端憑證的 JavaScript 或移動應用程式。要啟用該模式,請在應用程式的 App\Providers\AppServiceProvider 類的 boot 方法中呼叫 enableImplicitGrant 方法。

1/**
2 * Bootstrap any application services.
3 */
4public function boot(): void
5{
6 Passport::enableImplicitGrant();
7}

在您的應用程式能夠透過隱式模式簽發令牌之前,您需要建立一個隱式模式客戶端。您可以使用帶有 --implicit 選項的 passport:client Artisan 命令來完成此操作。

1php artisan passport:client --implicit

啟用該模式並建立隱式客戶端後,開發者可以使用他們的客戶端 ID 從您的應用程式請求訪問令牌。消費應用程式應向您應用程式的 /oauth/authorize 路由發出重定向請求,如下所示:

1use Illuminate\Http\Request;
2 
3Route::get('/redirect', function (Request $request) {
4 $request->session()->put('state', $state = Str::random(40));
5 
6 $query = http_build_query([
7 'client_id' => 'your-client-id',
8 'redirect_uri' => 'https://third-party-app.com/callback',
9 'response_type' => 'token',
10 'scope' => 'user:read orders:create',
11 'state' => $state,
12 // 'prompt' => '', // "none", "consent", or "login"
13 ]);
14 
15 return redirect('https://passport-app.test/oauth/authorize?'.$query);
16});

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

客戶端憑證模式 (Client Credentials Grant)

客戶端憑證模式適用於機器對機器(M2M)的認證。例如,您可以在執行維護任務的計劃任務中使用此模式透過 API 進行操作。

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

1php artisan passport:client --client

接下來,將 Laravel\Passport\Http\Middleware\EnsureClientIsResourceOwner 中介軟體分配給路由。

1use Laravel\Passport\Http\Middleware\EnsureClientIsResourceOwner;
2 
3Route::get('/orders', function (Request $request) {
4 // Access token is valid and the client is resource owner...
5})->middleware(EnsureClientIsResourceOwner::class);

要限制對路由的訪問,使其僅限於特定的作用域,您可以向 using 方法提供所需的列表。

1Route::get('/orders', function (Request $request) {
2 // Access token is valid, the client is resource owner, and has both "servers:read" and "servers:create" scopes...
3})->middleware(EnsureClientIsResourceOwner::using('servers:read', 'servers:create'));

檢索令牌

要使用此模式檢索令牌,請向 oauth/token 端點發出請求。

1use Illuminate\Support\Facades\Http;
2 
3$response = Http::asForm()->post('https://passport-app.test/oauth/token', [
4 'grant_type' => 'client_credentials',
5 'client_id' => 'your-client-id',
6 'client_secret' => 'your-client-secret',
7 'scope' => 'servers:read servers:create',
8]);
9 
10return $response->json()['access_token'];

個人訪問令牌 (Personal Access Tokens)

有時,使用者希望在不經過常規授權碼重定向流程的情況下自行簽發訪問令牌。允許使用者透過應用程式的 UI 自行簽發令牌,有助於讓使用者嘗試您的 API,或者作為簽發訪問令牌的一種更簡單的方法。

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

建立個人訪問客戶端

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

1php artisan passport:client --personal

自定義使用者提供程式

如果您的應用程式使用多個認證使用者提供程式,您可以在透過 artisan passport:client --personal 命令建立客戶端時提供 --provider 選項,從而指定個人訪問模式客戶端使用哪個使用者提供程式。提供的提供程式名稱應與您應用程式 config/auth.php 配置檔案中定義的有效提供程式匹配。然後,您可以使用中介軟體保護您的路由,以確保僅有來自守衛指定提供程式的使用者獲得授權。

管理個人訪問令牌

建立個人訪問客戶端後,您可以使用 App\Models\User 模型例項上的 createToken 方法為特定使用者簽發令牌。createToken 方法接受令牌名稱作為第一個引數,並接受可選的作用域陣列作為第二個引數。

1use App\Models\User;
2use Illuminate\Support\Facades\Date;
3use Laravel\Passport\Token;
4 
5$user = User::find($userId);
6 
7// Creating a token without scopes...
8$token = $user->createToken('My Token')->accessToken;
9 
10// Creating a token with scopes...
11$token = $user->createToken('My Token', ['user:read', 'orders:create'])->accessToken;
12 
13// Creating a token with all scopes...
14$token = $user->createToken('My Token', ['*'])->accessToken;
15 
16// Retrieving all the valid personal access tokens that belong to the user...
17$tokens = $user->tokens()
18 ->with('client')
19 ->where('revoked', false)
20 ->where('expires_at', '>', Date::now())
21 ->get()
22 ->filter(fn (Token $token) => $token->client->hasGrantType('personal_access'));

保護路由

透過中介軟體

Passport 包含一個認證守衛,可以驗證傳入請求中的訪問令牌。一旦您配置了 api 守衛以使用 passport 驅動,您只需在任何需要有效訪問令牌的路由上指定 auth:api 中介軟體即可。

1Route::get('/user', function () {
2 // Only API authenticated users may access this route...
3})->middleware('auth:api');

如果您使用的是客戶端憑證模式,則應使用 Laravel\Passport\Http\Middleware\EnsureClientIsResourceOwner 中介軟體來保護路由,而不是 auth:api 中介軟體。

多認證守衛

如果您的應用程式認證了使用完全不同 Eloquent 模型的不同型別使用者,您很可能需要為應用程式中的每種使用者提供程式型別定義守衛配置。這使您可以保護旨在針對特定使用者提供程式的請求。例如,考慮到 config/auth.php 配置檔案中的以下守衛配置:

1'guards' => [
2 'api' => [
3 'driver' => 'passport',
4 'provider' => 'users',
5 ],
6 
7 'api-customers' => [
8 'driver' => 'passport',
9 'provider' => 'customers',
10 ],
11],

以下路由將利用使用 customers 使用者提供程式的 api-customers 守衛來認證傳入的請求。

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

有關將多個使用者提供程式與 Passport 結合使用的更多資訊,請參閱個人訪問令牌文件密碼模式文件

傳遞訪問令牌

呼叫受 Passport 保護的路由時,應用程式的 API 消費者應在請求的 Authorization 標頭中指定其訪問令牌作為 Bearer 令牌。例如,使用 Http Facade 時:

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

令牌作用域 (Token Scopes)

作用域允許您的 API 客戶端在請求訪問賬戶許可權時請求特定的一組許可權。例如,如果您正在構建一個電子商務應用程式,並不是所有 API 消費者都需要下單的能力。相反,您可以允許消費者僅請求訪問訂單發貨狀態的許可權。換句話說,作用域允許您應用程式的使用者限制第三方應用程式代表他們執行的操作。

定義作用域

您可以在應用程式的 App\Providers\AppServiceProvider 類的 boot 方法中使用 Passport::tokensCan 方法定義 API 的作用域。tokensCan 方法接受作用域名稱和作用域描述的陣列。作用域描述可以是任何您希望的內容,並將顯示在授權批准螢幕上供使用者檢視。

1/**
2 * Bootstrap any application services.
3 */
4public function boot(): void
5{
6 Passport::tokensCan([
7 'user:read' => 'Retrieve the user info',
8 'orders:create' => 'Place orders',
9 'orders:read:status' => 'Check order status',
10 ]);
11}

預設作用域

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

1use Laravel\Passport\Passport;
2 
3Passport::tokensCan([
4 'user:read' => 'Retrieve the user info',
5 'orders:create' => 'Place orders',
6 'orders:read:status' => 'Check order status',
7]);
8 
9Passport::defaultScopes([
10 'user:read',
11 'orders:create',
12]);

為令牌分配作用域

請求授權碼時

使用授權碼模式請求訪問令牌時,消費者應將其所需的作用域指定為 scope 查詢字串引數。scope 引數應為以空格分隔的作用域列表。

1Route::get('/redirect', function () {
2 $query = http_build_query([
3 'client_id' => 'your-client-id',
4 'redirect_uri' => 'https://third-party-app.com/callback',
5 'response_type' => 'code',
6 'scope' => 'user:read orders:create',
7 ]);
8 
9 return redirect('https://passport-app.test/oauth/authorize?'.$query);
10});

簽發個人訪問令牌時

如果您正在使用 App\Models\User 模型的 createToken 方法簽發個人訪問令牌,您可以將所需作用域的陣列作為該方法的第二個引數傳遞。

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

檢查作用域

Passport 包含兩個中介軟體,可用於驗證傳入請求是否使用已授予特定作用域的令牌進行了認證。

檢查所有作用域

Laravel\Passport\Http\Middleware\CheckToken 中介軟體可以分配給路由,以驗證傳入請求的訪問令牌是否具有列出的所有作用域。

1use Laravel\Passport\Http\Middleware\CheckToken;
2 
3Route::get('/orders', function () {
4 // Access token has both "orders:read" and "orders:create" scopes...
5})->middleware(['auth:api', CheckToken::using('orders:read', 'orders:create')]);

檢查任意作用域

Laravel\Passport\Http\Middleware\CheckTokenForAnyScope 中介軟體可以分配給路由,以驗證傳入請求的訪問令牌是否具有所列作用域中的至少一個

1use Laravel\Passport\Http\Middleware\CheckTokenForAnyScope;
2 
3Route::get('/orders', function () {
4 // Access token has either "orders:read" or "orders:create" scope...
5})->middleware(['auth:api', CheckTokenForAnyScope::using('orders:read', 'orders:create')]);

檢查令牌例項上的作用域

一旦訪問令牌認證請求進入您的應用程式,您仍然可以使用已認證 App\Models\User 例項上的 tokenCan 方法來檢查令牌是否具有特定作用域。

1use Illuminate\Http\Request;
2 
3Route::get('/orders', function (Request $request) {
4 if ($request->user()->tokenCan('orders:create')) {
5 // ...
6 }
7});

其他作用域方法

scopeIds 方法將返回所有定義 ID / 名稱的陣列。

1use Laravel\Passport\Passport;
2 
3Passport::scopeIds();

scopes 方法將返回所有定義作用域的陣列(作為 Laravel\Passport\Scope 的例項)。

1Passport::scopes();

scopesFor 方法將返回與給定 ID / 名稱匹配的 Laravel\Passport\Scope 例項陣列。

1Passport::scopesFor(['user:read', 'orders:create']);

您可以使用 hasScope 方法確定是否已定義特定作用域。

1Passport::hasScope('orders:create');

SPA 認證

構建 API 時,能夠從您的 JavaScript 應用程式消費您自己的 API 非常有用。這種 API 開發方法允許您自己的應用程式消費與您向世界共享的相同 API。同一個 API 可以被您的 Web 應用程式、移動應用程式、第三方應用程式以及您可能釋出在各種包管理器上的任何 SDK 所消費。

通常,如果您想從 JavaScript 應用程式消費您的 API,您需要手動向應用程式傳送訪問令牌,並在每次嚮應用程式發出請求時傳遞它。然而,Passport 包含一個可以為您處理此問題的中介軟體。您所要做的就是在應用程式的 bootstrap/app.php 檔案中將 CreateFreshApiToken 中介軟體附加到 web 中介軟體組中。

1use Laravel\Passport\Http\Middleware\CreateFreshApiToken;
2 
3->withMiddleware(function (Middleware $middleware): void {
4 $middleware->web(append: [
5 CreateFreshApiToken::class,
6 ]);
7})

您應確保 CreateFreshApiToken 中介軟體是您中介軟體堆疊中列出的最後一箇中間件。

此中介軟體將向您的傳出響應附加一個 laravel_token cookie。該 cookie 包含一個加密的 JWT,Passport 將使用該 JWT 認證來自 JavaScript 應用程式的 API 請求。JWT 的有效期等於您的 session.lifetime 配置值。現在,由於瀏覽器會自動隨所有後續請求傳送 cookie,您可以嚮應用程式的 API 發出請求,而無需顯式傳遞訪問令牌。

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

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

1/**
2 * Bootstrap any application services.
3 */
4public function boot(): void
5{
6 Passport::cookie('custom_name');
7}

CSRF 保護

使用此認證方法時,您需要確保請求中包含有效的 CSRF 令牌標頭。框架自帶的預設 Laravel JavaScript 腳手架以及所有啟動包都包含一個 Axios 例項,它會自動使用加密的 XSRF-TOKEN cookie 值在同源請求上傳送 X-XSRF-TOKEN 標頭。

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

活動

Passport 在簽發訪問令牌和重新整理令牌時會觸發事件。您可以監聽這些事件以清理或撤銷資料庫中的其他訪問令牌。

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

測試

Passport 的 actingAs 方法可用於指定當前已認證的使用者及其作用域。傳遞給 actingAs 方法的第一個引數是使用者例項,第二個引數是應授予使用者令牌的作用域陣列。

1use App\Models\User;
2use Laravel\Passport\Passport;
3 
4test('orders can be created', function () {
5 Passport::actingAs(
6 User::factory()->create(),
7 ['orders:create']
8 );
9 
10 $response = $this->post('/api/orders');
11 
12 $response->assertStatus(201);
13});
1use App\Models\User;
2use Laravel\Passport\Passport;
3 
4public function test_orders_can_be_created(): void
5{
6 Passport::actingAs(
7 User::factory()->create(),
8 ['orders:create']
9 );
10 
11 $response = $this->post('/api/orders');
12 
13 $response->assertStatus(201);
14}

Passport 的 actingAsClient 方法可用於指定當前已認證的客戶端及其作用域。傳遞給 actingAsClient 方法的第一個引數是客戶端例項,第二個引數是應授予客戶端令牌的作用域陣列。

1use Laravel\Passport\Client;
2use Laravel\Passport\Passport;
3 
4test('servers can be retrieved', function () {
5 Passport::actingAsClient(
6 Client::factory()->create(),
7 ['servers:read']
8 );
9 
10 $response = $this->get('/api/servers');
11 
12 $response->assertStatus(200);
13});
1use Laravel\Passport\Client;
2use Laravel\Passport\Passport;
3 
4public function test_servers_can_be_retrieved(): void
5{
6 Passport::actingAsClient(
7 Client::factory()->create(),
8 ['servers:read']
9 );
10 
11 $response = $this->get('/api/servers');
12 
13 $response->assertStatus(200);
14}