跳至内容

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:apiArtisan 命令安装 Laravel Passport:

1php artisan install:api --passport

此命令将发布并运行必要的数据库迁移,以创建应用程序存储 OAuth2 客户端和访问令牌所需的表。该命令还将创建生成安全访问令牌所需的加密密钥。

运行该install:api命令后,将Laravel\Passport\HasApiTokens特征和Laravel\Passport\Contracts\OAuthenticatable接口添加到App\Models\User模型中。此特征将为模型提供一些辅助方法,使您可以检查已验证用户的令牌和作用域:

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选项设置为。这将指示你的应用程序在对传入的 API 请求进行身份验证时passport使用 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中调用bootApp\Providers\AppServiceProvider

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

从环境中加载密钥

或者,您可以使用 Artisan 命令发布 Passport 的配置文件vendor:publish

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 会发放长期访问令牌,有效期为一年。如果您想配置更长/更短的令牌有效期,可以使用tokensExpireInrefreshTokensExpireIn和方法。这些方法应该从应用程序类的方法personalAccessTokensExpireIn中调用bootApp\Providers\AppServiceProvider

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}

定义模型后,您可以通过类指示 Passport 使用您的自定义模型。通常,您应该在应用程序类的方法Laravel\Passport\Passport中告知 Passport 您的自定义模型bootApp\Providers\AppServiceProvider

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;
6 
7/**
8 * Bootstrap any application services.
9 */
10public function boot(): void
11{
12 Passport::useTokenModel(Token::class);
13 Passport::useRefreshTokenModel(RefreshToken::class);
14 Passport::useAuthCodeModel(AuthCode::class);
15 Passport::useClientModel(Client::class);
16 Passport::useDeviceCodeModel(DeviceCode::class)
17}

覆盖路由

有时你可能希望自定义 Passport 定义的路由。为此,你首先需要Passport::ignoreRoutesregister应用程序的 方法中添加以下内容,以忽略 Passport 注册的路由AppServiceProvider

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

授权码授予

大多数开发者都熟悉通过授权码使用 OAuth2。使用授权码时,客户端应用程序会将用户重定向到您的服务器,服务器会批准或拒绝向客户端颁发访问令牌的请求。

首先,我们需要指示 Passport 如何返回我们的“授权”视图。

所有授权视图的渲染逻辑都可以使用该类中提供的相应方法进行自定义Laravel\Passport\Passport。通常,你应该从boot应用程序App\Providers\AppServiceProvider类的方法中调用此方法:

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模板应包含一个表单,该表单向路由发出 POST 请求passport.authorizations.approve以批准授权,以及一个表单,该表单向路由发出 DELETE 请求passport.authorizations.deny以拒绝授权。passport.authorizations.approvepassport.authorizations.deny路由需要stateclient_idauth_token字段。

管理客户

开发者如果构建的应用程序需要与您的应用程序 API 交互,则需要通过创建“客户端”将其应用程序注册到您的 API 中。通常,这包括提供其应用程序的名称以及一个 URI,当用户批准授权请求后,您的应用程序可以重定向到该 URI。

第一方客户端

创建客户端最简单的方法是使用passport:clientArtisan 命令。此命令可用于创建第一方客户端或测试 OAuth2 功能。运行该passport:client命令时,Passport 会Prompts您输入有关客户端的更多信息,并提供客户端 ID 和密钥:

1php artisan passport:client

如果您希望为客户端允许多个重定向 URI,您可以在命令Prompts输入 URI 时使用逗号分隔的列表指定它们passport:client。任何包含逗号的 URI 都应进行 URI 编码:

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

第三方客户端

由于您的应用程序的用户将无法使用该passport:client命令,您可以使用类createAuthorizationCodeGrantClient的方法Laravel\Passport\ClientRepository来为给定用户注册客户端:

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 应用程序将始终Prompts用户重新登录,即使用户已有一个会话。

如果没有prompt提供任何值,则仅当用户之前未授权访问所请求范围的使用应用程序时,才会Prompts用户进行授权。

请记住,该/oauth/authorize路线已由 Passport 定义。您无需手动定义此路线。

批准请求

收到授权请求时,Passport 会根据prompt参数值(如有)自动响应,并可能向用户显示一个模板,允许用户批准或拒绝授权请求。如果用户批准请求,则会被重定向回redirect_uri使用应用程序指定的 URL。该 URLredirect_uri必须redirect与客户端创建时指定的 URL 匹配。

有时您可能希望跳过授权Prompts,例如在授权第一方客户端时。您可以通过扩展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/tokenJSON 响应属性包含访问令牌过期前的秒数。access_tokenrefresh_tokenexpires_inexpires_in

与路线一样/oauth/authorize/oauth/tokenPassport 会为您定义路线。无需手动定义此路线。

管理代币

tokens你可以使用 trait的方法检索用户的授权令牌Laravel\Passport\HasApiTokens。例如,这可以用于为你的用户提供一个仪表板,以跟踪他们与第三方应用程序的连接:

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/tokenJSON 响应属性包含访问令牌过期前的秒数。access_tokenrefresh_tokenexpires_inexpires_in

撤销令牌

revoke您可以使用模型上的方法来撤销令牌。您可以使用模型上的方法Laravel\Passport\Token来撤销令牌的刷新令牌revokeLaravel\Passport\RefreshToken

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:purgeArtisan 命令可以帮你完成此操作:

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 的客户端。你可以使用passport:clientArtisan 命令并结合以下--public选项来执行此操作:

1php artisan passport:client --public

请求令牌

代码验证器和代码挑战

由于此授权授予不提供客户端机密,因此开发人员需要生成代码验证器和代码质询的组合才能请求令牌。

代码验证器应为长度在 43 到 128 个字符之间的随机字符串,包含字母、数字和 "-"".""_""~"字符,如RFC 7636 规范中所定义。

代码验证应为 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});

设备授权授予

OAuth2 设备授权允许无浏览器或输入受限的设备(例如电视和游戏机)通过交换“设备代码”来获取访问令牌。使用设备Processes时,设备客户端将指示用户使用辅助设备(例如计算机或智能手机)连接到您的服务器,并在服务器中输入提供的“用户代码”,然后批准或拒绝访问请求。

首先,我们需要指示 Passport 如何返回我们的“用户代码”和“授权”视图。

所有授权视图的渲染逻辑都可以使用该类中提供的相应方法进行自定义。通常,您应该从应用程序类的方法Laravel\Passport\Passport中调用此方法bootApp\Providers\AppServiceProvider

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模板应该包含一个表单,用于向该路由发出 GET 请求passport.device.authorizations.authorize。该passport.device.authorizations.authorize路由需要一个user_code查询参数。

您的auth.oauth.device.authorize模板应包含一个表单,该表单向路由发出 POST 请求passport.device.authorizations.approve以批准授权,以及一个表单,该表单向passport.device.authorizations.deny路由发出 DELETE 请求以拒绝授权。passport.device.authorizations.approvepassport.device.authorizations.deny路由需要stateclient_idauth_token字段。

创建设备授权授予客户端

在您的应用程序能够通过设备授权授予机制发放令牌之前,您需要创建一个支持设备流的客户端。您可以使用passport:clientArtisan 命令并配合以下--device选项来执行此操作。此命令将创建一个支持设备流的第一方客户端,并为您提供客户端 ID 和密钥:

1php artisan passport:client --device

此外,您可以使用类createDeviceAuthorizationGrantClient上的方法ClientRepository来注册属于给定用户的第三方客户端:

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 向您的应用请求设备代码。首先,使用设备应向POST您的应用/oauth/device/code路由发出请求,以请求设备代码:

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();

device_code这将返回一个包含、user_codeverification_uriinterval和属性的JSON 响应expires_inexpires_in属性包含设备代码过期前的秒数。interval属性包含使用设备在轮询路由时应在两次请求之间等待的秒数,/oauth/token以避免速率限制错误。

请记住,该/oauth/device/code路线已由 Passport 定义。您无需手动定义此路线。

显示验证 URI 和用户代码

一旦获得设备代码请求,消费设备应指示用户使用另一台设备并访问所提供的verification_uri并输入user_code以批准授权请求。

轮询令牌请求

由于用户将使用单独的设备来授予(或拒绝)访问权限,因此使用设备应轮询应用的/oauth/token路由,以确定用户何时响应了请求。使用设备interval在请求设备代码时应使用 JSON 响应中提供的最小轮询次数,以避免出现速率限制错误:

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();

如果用户批准了授权请求,这将返回一个包含access_tokenrefresh_tokenexpires_in属性的 JSON 响应。expires_in属性包含访问令牌过期前的秒数。

密码授予

我们不再建议使用密码授权令牌。相反,您应该选择OAuth2 服务器当前推荐的授权类型

OAuth2 密码授予允许您的其他第一方客户端(例如移动应用)使用电子邮件地址/用户名和密码获取访问令牌。这使您能够安全地向第一方客户端发放访问令牌,而无需用户经历整个 OAuth2 授权码重定向Processes。

要启用密码授予,请enablePasswordGrantboot应用程序App\Providers\AppServiceProvider类的方法中调用该方法:

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

创建密码授予客户端

在你的应用程序能够通过密码授予机制颁发令牌之前,你需要创建一个密码授予客户端。你可以使用passport:clientArtisan 命令的--password选项来执行此操作。

1php artisan passport:client --password

请求令牌

启用授权并创建密码授权客户端后,您可以通过POST向路由发送包含用户电子邮件地址和密码的请求来请求访问令牌/oauth/token。请记住,此路由已由 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' => 'taylor@laravel.com',
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' => 'taylor@laravel.com',
8 'password' => 'my-password',
9 'scope' => '*',
10]);

自定义用户提供程序

如果您的应用程序使用多个身份验证用户提供程序--provider,您可以在通过命令创建客户端时提供一个选项,指定密码授予客户端使用哪个用户提供程序artisan passport:client --password。给定的提供程序名称应与应用程序配置文件中定义的有效提供程序匹配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\Contracts\OAuthenticatable;
8use Laravel\Passport\HasApiTokens;
9 
10class User extends Authenticatable implements OAuthenticatable
11{
12 use HasApiTokens, Notifiable;
13 
14 /**
15 * Find the user instance for the given username.
16 */
17 public function findForPassport(string $username): User
18 {
19 return $this->where('username', $username)->first();
20 }
21}

自定义密码验证

当使用密码授权进行身份验证时,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}

隐性授予

我们不再建议使用隐式授权令牌。相反,您应该选择OAuth2 服务器当前推荐的授权类型

隐式授权类似于授权码授权;但是,它会将令牌返回给客户端,而无需交换授权码。这种授权最常用于无法安全存储客户端凭据的 JavaScript 或移动应用。要启用此授权,请在应用类的方法enableImplicitGrant中调用以下方法bootApp\Providers\AppServiceProvider

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

在你的应用程序能够通过隐式授权发放令牌之前,你需要创建一个隐式授权客户端。你可以使用passport:clientArtisan 命令并添加以下--implicit选项来执行此操作。

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 定义。您无需手动定义此路线。

客户端凭证授予

客户端凭据授予适用于机器对机器身份验证。例如,您可以在通过 API 执行维护任务的计划作业中使用此授予。

在你的应用程序能够通过客户端凭据授予机制颁发令牌之前,你需要创建一个客户端凭据授予客户端。你可以使用Artisan 命令--client的选项来执行此操作passport: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'];

个人访问令牌

有时,您的用户可能希望自行发放访问令牌,而无需经过典型的授权码重定向Processes。允许用户通过应用程序的 UI 自行发放令牌,这对于方便用户试用 API 非常有用,或者可以作为一种更简单的访问令牌发放方法。

如果您的应用程序主要使用 Passport 来颁发个人访问令牌,请考虑使用Laravel Sanctum,这是 Laravel 用于颁发 API 访问令牌的轻量级第一方库。

创建个人访问客户端

在你的应用程序能够颁发个人访问令牌之前,你需要创建一个个人访问客户端。你可以使用passport:clientArtisan 命令并加上以下--personal选项来创建它。如果你已经运行过该passport:install命令,则无需再运行以下命令:

1php artisan passport:client --personal

自定义用户提供程序

如果您的应用程序使用多个身份验证用户提供程序--provider,您可以在通过命令创建客户端时提供一个选项,指定个人访问权限授予客户端使用哪个用户提供程序artisan passport:client --personal。给定的提供程序名称应与应用程序配置文件中定义的有效提供程序匹配config/auth.php。然后,您可以使用中间件保护您的路由,以确保只有来自守卫指定提供程序的用户才能获得授权。

管理个人访问令牌

创建个人访问客户端后,可以使用模型实例createToken上的方法来为指定用户颁发令牌App\Models\User。该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],

以下路线将利用api-customers使用customers用户提供程序的守卫来验证传入的请求:

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

有关使用 Passport 的多个用户提供程序的更多信息,请参阅个人访问令牌文档密码授予文档

传递访问令牌

调用受 Passport 保护的路由时,应用程序的 API 使用者应BearerAuthorization其请求标头中指定访问令牌。例如,使用HttpFacade 时:

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();

令牌作用域

范围允许您的 API 客户端在请求访问帐户的授权时请求一组特定的权限。例如,如果您正在构建一个电子商务应用,并非所有 API 使用者都需要下单的能力。您可以允许使用者仅请求访问订单发货状态的授权。换句话说,范围允许您的应用用户限制第三方应用可以代表他们执行的操作。

定义范围

您可以使用应用程序类Passport::tokensCan中的方法定义 API 的作用域。该方法接受一个包含作用域名称和作用域描述的数组。作用域描述可以是任何您想要的内容,并将在授权批准屏幕上显示给用户:bootApp\Providers\AppServiceProvidertokensCan

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}

默认范围

如果客户端没有请求任何特定的作用域,你可以使用以下方法配置 Passport 服务器,将默认作用域附加到令牌。通常,你应该从应用程序类的方法defaultScopes中调用此方法bootApp\Providers\AppServiceProvider

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');

检查令牌实例的作用域

一旦访问令牌验证请求进入您的应用程序,您仍然可以使用经过tokenCan身份验证的实例上的方法检查令牌是否具有给定的范围App\Models\User

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();

该方法将返回与给定 ID / 名称匹配的实例scopesFor数组:Laravel\Passport\Scope

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

您可以使用该方法确定给定的范围是否已经定义hasScope

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

SPA认证

构建 API 时,能够在 JavaScript 应用程序中使用您自己的 API 非常有用。这种 API 开发方法允许您自己的应用程序使用您与外界共享的 API。您的 Web 应用程序、移动应用程序、第三方应用程序以及您在各种包管理器上发布的任何 SDK 都可以使用相同的 API。

通常,如果您想在 JavaScript 应用程序中使用 API,则需要手动向应用程序发送访问令牌,并将其与每个请求一起传递。但是,Passport 包含一个可以为您处理此问题的中间件。您只需将CreateFreshApiToken中间件附加到web应用程序bootstrap/app.php文件中的中间件组中即可:

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

您应该确保该CreateFreshApiToken中间件是中间件堆栈中列出的最后一个中间件。

此中间件会将laravel_tokenCookie 附加到您发出的响应中。此 Cookie 包含一个加密的 JWT,Passport 将使用它来验证来自 JavaScript 应用程序的 API 请求。JWT 的生命周期等于您的session.lifetime配置值。现在,由于浏览器会自动将 Cookie 随所有后续请求一起发送,因此您可以无需显式传递访问令牌即可向应用程序的 API 发出请求:

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

如果需要,您可以laravel_token使用该Passport::cookie方法自定义 Cookie 的名称。通常,此方法应从boot应用程序App\Providers\AppServiceProvider类的方法中调用:

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

CSRF保护

使用此身份验证方法时,您需要确保请求中包含有效的 CSRF 令牌标头。主干应用程序和所有入门套件中默认包含的 Laravel JavaScript 脚手架都包含一个Axios实例,该实例会自动使用加密的Cookie 值在同源请求中XSRF-TOKEN发送标头。X-XSRF-TOKEN

如果您选择发送X-CSRF-TOKEN标头而不是X-XSRF-TOKEN,则需要使用提供的未加密令牌csrf_token()

Events

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}