Skip to content
虚位以待
赞助商赞助商
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待

Laravel Passport

介绍

Laravel Passport 为您的 Laravel 应用程序提供了一个完整的 OAuth2 服务器实现,只需几分钟即可完成。Passport 构建在由 Andy Millington 和 Simon Hamp 维护的 League OAuth2 服务器 之上。

WARNING

本文档假设您已经熟悉 OAuth2。如果您对 OAuth2 一无所知,请考虑在继续之前先熟悉 OAuth2 的一般术语和功能。

选择 Passport 还是 Sanctum?

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

然而,如果您尝试验证单页应用程序、移动应用程序或发布 API 令牌,您应该使用 Laravel Sanctum。Laravel Sanctum 不支持 OAuth2;然而,它提供了一个更简单的 API 身份验证开发体验。

安装

要开始,请通过 Composer 包管理器安装 Passport:

shell
composer require laravel/passport

Passport 的服务提供者注册了自己的数据库迁移目录,因此在安装包后,您应该迁移您的数据库。Passport 迁移将创建您的应用程序需要的表来存储 OAuth2 客户端和访问令牌:

shell
php artisan migrate

接下来,您应该执行 passport:install Artisan 命令。此命令将创建生成安全访问令牌所需的加密密钥。此外,该命令将创建“个人访问”和“密码授权”客户端,这些客户端将用于生成访问令牌:

shell
php artisan passport:install

NOTE

如果您希望使用 UUID 作为 Passport Client 模型的主键值而不是自动递增整数,请使用 uuids 选项安装 Passport。

运行 passport:install 命令后,将 Laravel\Passport\HasApiTokens trait 添加到您的 App\Models\User 模型中。此 trait 将为您的模型提供一些辅助方法,允许您检查经过身份验证的用户的令牌和范围。如果您的模型已经使用了 Laravel\Sanctum\HasApiTokens trait,您可以删除该 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',
    ],
],

客户端 UUIDs

您还可以在运行 passport:install 命令时使用 --uuids 选项。此选项将指示 Passport 您希望使用 UUID 而不是自动递增整数作为 Passport Client 模型的主键值。在使用 --uuids 选项运行 passport:install 命令后,您将获得有关禁用 Passport 默认迁移的其他说明:

shell
php artisan passport:install --uuids

部署 Passport

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

shell
php artisan passport:keys

如果需要,您可以定义 Passport 的密钥应从何处加载。您可以使用 Passport::loadKeysFrom 方法来实现此目的。通常,此方法应从应用程序的 App\Providers\AuthServiceProvider 类的 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 的默认迁移,您应该在 App\Providers\AppServiceProvider 类的 register 方法中调用 Passport::ignoreMigrations 方法。您可以使用 vendor:publish Artisan 命令导出默认迁移:

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

升级 Passport

在升级到 Passport 的新主要版本时,重要的是仔细查看升级指南

配置

客户端密钥哈希

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

php
use Laravel\Passport\Passport;

Passport::hashClientSecrets();

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

令牌有效期

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

php
/**
 * 注册任何身份验证/授权服务。
 */
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\AuthServiceProvider 类的 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;

/**
 * 注册任何身份验证/授权服务。
 */
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 定义的路由。要实现此目的,您首先需要通过在应用程序的 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 的方式。使用授权码时,客户端应用程序将重定向用户到您的服务器,在那里他们将批准或拒绝向客户端发放访问令牌的请求。

管理客户端

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

passport:client 命令

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

shell
php artisan passport:client

重定向 URLs

如果您希望为您的客户端允许多个重定向 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 由 webauth 中间件保护;因此,它只能从您自己的应用程序调用。无法从外部来源调用。

GET /oauth/clients

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

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

POST /oauth/clients

此路由用于创建新客户端。它需要两条数据:客户端的 name 和一个 redirect URL。redirect 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) => {
    // 列出响应中的错误...
  });

PUT /oauth/clients/{client-id}

此路由用于更新客户端。它需要两条数据:客户端的 name 和一个 redirect URL。redirect 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) => {
    // 列出响应中的错误...
  });

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_uriredirect_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
{
    /**
     * 确定客户端是否应跳过授权提示。
     */
    public function skipsAuthorization(): bool
    {
        return $this->firstParty();
    }
}

将授权码转换为访问令牌

如果用户批准授权请求,他们将被重定向回消费应用程序。消费者应首先验证 state 参数与重定向前存储的值是否匹配。如果 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 路由将返回一个包含 access_tokenrefresh_tokenexpires_in 属性的 JSON 响应。expires_in 属性包含访问令牌过期的秒数。

NOTE

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

JSON API

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

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 路由将返回一个包含 access_tokenrefresh_tokenexpires_in 属性的 JSON 响应。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);

// 撤销访问令牌...
$tokenRepository->revokeAccessToken($tokenId);

// 撤销令牌的所有刷新令牌...
$refreshTokenRepository->revokeRefreshTokensByAccessTokenId($tokenId);

清除令牌

当令牌被撤销或过期时,您可能希望从数据库中清除它们。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

您还可以在应用程序的 App\Console\Kernel 类中配置一个计划任务以自动按计划修剪您的令牌:

php
/**
 * 定义应用程序的命令计划。
 */
protected function schedule(Schedule $schedule): void
{
    $schedule->command('passport:purge')->hourly();
}

带 PKCE 的授权码授权

带有“代码交换证明密钥”(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 参数与重定向前存储的值是否匹配,如标准授权码授权中一样。

如果 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 授权码重定向流程。

创建密码授权客户端

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

shell
php artisan passport:client --password

请求令牌

一旦您创建了密码授权客户端,您可以通过向 /oauth/token 路由发出 POST 请求并提供用户的电子邮件地址和密码来请求访问令牌。请记住,此路由已由 Passport 注册,因此无需手动定义。如果请求成功,您将收到服务器返回的 JSON 响应中的 access_tokenrefresh_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。此范围只能分配给使用 passwordclient_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' => '*',
]);

自定义用户提供者

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

class User extends Authenticatable
{
    use HasApiTokens, Notifiable;

    /**
     * 查找给定用户名的用户实例。
     */
    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;

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

隐式授权令牌

WARNING

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

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

php
/**
 * 注册任何身份验证/授权服务。
 */
public function boot(): void
{
    Passport::enableImplicitGrant();
}

一旦启用了授权,开发人员可以使用他们的客户端 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' => '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 选项来执行此操作:

shell
php artisan passport:client --client

接下来,要使用此授权类型,您可以将 CheckClientCredentials 中间件添加到应用程序的 app/Http/Kernel.php 文件的 $middlewareAliases 属性中:

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

protected $middlewareAliases = [
    'client' => CheckClientCredentials::class,
];

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

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

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

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

检索令牌

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

php
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 访问令牌。

创建个人访问客户端

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

shell
php artisan passport:client --personal

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

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

管理个人访问令牌

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

php
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 由 webauth 中间件保护;因此,它只能从您自己的应用程序调用。无法从外部来源调用。

GET /oauth/scopes

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

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

GET /oauth/personal-access-tokens

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

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

POST /oauth/personal-access-tokens

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

js
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}

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

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

保护路由

通过中间件

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

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

WARNING

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

多个身份验证守卫

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

php
'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 令牌。例如,使用 Guzzle HTTP 库时:

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 消费者都需要下订单的能力。相反,您可以允许消费者仅请求授权访问订单发货状态。换句话说,范围允许您的应用程序用户限制第三方应用程序可以代表他们执行的操作。

定义范围

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

php
/**
 * 注册任何身份验证/授权服务。
 */
public function boot(): void
{
    Passport::tokensCan([
        'place-orders' => 'Place orders',
        'check-status' => 'Check order status',
    ]);
}

默认范围

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

php
use Laravel\Passport\Passport;

Passport::tokensCan([
    'place-orders' => 'Place orders',
    'check-status' => 'Check order status',
]);

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

NOTE

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

分配范围给令牌

请求授权码时

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

php
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 方法发放个人访问令牌,您可以将所需范围的数组作为方法的第二个参数传递:

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

检查范围

Passport 包含两个中间件,可用于验证传入请求是否使用已授予给定范围的令牌进行身份验证。要开始,请将以下中间件添加到 app/Http/Kernel.php 文件的 $middlewareAliases 属性中:

php
'scopes' => \Laravel\Passport\Http\Middleware\CheckScopes::class,
'scope' => \Laravel\Passport\Http\Middleware\CheckForAnyScope::class,

检查所有范围

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

php
Route::get('/orders', function () {
    // 访问令牌具有“check-status”和“place-orders”范围...
})->middleware(['auth:api', 'scopes:check-status,place-orders']);

检查任何范围

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

php
Route::get('/orders', function () {
    // 访问令牌具有“check-status”或“place-orders”范围...
})->middleware(['auth:api', 'scope:check-status,place-orders']);

在令牌实例上检查范围

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

php
use Illuminate\Http\Request;

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

其他范围方法

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

php
use Laravel\Passport\Passport;

Passport::scopeIds();

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

php
Passport::scopes();

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

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

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

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

使用 JavaScript 消费您的 API

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

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

php
'web' => [
    // 其他中间件...
    \Laravel\Passport\Http\Middleware\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\AuthServiceProvider 类的 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 在发放访问令牌和刷新令牌时会触发事件。您可以使用这些事件来修剪或撤销数据库中的其他访问令牌。如果您愿意,您可以在应用程序的 App\Providers\EventServiceProvider 类中附加这些事件的监听器:

php
/**
 * 应用程序的事件监听器映射。
 *
 * @var array
 */
protected $listen = [
    'Laravel\Passport\Events\AccessTokenCreated' => [
        'App\Listeners\RevokeOldTokens',
    ],

    'Laravel\Passport\Events\RefreshTokenCreated' => [
        'App\Listeners\PruneOldTokens',
    ],
];

测试

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

php
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 方法的第一个参数是客户端实例,第二个参数是应授予客户端令牌的范围数组:

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