Skip to content

Laravel Passport ​

简介 ​

Laravel Passport 为您的 Laravel 应用程序提供完整的 OAuth2 服务器实现,只需几分钟。Passport 基于 League OAuth2 服务器,由 Andy Millington 和 Simon Hamp 维护。

WARNING

本文档假设您已经熟悉 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 客户端和访问令牌。该命令还将创建生成安全访问令牌所需的加密密钥。

此外,此命令将询问您是否要使用 UUID 作为 Passport Client 模型的主键值,而不是自动递增的整数。

运行 install:api 命令后,将 Laravel\Passport\HasApiTokens trait 添加到您的 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\HasApiTokens;

    class User extends Authenticatable
    {
        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
    /**
     * Bootstrap any application services.
     */
    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 的新主要版本时,重要的是您仔细阅读 升级指南。

配置 ​

客户端密钥哈希 ​

如果您希望在数据库中存储客户端的密钥时对其进行哈希处理,您应该在应用程序的 App\Providers\AppServiceProvider 类的 boot 方法中调用 Passport::hashClientSecrets 方法:

php
    use Laravel\Passport\Passport;

    Passport::hashClientSecrets();

启用后,所有客户端密钥只会在创建后立即显示给用户。由于纯文本客户端密钥值不会存储在数据库中,因此如果密钥丢失,则无法恢复密钥的值。

令牌生命周期 ​

默认情况下,Passport 发放的访问令牌有效期为一年。如果您想配置更长/更短的令牌生命周期,您可以在应用程序的 App\Providers\AppServiceProvider 类的 boot 方法中使用 tokensExpireIn、refreshTokensExpireIn 和 personalAccessTokensExpireIn 方法。这些方法应该从您应用程序的 App\Providers\AppServiceProvider 类的 boot 方法中调用:

php
    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        Passport::tokensExpireIn(now()->addDays(15));
        Passport::refreshTokensExpireIn(now()->addDays(30));
        Passport::personalAccessTokensExpireIn(now()->addMonths(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\PersonalAccessClient;
    use App\Models\Passport\RefreshToken;
    use App\Models\Passport\Token;

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

覆盖路由 ​

有时您可能希望自定义 Passport 定义的路由。要实现这一点,您首先需要忽略 Passport 注册的路由,方法是在应用程序的 AppServiceProvider 的 register 方法中添加 Passport::ignoreRoutes:

php
    use Laravel\Passport\Passport;

    /**
     * Register any application services.
     */
    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 routes...
    });

发放访问令牌 ​

使用授权码的 OAuth2 是大多数开发人员熟悉 OAuth2 的方式。使用授权码时,客户端应用程序将重定向用户到您的服务器,用户将批准或拒绝向客户端发放访问令牌的请求。

管理客户端 ​

首先,开发人员构建需要与您的应用程序 API 交互的应用程序时,他们将需要通过创建 "客户端" 来向您的应用程序注册他们的应用程序。通常,这包括提供他们应用程序的名称和一个 URL,您的应用程序可以在用户批准授权后重定向到该 URL。

passport:client 命令 ​

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

shell
php artisan passport:client

重定向 URL

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

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

JSON API ​

由于您的应用程序用户将无法使用 client 命令,Passport 提供了一个 JSON API,您可以使用它来创建客户端。这可以避免您手动编写用于创建、更新和删除客户端的控制器的麻烦。

但是,您将需要将 Passport 的 JSON API 与您自己的前端配对,为您的用户提供一个仪表板,让他们管理他们的客户端。下面,我们将回顾所有用于管理客户端的 API 端点。为方便起见,我们将使用 Axios 演示如何向这些端点发出 HTTP 请求。

JSON API 受 web 和 auth 中间件保护;因此,它只能从您自己的应用程序调用。它无法从外部源调用。

GET /oauth/clients ​

此路由返回经过身份验证的用户的所有客户端。这主要用于列出用户的所有客户端,以便他们可以编辑或删除它们:

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

POST /oauth/clients ​

此路由用于创建新客户端。它需要两个数据:客户端的 name 和一个 redirect URL。redirect URL 是用户批准授权请求后将重定向到的 URL。

创建客户端时,它将被发放一个客户端 ID 和客户端密钥。这些值将在向您的应用程序请求访问令牌时使用。客户端创建路由将返回新的客户端实例:

js
const data = {
    name: 'Client Name',
    redirect: 'http://example.com/callback'
};

axios.post('/oauth/clients', data)
    .then(response => {
        console.log(response.data);
    })
    .catch (response => {
        // List errors on response...
    });

PUT /oauth/clients/{client-id} ​

此路由用于更新客户端。它需要两个数据:客户端的 name 和一个 redirect URL。redirect URL 是用户批准授权请求后将重定向到的 URL。该路由将返回更新后的客户端实例:

js
const data = {
    name: 'New Client Name',
    redirect: 'http://example.com/callback'
};

axios.put('/oauth/clients/' + clientId, data)
    .then(response => {
        console.log(response.data);
    })
    .catch (response => {
        // List errors on response...
    });

DELETE /oauth/clients/{client-id} ​

此路由用于删除客户端:

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

请求令牌 ​

重定向进行授权 ​

一旦创建了客户端,开发人员就可以使用他们的客户端 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' => 'client-id',
            'redirect_uri' => 'http://third-party-app.com/callback',
            'response_type' => 'code',
            'scope' => '',
            'state' => $state,
            // 'prompt' => '', // "none", "consent", or "login"
        ]);

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

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

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

如果未提供 prompt 值,则只有当用户之前未为请求的范围授予访问权限时,才会提示用户进行授权。

NOTE

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

批准请求 ​

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

如果您想自定义授权批准屏幕,您可以使用 vendor:publish Artisan 命令发布 Passport 的视图。发布的视图将放置在 resources/views/vendor/passport 目录中:

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

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

php
    <?php

    namespace App\Models\Passport;

    use Laravel\Passport\Client as BaseClient;

    class Client extends BaseClient
    {
        /**
         * Determine if the client should skip the authorization prompt.
         */
        public function skipsAuthorization(): bool
        {
            return $this->firstParty();
        }
    }

将授权码转换为访问令牌 ​

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

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('http://passport-app.test/oauth/token', [
            'grant_type' => 'authorization_code',
            'client_id' => 'client-id',
            'client_secret' => 'client-secret',
            'redirect_uri' => 'http://third-party-app.com/callback',
            'code' => $request->code,
        ]);

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

此 /oauth/token 路由将返回一个 JSON 响应,其中包含 access_token、refresh_token 和 expires_in 属性。expires_in 属性包含访问令牌到期前的秒数。

NOTE

与 /oauth/authorize 路由一样,/oauth/token 路由也由 Passport 定义。您无需手动定义此路由。

JSON API ​

Passport 还包括一个 JSON API,用于管理已授权的访问令牌。您可以将其与您自己的前端配对,为您的用户提供一个仪表板,让他们管理访问令牌。为方便起见,我们将使用 Axios 演示如何向这些端点发出 HTTP 请求。JSON API 受 web 和 auth 中间件保护;因此,它只能从您自己的应用程序调用。

GET /oauth/tokens ​

此路由返回经过身份验证的用户创建的所有已授权访问令牌。这主要用于列出用户的所有令牌,以便他们可以撤销它们:

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

DELETE /oauth/tokens/{token-id} ​

此路由可用于撤销已授权的访问令牌及其相关的刷新令牌:

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

刷新令牌 ​

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

php
    use Illuminate\Support\Facades\Http;

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

    return $response->json();

此 /oauth/token 路由将返回一个 JSON 响应,其中包含 access_token、refresh_token 和 expires_in 属性。expires_in 属性包含访问令牌到期前的秒数。

撤销令牌 ​

您可以使用 Laravel\Passport\TokenRepository 上的 revokeAccessToken 方法撤销令牌。您还可以使用 Laravel\Passport\RefreshTokenRepository 上的 revokeRefreshTokensByAccessTokenId 方法撤销令牌的刷新令牌。这些类可以使用 Laravel 的 服务容器 解析:

php
    use Laravel\Passport\TokenRepository;
    use Laravel\Passport\RefreshTokenRepository;

    $tokenRepository = app(TokenRepository::class);
    $refreshTokenRepository = app(RefreshTokenRepository::class);

    // Revoke an access token...
    $tokenRepository->revokeAccessToken($tokenId);

    // Revoke all of the token's refresh tokens...
    $refreshTokenRepository->revokeRefreshTokensByAccessTokenId($tokenId);

清除令牌 ​

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

shell
# Purge revoked and expired tokens and auth codes...
php artisan passport:purge

# Only purge tokens expired for more than 6 hours...
php artisan passport:purge --hours=6

# Only purge revoked tokens and auth codes...
php artisan passport:purge --revoked

# Only purge expired tokens and auth codes...
php artisan passport:purge --expired

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

php
    use Laravel\Support\Facades\Schedule;

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

带 PKCE 的授权码授权 ​

授权码授权与 "Proof Key for Code Exchange" (PKCE) 是一种安全的方式,可以让单页应用程序或本机应用程序访问您的 API。当您无法保证客户端密钥将被保密存储或者要减轻授权码被拦截的威胁时,应使用此授权。代码验证器和代码挑战替换了在交换授权码以获取访问令牌时使用的客户端密钥。

创建客户端 ​

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

shell
php artisan passport:client --public

请求令牌 ​

代码验证器和代码挑战 ​

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

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

代码挑战应为 Base64 编码的字符串,其中包含 URL 和文件名安全字符。应删除尾随的 '=' 字符,不应存在任何换行符、空白字符或其他附加字符。

php
    $encoded = base64_encode(hash('sha256', $code_verifier, 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', $code_verifier = Str::random(128)
        );

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

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

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

将授权码转换为访问令牌 ​

如果用户批准授权请求,他们将被重定向回消费应用程序。消费者应验证 state 参数是否与存储在重定向之前的值匹配,如标准授权码授权中所述。

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

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('http://passport-app.test/oauth/token', [
            'grant_type' => 'authorization_code',
            'client_id' => 'client-id',
            'redirect_uri' => 'http://third-party-app.com/callback',
            'code_verifier' => $codeVerifier,
            'code' => $request->code,
        ]);

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

密码授权令牌 ​

WARNING

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

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

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

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

创建密码授权客户端 ​

在您的应用程序可以通过密码授权发放令牌之前,您需要创建一个密码授权客户端。您可以使用 passport:client Artisan 命令的 --password 选项来执行此操作。如果您已经运行了 passport:install 命令,则无需运行此命令:

shell
php artisan passport:client --password

请求令牌 ​

创建密码授权客户端后,您可以通过向 /oauth/token 路由发出 POST 请求来请求访问令牌,其中包含用户的电子邮件地址和密码。请记住,此路由已由 Passport 注册,因此无需手动定义它。如果请求成功,您将从服务器的 JSON 响应中收到 access_token 和 refresh_token:

php
    use Illuminate\Support\Facades\Http;

    $response = Http::asForm()->post('http://passport-app.test/oauth/token', [
        'grant_type' => 'password',
        'client_id' => 'client-id',
        'client_secret' => 'client-secret',
        'username' => 'taylor@laravel.com',
        'password' => 'my-password',
        'scope' => '',
    ]);

    return $response->json();

NOTE

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

请求所有范围 ​

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

php
    use Illuminate\Support\Facades\Http;

    $response = Http::asForm()->post('http://passport-app.test/oauth/token', [
        'grant_type' => 'password',
        'client_id' => 'client-id',
        'client_secret' => 'client-secret',
        'username' => 'taylor@laravel.com',
        'password' => 'my-password',
        'scope' => '*',
    ]);

自定义用户提供者 ​

如果您的应用程序使用多个 身份验证用户提供者,您可以通过在创建客户端时提供 --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\HasApiTokens;

    class User extends Authenticatable
    {
        use HasApiTokens, Notifiable;

        /**
         * Find the user instance for the given username.
         */
        public function findForPassport(string $username): User
        {
            return $this->where('username', $username)->first();
        }
    }

自定义密码验证 ​

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

php
    <?php

    namespace App\Models;

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

    class User extends Authenticatable
    {
        use HasApiTokens, Notifiable;

        /**
         * Validate the password of the user for the Passport password grant.
         */
        public function validateForPassportPasswordGrant(string $password): bool
        {
            return Hash::check($password, $this->password);
        }
    }

隐式授权令牌 ​

WARNING

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

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

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

启用授权后,开发人员可以使用其客户端 ID 从应用程序请求访问令牌。消费应用程序应像下面这样向应用程序的 /oauth/authorize 路由发起重定向请求:

    use Illuminate\Http\Request;

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

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

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

NOTE

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

客户端凭据授权令牌 ​

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

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

    php artisan passport:client --client

接下来,要使用此授权类型,请为 CheckClientCredentials 中间件注册一个中间件别名。您可以在应用程序的 bootstrap/app.php 文件中定义中间件别名:

    use Laravel\Passport\Http\Middleware\CheckClientCredentials;

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

然后,将中间件附加到路由:

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

要限制对特定作用域的路由访问,您可以在将 client 中间件附加到路由时提供所需作用域的逗号分隔列表:

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

检索令牌 ​

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

    use Illuminate\Support\Facades\Http;

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

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

个人访问令牌 ​

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

NOTE

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

创建个人访问客户端 ​

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

    php artisan passport:client --personal

创建个人访问客户端后,将客户端的 ID 和明文密钥值放入应用程序的 .env 文件中:

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

管理个人访问令牌 ​

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

    use App\Models\User;

    $user = User::find(1);

    // 创建没有作用域的令牌...
    $token = $user->createToken('Token Name')->accessToken;

    // 创建带有作用域的令牌...
    $token = $user->createToken('My Token', ['place-orders'])->accessToken;

JSON API ​

Passport 还包括一个用于管理个人访问令牌的 JSON API。您可以将其与自己的前端配对,为用户提供管理个人访问令牌的仪表板。下面,我们将回顾管理个人访问令牌的所有 API 端点。为了方便起见,我们将使用 Axios 来演示向端点发出 HTTP 请求。

JSON API 受到 web 和 auth 中间件的保护;因此,它只能从您自己的应用程序调用。无法从外部源调用。

GET /oauth/scopes ​

此路由返回为您的应用程序定义的所有 作用域。您可以使用此路由列出用户可以分配给个人访问令牌的作用域:

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

GET /oauth/personal-access-tokens ​

此路由返回已认证用户创建的所有个人访问令牌。这主要用于列出用户的所有令牌,以便他们可以编辑或撤销它们:

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

POST /oauth/personal-access-tokens ​

此路由创建新的个人访问令牌。它需要两个数据:令牌的 name 和应分配给令牌的 scopes:

    const data = {
        name: 'Token Name',
        scopes: []
    };

    axios.post('/oauth/personal-access-tokens', data)
        .then(response => {
            console.log(response.data.accessToken);
        })
        .catch (response => {
            // 列出响应中的错误...
        });

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

此路由可用于撤销个人访问令牌:

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

保护路由 ​

通过中间件 ​

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

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

WARNING

如果您使用的是 客户端凭据授权,则应使用 client 中间件 来保护您的路由,而不是 auth:api 中间件。

多个身份验证守卫 ​

如果您的应用程序对不同类型的用户进行身份验证,可能使用完全不同的 Eloquent 模型,您可能需要为应用程序中的每种用户提供类型定义守卫配置。这使您能够保护针对特定用户提供者的请求。例如,给定以下守卫配置,config/auth.php 配置文件:

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

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

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

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

NOTE

有关使用多个用户提供者与 Passport 的更多信息,请查阅 密码授权文档。

传递访问令牌 ​

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

    use Illuminate\Support\Facades\Http;

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

    return $response->json();

令牌作用域 ​

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

定义作用域 ​

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

    /**
     * 启动任何应用程序服务。
     */
    public function boot(): void
    {
        Passport::tokensCan([
            'place-orders' => '下订单',
            'check-status' => '检查订单状态',
        ]);
    }

默认作用域 ​

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

    use Laravel\Passport\Passport;

    Passport::tokensCan([
        'place-orders' => '下订单',
        'check-status' => '检查订单状态',
    ]);

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

NOTE

Passport 的默认作用域不适用于用户生成的个人访问令牌。

将作用域分配给令牌 ​

请求授权码时 ​

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

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

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

发放个人访问令牌时 ​

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

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

检查作用域 ​

Passport 包含两个中间件,可用于验证传入请求是否经过身份验证,并且访问令牌已授予给定作用域。要开始,请在应用程序的 bootstrap/app.php 文件中定义以下中间件别名:

    use Laravel\Passport\Http\Middleware\CheckForAnyScope;
    use Laravel\Passport\Http\Middleware\CheckScopes;

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

检查所有作用域 ​

scopes 中间件可以分配给路由,以验证传入请求的访问令牌是否具有所有列出的作用域:

    Route::get('/orders', function () {
        // 访问令牌具有 "check-status" 和 "place-orders" 作用域...
    })->middleware(['auth:api', 'scopes:check-status,place-orders']);

检查任何作用域 ​

scope 中间件可以分配给路由,以验证传入请求的访问令牌至少具有列出的一个作用域:

    Route::get('/orders', function () {
        // 访问令牌具有 "check-status" 或 "place-orders" 作用域...
    })->middleware(['auth:api', 'scope:check-status,place-orders']);

在令牌实例上检查作用域 ​

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

    use Illuminate\Http\Request;

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

其他作用域方法 ​

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

    use Laravel\Passport\Passport;

    Passport::scopeIds();

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

    Passport::scopes();

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

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

您可以使用 hasScope 方法确定给定作用域是否已定义:

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

使用 JavaScript 消费您的 API ​

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

通常,如果您想从 JavaScript 应用程序消费您的 API,您需要手动将访问令牌发送到应用程序,并在每次请求时将其传递给应用程序。然而,Passport 包含一个可以为您处理此操作的中间件。您只需将 CreateFreshApiToken 中间件附加到应用程序的 bootstrap/app.php 文件中的 web 中间件组中:

    use Laravel\Passport\Http\Middleware\CreateFreshApiToken;

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

WARNING

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

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

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

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

    /**
     * 启动任何应用程序服务。
     */
    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\RefreshTokenCreated

测试 ​

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

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

    test('servers can be created', function () {
        Passport::actingAs(
            User::factory()->create(),
            ['create-servers']
        );

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

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

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

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

        $response->assertStatus(201);
    }

Passport 的 actingAsClient 方法可用于指定当前经过身份验证的客户端及其作用域。传递给 actingAsClient 方法的第一个参数是客户端实例,第二个参数是应授予客户端的作用域数组:

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

    test('orders can be retrieved', function () {
        Passport::actingAsClient(
            Client::factory()->create(),
            ['check-status']
        );

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

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

    public function test_orders_can_be_retrieved(): void
    {
        Passport::actingAsClient(
            Client::factory()->create(),
            ['check-status']
        );

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

        $response->assertStatus(200);
    }