Skip to content
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待

Laravel Passport

简介

Laravel Passport 可以在几分钟内为你的 Laravel 应用程序提供完整的 OAuth2 服务器实现。Passport 构建于由 Andy Millington 和 Simon Hamp 维护的 League OAuth2 服务器之上。

NOTE

本文档假设你已经熟悉 OAuth2。如果你对 OAuth2 一无所知,请在继续之前熟悉一下 OAuth2 的一般术语和特性。

Passport 还是 Sanctum?

在开始之前,你可能需要确定你的应用程序更适合使用 Laravel Passport 还是 Laravel Sanctum。如果你的应用程序绝对需要支持 OAuth2,那么你应该使用 Laravel Passport。

然而,如果你正在尝试认证单页应用程序、移动应用程序或颁发 API 令牌,你应该使用 Laravel Sanctum。Laravel Sanctum 不支持 OAuth2;但它提供了更简单的 API 认证开发体验。

安装

你可以通过 install:api Artisan 命令安装 Laravel Passport:

shell
php artisan install:api --passport

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

运行 install:api 命令后,将 Laravel\Passport\HasApiTokens trait 和 Laravel\Passport\Contracts\OAuthenticatable 接口添加到你的 App\Models\User 模型中。该 trait 将为你的模型提供一些辅助方法,允许你检查已验证用户的令牌和范围:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Passport\Contracts\OAuthenticatable;
use Laravel\Passport\HasApiTokens;

class User extends Authenticatable implements OAuthenticatable
{
    use HasApiTokens, HasFactory, Notifiable;
}

最后,在你的应用程序的 config/auth.php 配置文件中,你应该定义一个 api 认证守卫,并将 driver 选项设置为 passport。这将指示你的应用程序在认证传入的 API 请求时使用 Passport 的 TokenGuard

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

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

部署 Passport

首次将 Passport 部署到应用程序的服务器时,你可能需要运行 passport:keys 命令。此命令生成 Passport 生成访问令牌所需的加密密钥。生成的密钥通常不保存在源代码控制中:

shell
php artisan passport:keys

如有必要,你可以定义 Passport 密钥应从中加载的路径。你可以使用 Passport::loadKeysFrom 方法来完成此操作。通常,此方法应在应用程序的 App\Providers\AppServiceProvider 类的 boot 方法中调用:

php
/**
 * 引导任何应用程序服务。
 */
public function boot(): void
{
    Passport::loadKeysFrom(__DIR__.'/../secrets/oauth');
}

从环境中加载密钥

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

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

发布配置文件后,你可以通过将应用程序的加密密钥定义为环境变量来加载它们:

ini
PASSPORT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
<private key here>
-----END RSA PRIVATE KEY-----"

PASSPORT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
<public key here>
-----END PUBLIC KEY-----"

升级 Passport

升级到 Passport 的新主要版本时,仔细阅读升级指南非常重要。

配置

令牌有效期

默认情况下,Passport 颁发一年后过期的长期访问令牌。如果你希望配置更长/更短的令牌有效期,可以使用 tokensExpireInrefreshTokensExpireInpersonalAccessTokensExpireIn 方法。这些方法应在应用程序的 App\Providers\AppServiceProvider 类的 boot 方法中调用:

php
use Carbon\CarbonInterval;

/**
 * 引导任何应用程序服务。
 */
public function boot(): void
{
    Passport::tokensExpireIn(CarbonInterval::days(15));
    Passport::refreshTokensExpireIn(CarbonInterval::days(30));
    Passport::personalAccessTokensExpireIn(CarbonInterval::months(6));
}

WARNING

Passport 数据库表上的 expires_at 列是只读的,仅用于显示目的。颁发令牌时,Passport 将过期信息存储在签名和加密的令牌中。如果你需要使令牌失效,你应该撤销它

覆盖默认模型

你可以通过定义自己的模型并扩展相应的 Passport 模型来自由扩展 Passport 内部使用的模型:

php
use Laravel\Passport\Client as PassportClient;

class Client extends PassportClient
{
    // ...
}

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

php
use App\Models\Passport\AuthCode;
use App\Models\Passport\Client;
use App\Models\Passport\DeviceCode;
use App\Models\Passport\RefreshToken;
use App\Models\Passport\Token;
use Laravel\Passport\Passport;

/**
 * 引导任何应用程序服务。
 */
public function boot(): void
{
    Passport::useTokenModel(Token::class);
    Passport::useRefreshTokenModel(RefreshToken::class);
    Passport::useAuthCodeModel(AuthCode::class);
    Passport::useClientModel(Client::class);
    Passport::useDeviceCodeModel(DeviceCode::class);
}

覆盖路由

有时你可能希望自定义 Passport 定义的路由。为此,你首先需要通过在你的应用程序的 AppServiceProviderregister 方法中添加 Passport::ignoreRoutes 来忽略 Passport 注册的路由:

php
use Laravel\Passport\Passport;

/**
 * 注册任何应用程序服务。
 */
public function register(): void
{
    Passport::ignoreRoutes();
}

然后,你可以将 Passport 在其路由文件中定义的路由复制到你的应用程序的 routes/web.php 文件中,并根据你的喜好进行修改:

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

授权码授权

通过授权码使用 OAuth2 是大多数开发人员熟悉的 OAuth2 使用方式。使用授权码时,客户端应用程序会将用户重定向到你的服务器,在那里他们将批准或拒绝向客户端颁发访问令牌的请求。

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

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

php
use Inertia\Inertia;
use Laravel\Passport\Passport;

/**
 * 引导任何应用程序服务。
 */
public function boot(): void
{
    // 通过提供视图名称...
    Passport::authorizationView('auth.oauth.authorize');

    // 通过提供闭包...
    Passport::authorizationView(
        fn ($parameters) => Inertia::render('Auth/OAuth/Authorize', [
            'request' => $parameters['request'],
            'authToken' => $parameters['authToken'],
            'client' => $parameters['client'],
            'user' => $parameters['user'],
            'scopes' => $parameters['scopes'],
        ])
    );
}

Passport 将自动定义返回此视图的 /oauth/authorize 路由。你的 auth.oauth.authorize 模板应包含一个向 passport.authorizations.approve 路由发起 POST 请求以批准授权的表单,以及一个向 passport.authorizations.deny 路由发起 DELETE 请求以拒绝授权的表单。passport.authorizations.approvepassport.authorizations.deny 路由需要 stateclient_idauth_token 字段。

管理客户端

构建需要与你的应用程序 API 交互的应用程序的开发人员需要通过创建“客户端”来将其应用程序注册到你的应用程序。通常,这包括提供其应用程序的名称以及用户批准其授权请求后你的应用程序可以重定向到的 URI。

第一方客户端

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

shell
php artisan passport:client

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

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

第三方客户端

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

php
use App\Models\User;
use Laravel\Passport\ClientRepository;

$user = User::find($userId);

// 创建属于给定用户的 OAuth 应用程序客户端...
$client = app(ClientRepository::class)->createAuthorizationCodeGrantClient(
    user: $user,
    name: 'Example App',
    redirectUris: ['https://third-party-app.com/callback'],
    confidential: false,
    enableDeviceFlow: true
);

// 检索属于该用户的所有 OAuth 应用程序客户端...
$clients = $user->oauthApps()->get();

createAuthorizationCodeGrantClient 方法返回一个 Laravel\Passport\Client 实例。你可以向用户显示 $client->id 作为客户端 ID,显示 $client->plainSecret 作为客户端密钥。

请求令牌

重定向以进行授权

创建客户端后,开发人员可以使用其客户端 ID 和密钥向你的应用程序请求授权码和访问令牌。首先,消费应用程序应向你的应用程序的 /oauth/authorize 路由发起重定向请求,如下所示:

php
use Illuminate\Http\Request;
use Illuminate\Support\Str;

Route::get('/redirect', function (Request $request) {
    $request->session()->put('state', $state = Str::random(40));

    $query = http_build_query([
        'client_id' => 'your-client-id',
        'redirect_uri' => 'https://third-party-app.com/callback',
        'response_type' => 'code',
        'scope' => 'user:read orders:create',
        'state' => $state,
        // 'prompt' => '', // "none", "consent", or "login"
    ]);

    return redirect('https://passport-app.test/oauth/authorize?'.$query);
});

prompt 参数可用于指定 Passport 应用程序的认证行为。

如果 prompt 值为 none,如果用户尚未在 Passport 应用程序中进行认证,Passport 将始终抛出认证错误。如果值为 consent,Passport 将始终显示授权批准屏幕,即使之前已将所有范围授予消费应用程序。当值为 login 时,Passport 应用程序将始终提示用户重新登录应用程序,即使他们已经有一个现有会话。

如果未提供 prompt 值,仅当用户之前未授权消费应用程序访问请求的范围时,才会提示用户授权。

NOTE

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

批准请求

当接收到授权请求时,Passport 将根据 prompt 参数的值(如果存在)自动响应,并可能向用户显示一个模板,允许他们批准或拒绝授权请求。如果他们批准请求,他们将被重定向回消费应用程序指定的 redirect_uriredirect_uri 必须与创建客户端时指定的 redirect URL 匹配。

有时你可能希望跳过授权提示,例如在授权第一方客户端时。你可以通过扩展 Client 模型并定义一个 skipsAuthorization 方法来实现。如果 skipsAuthorization 返回 true,则客户端将被批准,并且用户将立即被重定向回 redirect_uri,除非消费应用程序在重定向授权时显式设置了 prompt 参数:

php
<?php

namespace App\Models\Passport;

use Illuminate\Contracts\Auth\Authenticatable;
use Laravel\Passport\Client as BaseClient;

class Client extends BaseClient
{
    /**
     * 确定客户端是否应跳过授权提示。
     *
     * @param  \Laravel\Passport\Scope[]  $scopes
     */
    public function skipsAuthorization(Authenticatable $user, array $scopes): bool
    {
        return $this->firstParty();
    }
}

将授权码转换为访问令牌

如果用户批准授权请求,他们将被重定向回消费应用程序。消费者应首先根据重定向之前存储的值验证 state 参数。如果状态参数匹配,则消费者应向你的应用程序发起 POST 请求以请求访问令牌。该请求应包括用户批准授权请求时你的应用程序颁发的授权码:

php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;

Route::get('/callback', function (Request $request) {
    $state = $request->session()->pull('state');

    throw_unless(
        strlen($state) > 0 && $state === $request->state,
        InvalidArgumentException::class,
        'Invalid state value.'
    );

    $response = Http::asForm()->post('https://passport-app.test/oauth/token', [
        'grant_type' => 'authorization_code',
        'client_id' => 'your-client-id',
        'client_secret' => 'your-client-secret',
        'redirect_uri' => 'https://third-party-app.com/callback',
        'code' => $request->code,
    ]);

    return $response->json();
});

/oauth/token 路由将返回一个包含 access_tokenrefresh_tokenexpires_in 属性的 JSON 响应。expires_in 属性包含访问令牌过期前的秒数。

NOTE

/oauth/authorize 路由一样,/oauth/token 路由已由 Passport 为你定义。无需手动定义此路由。

管理令牌

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

php
use App\Models\User;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Date;
use Laravel\Passport\Token;

$user = User::find($userId);

// 检索该用户的所有有效令牌...
$tokens = $user->tokens()
    ->where('revoked', false)
    ->where('expires_at', '>', Date::now())
    ->get();

// 检索用户与第三方 OAuth 应用程序客户端的所有连接...
$connections = $tokens->load('client')
    ->reject(fn (Token $token) => $token->client->firstParty())
    ->groupBy('client_id')
    ->map(fn (Collection $tokens) => [
        'client' => $tokens->first()->client,
        'scopes' => $tokens->pluck('scopes')->flatten()->unique()->values()->all(),
        'tokens_count' => $tokens->count(),
    ])
    ->values();

刷新令牌

如果你的应用程序颁发短期的访问令牌,用户将需要通过颁发访问令牌时提供给他们的刷新令牌来刷新其访问令牌:

php
use Illuminate\Support\Facades\Http;

$response = Http::asForm()->post('https://passport-app.test/oauth/token', [
    'grant_type' => 'refresh_token',
    'refresh_token' => 'the-refresh-token',
    'client_id' => 'your-client-id',
    'client_secret' => 'your-client-secret', // 仅限机密客户端需要...
    'scope' => 'user:read orders:create',
]);

return $response->json();

/oauth/token 路由将返回一个包含 access_tokenrefresh_tokenexpires_in 属性的 JSON 响应。expires_in 属性包含访问令牌过期前的秒数。

撤销令牌

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

php
use Laravel\Passport\Passport;
use Laravel\Passport\Token;

$token = Passport::token()->find($tokenId);

// 撤销访问令牌...
$token->revoke();

// 撤销令牌的刷新令牌...
$token->refreshToken?->revoke();

// 撤销用户的所有令牌...
User::find($userId)->tokens()->each(function (Token $token) {
    $token->revoke();
    $token->refreshToken?->revoke();
});

清除令牌

当令牌已被撤销或过期时,你可能希望将它们从数据库中清除。Passport 包含的 passport:purge Artisan 命令可以为你执行此操作:

shell
# 清除已撤销和过期的令牌、授权码和设备码...
php artisan passport:purge

# 仅清除过期超过 6 小时的令牌...
php artisan passport:purge --hours=6

# 仅清除已撤销的令牌、授权码和设备码...
php artisan passport:purge --revoked

# 仅清除过期的令牌、授权码和设备码...
php artisan passport:purge --expired

你也可以在应用程序的 routes/console.php 文件中配置一个计划任务,以按计划自动清除你的令牌:

php
use Illuminate\Support\Facades\Schedule;

Schedule::command('passport:purge')->hourly();

使用 PKCE 的授权码授权

带有“代码交换证明密钥”(PKCE)的授权码授权是一种安全的方式,用于认证单页应用程序或移动应用程序以访问你的 API。当你无法保证客户端密钥将被保密存储,或者为了减轻授权码被攻击者截获的威胁时,应使用此授权。在将授权码交换为访问令牌时,“代码验证器”和“代码挑战”的组合取代了客户端密钥。

创建客户端

在你的应用程序可以通过带有 PKCE 的授权码授权颁发令牌之前,你需要创建一个启用了 PKCE 的客户端。你可以使用带有 --public 选项的 passport:client Artisan 命令来执行此操作:

shell
php artisan passport:client --public

请求令牌

代码验证器和代码挑战

由于此授权授权不提供客户端密钥,开发人员需要生成代码验证器和代码挑战的组合来请求令牌。

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

代码挑战应是一个 Base64 编码的字符串,包含 URL 和文件名安全字符。应删除尾部的 '=' 字符,并且不应存在换行符、空白或其他额外字符。

php
$encoded = base64_encode(hash('sha256', $codeVerifier, true));

$codeChallenge = strtr(rtrim($encoded, '='), '+/', '-_');

重定向以进行授权

创建客户端后,你可以使用客户端 ID 以及生成的代码验证器和代码挑战向你的应用程序请求授权码和访问令牌。首先,消费应用程序应向你的应用程序的 /oauth/authorize 路由发起重定向请求:

php
use Illuminate\Http\Request;
use Illuminate\Support\Str;

Route::get('/redirect', function (Request $request) {
    $request->session()->put('state', $state = Str::random(40));

    $request->session()->put(
        'code_verifier', $codeVerifier = Str::random(128)
    );

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

    $query = http_build_query([
        'client_id' => 'your-client-id',
        'redirect_uri' => 'https://third-party-app.com/callback',
        'response_type' => 'code',
        'scope' => 'user:read orders:create',
        'state' => $state,
        'code_challenge' => $codeChallenge,
        'code_challenge_method' => 'S256',
        // 'prompt' => '', // "none", "consent", or "login"
    ]);

    return redirect('https://passport-app.test/oauth/authorize?'.$query);
});

将授权码转换为访问令牌

如果用户批准授权请求,他们将被重定向回消费应用程序。与标准的授权码授权一样,消费者应根据重定向之前存储的值验证 state 参数。

如果状态参数匹配,则消费者应向你的应用程序发起 POST 请求以请求访问令牌。该请求应包括用户批准授权请求时你的应用程序颁发的授权码以及最初生成的代码验证器:

php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;

Route::get('/callback', function (Request $request) {
    $state = $request->session()->pull('state');

    $codeVerifier = $request->session()->pull('code_verifier');

    throw_unless(
        strlen($state) > 0 && $state === $request->state,
        InvalidArgumentException::class
    );

    $response = Http::asForm()->post('https://passport-app.test/oauth/token', [
        'grant_type' => 'authorization_code',
        'client_id' => 'your-client-id',
        'redirect_uri' => 'https://third-party-app.com/callback',
        'code_verifier' => $codeVerifier,
        'code' => $request->code,
    ]);

    return $response->json();
});

设备授权授权

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

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

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

php
use Inertia\Inertia;
use Laravel\Passport\Passport;

/**
 * 引导任何应用程序服务。
 */
public function boot(): void
{
    // 通过提供视图名称...
    Passport::deviceUserCodeView('auth.oauth.device.user-code');
    Passport::deviceAuthorizationView('auth.oauth.device.authorize');

    // 通过提供闭包...
    Passport::deviceUserCodeView(
        fn ($parameters) => Inertia::render('Auth/OAuth/Device/UserCode')
    );

    Passport::deviceAuthorizationView(
        fn ($parameters) => Inertia::render('Auth/OAuth/Device/Authorize', [
            'request' => $parameters['request'],
            'authToken' => $parameters['authToken'],
            'client' => $parameters['client'],
            'user' => $parameters['user'],
            'scopes' => $parameters['scopes'],
        ])
    );

    // ...
}

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 和密钥:

shell
php artisan passport:client --device

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

php
use App\Models\User;
use Laravel\Passport\ClientRepository;

$user = User::find($userId);

$client = app(ClientRepository::class)->createDeviceAuthorizationGrantClient(
    user: $user,
    name: 'Example Device',
    confidential: false,
);

请求令牌

请求设备码

创建客户端后,开发人员可以使用其客户端 ID 向你的应用程序请求设备码。首先,消费设备应向你的应用程序的 /oauth/device/code 路由发起 POST 请求以请求设备码:

php
use Illuminate\Support\Facades\Http;

$response = Http::asForm()->post('https://passport-app.test/oauth/device/code', [
    'client_id' => 'your-client-id',
    'scope' => 'user:read orders:create',
]);

return $response->json();

这将返回一个包含 device_codeuser_codeverification_uriintervalexpires_in 属性的 JSON 响应。expires_in 属性包含设备码过期前的秒数。interval 属性包含消费设备在轮询 /oauth/token 路由时应等待的秒数,以避免速率限制错误。

NOTE

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

显示验证 URI 和用户码

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

轮询令牌请求

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

php
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Sleep;

$interval = 5;

do {
    Sleep::for($interval)->seconds();

    $response = Http::asForm()->post('https://passport-app.test/oauth/token', [
        'grant_type' => 'urn:ietf:params:oauth:grant-type:device_code',
        'client_id' => 'your-client-id',
        'client_secret' => 'your-client-secret', // 仅限机密客户端需要...
        'device_code' => 'the-device-code',
    ]);

    if ($response->json('error') === 'slow_down') {
        $interval += 5;
    }
} while (in_array($response->json('error'), ['authorization_pending', 'slow_down']));

return $response->json();

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

密码授权

WARNING

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

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

要启用密码授权,请在应用程序的 App\Providers\AppServiceProvider 类的 boot 方法中调用 enablePasswordGrant 方法:

php
/**
 * 引导任何应用程序服务。
 */
public function boot(): void
{
    Passport::enablePasswordGrant();
}

创建密码授权客户端

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

shell
php artisan passport:client --password

请求令牌

一旦你启用了该授权并创建了密码授权客户端,你就可以通过向 /oauth/token 路由发起包含用户电子邮件地址和密码的 POST 请求来请求访问令牌。请记住,此路由已由 Passport 注册,因此无需手动定义它。如果请求成功,你将收到服务器 JSON 响应中的 access_tokenrefresh_token

php
use Illuminate\Support\Facades\Http;

$response = Http::asForm()->post('https://passport-app.test/oauth/token', [
    'grant_type' => 'password',
    'client_id' => 'your-client-id',
    'client_secret' => 'your-client-secret', // 仅限机密客户端需要...
    'username' => 'taylor@laravel.com',
    'password' => 'my-password',
    'scope' => 'user:read orders:create',
]);

return $response->json();

NOTE

请记住,访问令牌默认是长期的。但是,如果需要,你可以自由地配置你的最大访问令牌有效期

请求所有范围

使用密码授权或客户端凭证授权时,你可能希望为令牌授权你的应用程序支持的所有范围。你可以通过请求 * 范围来实现。如果你请求 * 范围,令牌实例上的 can 方法将始终返回 true。此范围只能分配给使用 passwordclient_credentials 授权颁发的令牌:

php
use Illuminate\Support\Facades\Http;

$response = Http::asForm()->post('https://passport-app.test/oauth/token', [
    'grant_type' => 'password',
    'client_id' => 'your-client-id',
    'client_secret' => 'your-client-secret', // 仅限机密客户端需要...
    'username' => 'taylor@laravel.com',
    'password' => 'my-password',
    'scope' => '*',
]);

自定义用户提供器

如果你的应用程序使用多个认证用户提供器,你可以通过在通过 artisan passport:client --password 命令创建客户端时提供 --provider 选项来指定密码授权客户端使用的用户提供器。给定的提供器名称应与应用程序的 config/auth.php 配置文件中定义的有效提供器匹配。然后,你可以使用中间件保护你的路由,以确保只有来自守卫指定提供器的用户被授权。

自定义用户名字段

使用密码授权进行认证时,Passport 将使用你的可认证模型的 email 属性作为“用户名”。但是,你可以通过在模型上定义一个 findForPassport 方法来自定义此行为:

php
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Passport\Bridge\Client;
use Laravel\Passport\Contracts\OAuthenticatable;
use Laravel\Passport\HasApiTokens;

class User extends Authenticatable implements OAuthenticatable
{
    use HasApiTokens, Notifiable;

    /**
     * 为给定的用户名查找用户实例。
     */
    public function findForPassport(string $username, Client $client): User
    {
        return $this->where('username', $username)->first();
    }
}

自定义密码验证

使用密码授权进行认证时,Passport 将使用模型的 password 属性来验证给定的密码。如果你的模型没有 password 属性,或者你希望自定义密码验证逻辑,你可以在模型上定义一个 validateForPassportPasswordGrant 方法:

php
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Hash;
use Laravel\Passport\Contracts\OAuthenticatable;
use Laravel\Passport\HasApiTokens;

class User extends Authenticatable implements OAuthenticatable
{
    use HasApiTokens, Notifiable;

    /**
     * 为 Passport 密码授权验证用户密码。
     */
    public function validateForPassportPasswordGrant(string $password): bool
    {
        return Hash::check($password, $this->password);
    }
}

隐式授权

WARNING

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

隐式授权与授权码授权类似;但是,令牌返回给客户端而无需交换授权码。此授权最常用于 JavaScript 或移动应用程序,其中客户端凭据无法安全存储。要启用该授权,请在应用程序的 App\Providers\AppServiceProvider 类的 boot 方法中调用 enableImplicitGrant 方法:

php
/**
 * 引导任何应用程序服务。
 */
public function boot(): void
{
    Passport::enableImplicitGrant();
}

在你的应用程序可以通过隐式授权颁发令牌之前,你需要创建一个隐式授权客户端。你可以使用带有 --implicit 选项的 passport:client Artisan 命令来执行此操作。

shell
php artisan passport:client --implicit

一旦授权被启用并且隐式客户端被创建,开发人员就可以使用其客户端 ID 向你的应用程序请求访问令牌。消费应用程序应向你的应用程序的 /oauth/authorize 路由发起重定向请求,如下所示:

php
use Illuminate\Http\Request;

Route::get('/redirect', function (Request $request) {
    $request->session()->put('state', $state = Str::random(40));

    $query = http_build_query([
        'client_id' => 'your-client-id',
        'redirect_uri' => 'https://third-party-app.com/callback',
        'response_type' => 'token',
        'scope' => 'user:read orders:create',
        'state' => $state,
        // 'prompt' => '', // "none", "consent", or "login"
    ]);

    return redirect('https://passport-app.test/oauth/authorize?'.$query);
});

NOTE

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

客户端凭证授权

客户端凭证授权适用于机器对机器认证。例如,你可以在通过 API 执行维护任务的计划任务中使用此授权。

在你的应用程序可以通过客户端凭证授权颁发令牌之前,你需要创建一个客户端凭证授权客户端。你可以使用 passport:client Artisan 命令的 --client 选项来执行此操作:

shell
php artisan passport:client --client

接下来,将 Laravel\Passport\Http\Middleware\EnsureClientIsResourceOwner 中间件分配给一个路由:

php
use Laravel\Passport\Http\Middleware\EnsureClientIsResourceOwner;

Route::get('/orders', function (Request $request) {
    // 访问令牌有效且客户端是资源所有者...
})->middleware(EnsureClientIsResourceOwner::class);

要限制对路由的访问到特定的范围,你可以向 using 方法提供一个所需范围的列表:

php
Route::get('/orders', function (Request $request) {
    // 访问令牌有效,客户端是资源所有者,并且同时具有 "servers:read" 和 "servers:create" 范围...
})->middleware(EnsureClientIsResourceOwner::using('servers:read', 'servers:create'));

WARNING

底层的 OAuth2 服务器 在生成客户端凭据令牌时,会将令牌中的 sub 声明设置为客户端的标识符。默认情况下,Passport 使用 UUID 作为客户端标识符,因此不会与用户的整数主键产生冲突。但是,如果你将 Passport::$clientUuids 设置为 false,则客户端凭据令牌可能会错误地匹配到与客户端 ID 相同的用户 ID。在这种情况下,使用该中间件无法保证传入的令牌一定是客户端凭据令牌。

检索令牌

要使用此授权类型检索令牌,请向 oauth/token 端点发出请求:

php
use Illuminate\Support\Facades\Http;

$response = Http::asForm()->post('https://passport-app.test/oauth/token', [
    'grant_type' => 'client_credentials',
    'client_id' => 'your-client-id',
    'client_secret' => 'your-client-secret',
    'scope' => 'servers:read servers:create',
]);

return $response->json()['access_token'];

个人访问令牌

有时,你的用户可能希望在不经历典型授权码重定向流程的情况下为自己颁发访问令牌。允许用户通过你的应用程序 UI 为自己颁发令牌对于允许用户试验你的 API 可能很有用,或者可以作为颁发访问令牌的更简单方法。

NOTE

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

创建个人访问客户端

在你的应用程序可以颁发个人访问令牌之前,你需要创建一个个人访问客户端。你可以通过执行带有 --personal 选项的 passport:client Artisan 命令来执行此操作。如果你已经运行了 passport:install 命令,则无需运行此命令:

shell
php artisan passport:client --personal

自定义用户提供器

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

管理个人访问令牌

一旦你创建了一个个人访问客户端,你就可以使用 App\Models\User 模型实例上的 createToken 方法为给定用户颁发令牌。createToken 方法接受令牌名称作为其第一个参数,并接受一个可选的范围数组作为其第二个参数:

php
use App\Models\User;
use Illuminate\Support\Facades\Date;
use Laravel\Passport\Token;

$user = User::find($userId);

// 创建没有范围的令牌...
$token = $user->createToken('My Token')->accessToken;

// 创建带有范围的令牌...
$token = $user->createToken('My Token', ['user:read', 'orders:create'])->accessToken;

// 创建带有所有范围的令牌...
$token = $user->createToken('My Token', ['*'])->accessToken;

// 检索属于该用户的所有有效个人访问令牌...
$tokens = $user->tokens()
    ->with('client')
    ->where('revoked', false)
    ->where('expires_at', '>', Date::now())
    ->get()
    ->filter(fn (Token $token) => $token->client->hasGrantType('personal_access'));

保护路由

通过中间件

Passport 包含一个认证守卫,它将验证传入请求上的访问令牌。一旦你配置了 api 守卫使用 passport 驱动,你只需在任何需要有效访问令牌的路由上指定 auth:api 中间件:

php
Route::get('/user', function () {
    // 只有通过 API 认证的用户可以访问此路由...
})->middleware('auth:api');

WARNING

如果你正在使用客户端凭证授权,你应该使用Laravel\Passport\Http\Middleware\EnsureClientIsResourceOwner 中间件来保护你的路由,而不是 auth:api 中间件。

多个认证守卫

如果你的应用程序认证不同类型的用户(可能使用完全不同的 Eloquent 模型),你可能需要在应用程序中为每种用户提供器类型定义守卫配置。这允许你保护针对特定用户提供器的请求。例如,给定 config/auth.php 配置文件中的以下守卫配置:

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

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

以下路由将利用 api-customers 守卫,该守卫使用 customers 用户提供器,来认证传入请求:

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

NOTE

有关将多个用户提供器与 Passport 一起使用的更多信息,请查阅个人访问令牌文档密码授权文档

传递访问令牌

在调用受 Passport 保护的路由时,你的应用程序的 API 消费者应在其请求的 Authorization 标头中将其访问令牌指定为 Bearer 令牌。例如,使用 Http Facade 时:

php
use Illuminate\Support\Facades\Http;

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

return $response->json();

令牌范围

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

定义范围

你可以使用 Passport::tokensCan 方法在应用程序的 App\Providers\AppServiceProvider 类的 boot 方法中定义你的 API 的范围。tokensCan 方法接受范围名称和范围描述的数组。范围描述可以是任何你想要的内容,并将显示在授权批准屏幕上给用户:

php
/**
 * 引导任何应用程序服务。
 */
public function boot(): void
{
    Passport::tokensCan([
        'user:read' => '检索用户信息',
        'orders:create' => '下单',
        'orders:read:status' => '检查订单状态',
    ]);
}

默认范围

如果客户端未请求任何特定范围,你可以配置你的 Passport 服务器使用 defaultScopes 方法将默认范围附加到令牌。通常,你应该在应用程序的 App\Providers\AppServiceProvider 类的 boot 方法中调用此方法:

php
use Laravel\Passport\Passport;

Passport::tokensCan([
    'user:read' => '检索用户信息',
    'orders:create' => '下单',
    'orders:read:status' => '检查订单状态',
]);

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

将范围分配给令牌

请求授权码时

使用授权码授权请求访问令牌时,消费者应将其所需的范围指定为 scope 查询字符串参数。scope 参数应是一个空格分隔的范围列表:

php
Route::get('/redirect', function () {
    $query = http_build_query([
        'client_id' => 'your-client-id',
        'redirect_uri' => 'https://third-party-app.com/callback',
        'response_type' => 'code',
        'scope' => 'user:read orders:create',
    ]);

    return redirect('https://passport-app.test/oauth/authorize?'.$query);
});

颁发个人访问令牌时

如果你正在使用 App\Models\User 模型的 createToken 方法颁发个人访问令牌,你可以将所需范围的数组作为第二个参数传递给该方法:

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

检查范围

Passport 包含两个中间件,可用于验证传入请求是否使用已被授予给定范围的令牌进行了认证。

检查所有范围

Laravel\Passport\Http\Middleware\CheckToken 中间件可以分配给一个路由,以验证传入请求的访问令牌是否具有所有列出的范围:

php
use Laravel\Passport\Http\Middleware\CheckToken;

Route::get('/orders', function () {
    // 访问令牌同时具有 "orders:read" 和 "orders:create" 范围...
})->middleware(['auth:api', CheckToken::using('orders:read', 'orders:create')]);

检查任何范围

Laravel\Passport\Http\Middleware\CheckTokenForAnyScope 中间件可以分配给一个路由,以验证传入请求的访问令牌是否具有列出的范围至少一个

php
use Laravel\Passport\Http\Middleware\CheckTokenForAnyScope;

Route::get('/orders', function () {
    // 访问令牌具有 "orders:read" 或 "orders:create" 范围...
})->middleware(['auth:api', CheckTokenForAnyScope::using('orders:read', 'orders:create')]);

范围属性

如果您的应用程序使用了控制器中间件属性,则可以使用 Laravel\Passport\Attributes\AuthorizeToken 属性作为 Passport 范围中间件的便捷快捷方式:

php
<?php

namespace App\Http\Controllers;

use Laravel\Passport\Attributes\AuthorizeToken;

#[AuthorizeToken('orders:read')]
#[AuthorizeToken('orders:create', only: ['store'])]
class OrderController
{
    #[AuthorizeToken(['orders:read', 'orders:create'], anyScope: true)]
    public function index()
    {
        // 访问令牌具有“orders:read”或“orders:create”范围之一...
    }

    public function store()
    {
        // 访问令牌同时具有“orders:read”和“orders:create”范围...
    }
}

默认情况下,AuthorizeToken 属性要求所有给定的范围。如果传递 anyScope: true,则当令牌至少具有一个给定范围时,请求即被授权。

在令牌实例上检查范围

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

php
use Illuminate\Http\Request;

Route::get('/orders', function (Request $request) {
    if ($request->user()->tokenCan('orders:create')) {
        // ...
    }
});

附加范围方法

scopeIds 方法将返回所有已定义 ID/名称的数组:

php
use Laravel\Passport\Passport;

Passport::scopeIds();

scopes 方法将返回所有已定义范围的数组,作为 Laravel\Passport\Scope 的实例:

php
Passport::scopes();

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

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

你可以使用 hasScope 方法确定是否定义了给定范围:

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

SPA 认证

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

通常,如果你希望从你的 JavaScript 应用程序消费你的 API,你需要手动向应用程序发送一个访问令牌,并在每次请求时将其传递给你的应用程序。但是,Passport 包含一个可以为你处理此问题的中间件。你所需要做的就是在应用程序的 bootstrap/app.php 文件中将 CreateFreshApiToken 中间件附加到 web 中间件组:

php
use Laravel\Passport\Http\Middleware\CreateFreshApiToken;

->withMiddleware(function (Middleware $middleware): void {
    $middleware->web(append: [
        CreateFreshApiToken::class,
    ]);
})

WARNING

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

此中间件将向你的传出响应附加一个 laravel_token cookie。此 cookie 包含一个加密的 JWT,Passport 将使用它来认证来自你的 JavaScript 应用程序的 API 请求。JWT 的有效期等于你的 session.lifetime 配置值。现在,由于浏览器将自动在后续所有请求中发送此 cookie,你可以向你的应用程序的 API 发出请求而无需显式传递访问令牌:

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

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

php
/**
 * 引导任何应用程序服务。
 */
public function boot(): void
{
    Passport::cookie('custom_name');
}

CSRF 保护

使用这种认证方法时,你需要确保请求中包含一个有效的 CSRF 令牌标头。骨架应用程序中包含的默认 Laravel JavaScript 脚手架以及所有启动套件都包含一个 Axios 实例,它将自动使用加密的 XSRF-TOKEN cookie 值在同源请求上发送 X-XSRF-TOKEN 标头。

NOTE

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

事件

Passport 在颁发访问令牌和刷新令牌时会引发事件。你可以监听这些事件以在你的数据库中清除或撤销其他访问令牌:

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

测试

Passport 的 actingAs 方法可用于指定当前已验证的用户及其范围。传递给 actingAs 方法的第一个参数是用户实例,第二个参数是应授予用户令牌的范围数组:

php
use App\Models\User;
use Laravel\Passport\Passport;

test('订单可以被创建', function () {
    Passport::actingAs(
        User::factory()->create(),
        ['orders:create']
    );

    $response = $this->post('/api/orders');

    $response->assertStatus(201);
});
php
use App\Models\User;
use Laravel\Passport\Passport;

public function test_orders_can_be_created(): void
{
    Passport::actingAs(
        User::factory()->create(),
        ['orders:create']
    );

    $response = $this->post('/api/orders');

    $response->assertStatus(201);
}

Passport 的 actingAsClient 方法可用于指定当前已验证的客户端及其范围。传递给 actingAsClient 方法的第一个参数是客户端实例,第二个参数是应授予客户端令牌的范围数组:

php
use Laravel\Passport\Client;
use Laravel\Passport\Passport;

test('服务器可以被检索', function () {
    Passport::actingAsClient(
        Client::factory()->create(),
        ['servers:read']
    );

    $response = $this->get('/api/servers');

    $response->assertStatus(200);
});
php
use Laravel\Passport\Client;
use Laravel\Passport\Passport;

public function test_servers_can_be_retrieved(): void
{
    Passport::actingAsClient(
        Client::factory()->create(),
        ['servers:read']
    );

    $response = $this->get('/api/servers');

    $response->assertStatus(200);
}