Laravel Passport
- 简介
- 安装
- 配置
- 授权码模式 (Authorization Code Grant)
- 带有 PKCE 的授权码模式
- 设备授权模式 (Device Authorization Grant)
- 密码模式 (Password Grant)
- 隐式模式 (Implicit Grant)
- 客户端凭证模式 (Client Credentials Grant)
- 个人访问令牌 (Personal Access Tokens)
- 保护路由
- 令牌作用域 (Token Scopes)
- SPA 认证
- 活动
- 测试
简介
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 OAuthenticatable12{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(): void5{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 签发的访问令牌有效期为一年。如果您想配置更长或更短的令牌有效期,可以使用 tokensExpireIn、refreshTokensExpireIn 和 personalAccessTokensExpireIn 方法。这些方法应在您应用程序的 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 PassportClient4{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(): void12{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 定义的路由。要实现这一点,您首先需要通过在应用程序的 AppServiceProvider 的 register 方法中添加 Passport::ignoreRoutes 来忽略 Passport 注册的路由。
1use Laravel\Passport\Passport;2 3/**4 * Register any application services.5 */6public function register(): void7{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.approve 和 passport.authorizations.deny 路由期望包含 state、client_id 和 auth_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: true13);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_uri。redirect_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[] $scopes14 */15 public function skipsAuthorization(Authenticatable $user, array $scopes): bool16 {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_token、refresh_token 和 expires_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_token、refresh_token 和 expires_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::class12 );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.approve 和 passport.device.authorizations.deny 路由期望包含 state、client_id 和 auth_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_code、user_code、verification_uri、interval 和 expires_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_token、refresh_token 和 expires_in 属性。expires_in 属性包含访问令牌过期前的秒数。
密码模式 (Password Grant)
我们不再建议使用密码模式令牌。相反,您应该选择 OAuth2 服务器当前推荐的授权类型。
OAuth2 密码模式允许您的其他第一方客户端(如移动应用程序)使用电子邮件地址/用户名和密码获取访问令牌。这使您可以安全地向第一方客户端签发访问令牌,而无需用户经历整个 OAuth2 授权码重定向流程。
要启用密码模式,请在应用程序的 App\Providers\AppServiceProvider 类的 boot 方法中调用 enablePasswordGrant 方法。
1/**2 * Bootstrap any application services.3 */4public function boot(): void5{6 Passport::enablePasswordGrant();7}
创建密码模式客户端
在您的应用程序能够通过密码模式签发令牌之前,您需要创建一个密码模式客户端。您可以使用带有 --password 选项的 passport:client Artisan 命令来完成此操作。
1php artisan passport:client --password
请求令牌
启用该模式并创建密码模式客户端后,您可以通过使用用户的电子邮件地址和密码向 /oauth/token 路由发出 POST 请求来请求访问令牌。请记住,此路由已由 Passport 注册,因此无需手动定义。如果请求成功,您将在服务器的 JSON 响应中收到 access_token 和 refresh_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... 8 'password' => 'my-password', 9 'scope' => 'user:read orders:create',10]);11 12return $response->json();
请记住,访问令牌默认是长期有效的。但是,如有必要,您可以自由配置您的最大访问令牌有效期。
请求所有作用域
使用密码模式或客户端凭证模式时,您可能希望授权该令牌访问您应用程序支持的所有作用域。您可以通过请求 * 作用域来实现这一点。如果您请求 * 作用域,令牌实例上的 can 方法将始终返回 true。此作用域只能分配给使用 password 或 client_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... 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 OAuthenticatable12{13 use HasApiTokens, Notifiable;14 15 /**16 * Find the user instance for the given username.17 */18 public function findForPassport(string $username, Client $client): User19 {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 OAuthenticatable12{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): bool19 {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(): void5{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 保护的路由时,应用程序的 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 });
自定义 Cookie 名称
如果需要,您可以使用 Passport::cookie 方法自定义 laravel_token cookie 的名称。通常,此方法应在应用程序的 App\Providers\AppServiceProvider 类的 boot 方法中调用。
1/**2 * Bootstrap any application services.3 */4public function boot(): void5{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}