laravel
10. 软件包
Laravel Passport

介绍

Laravel Passport (opens in a new tab) 可以在短短几分钟内为您的 Laravel 应用程序提供完整的 OAuth2 服务器实现。Passport 构建在 League OAuth2 服务器 (opens in a new tab) 之上,该服务器由 Andy Millington 和 Simon Hamp 维护。

[!警告]
本文档假定您已经熟悉 OAuth2。如果您对 OAuth2 一无所知,建议在继续阅读之前先熟悉 OAuth2 的一般术语 (opens in a new tab)和功能。

Passport 还是 Sanctum?

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

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

安装

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

php artisan install:api --passport

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

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

运行 install:api 命令后,将 Laravel\Passport\HasApiTokens 特征添加到您的 App\Models\User 模型中。此特征将为您的模型提供一些辅助方法,使您能够检查经过身份验证的用户的令牌和范围:

<?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

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

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

部署 Passport

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

php artisan passport:keys

如有必要,您可以定义 Passport 的密钥应从何处加载的路径。您可以使用 Passport::loadKeysFrom 方法来实现此目的。通常,此方法应从您的应用程序的 App\Providers\AppServiceProvider 类的 boot 方法中调用:

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

从环境中加载密钥

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

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

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

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 的新版本时,重要的是您要仔细查看 升级指南 (opens in a new tab)

配置

客户端密钥哈希

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

use Laravel\Passport\Passport;

Passport::hashClientSecrets();

启用后,所有客户端密钥只有在创建后立即对用户可见。由于明文客户端密钥值永远不会存储在数据库中,因此如果丢失,无法恢复密钥的值。

令牌有效期

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

/**
 * 引导任何应用程序服务。
 */
public function boot(): void
{
    Passport::tokensExpireIn(now()->addDays(15));
    Passport::refreshTokensExpireIn(now()->addDays(30));
    Passport::personalAccessTokensExpireIn(now()->addMonths(6));
}

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

覆盖默认模型

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

use Laravel\Passport\Client as PassportClient;

class Client extends PassportClient
{
    //...
}

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

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 注册的路由:

use Laravel\Passport\Passport;

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

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

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

颁发访问令牌

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

管理客户端

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

passport:client 命令

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

php artisan passport:client

重定向 URL

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

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

JSON API

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

但是,您需要将 Passport 的 JSON API 与您自己的前端配对,为您的用户提供一个仪表板来管理他们的客户端。下面,我们将回顾管理客户端的所有 API 端点。为了方便起见,我们将使用 Axios (opens in a new tab) 来演示对端点进行 HTTP 请求。

JSON API 由 webauth 中间件保护;因此,它只能从您自己的应用程序中调用。它不能从外部源调用。

GET /oauth/clients

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

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

POST /oauth/clients

此路由用于创建新客户端。它需要两段数据:客户端的 nameredirect URL。redirect URL 是用户在批准或拒绝授权请求后将被重定向到的位置。

创建客户端时,将为其颁发客户端 ID 和客户端密钥。从您的应用程序请求访问令牌时将使用这些值。客户端创建路由将返回新的客户端实例:

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}

此路由用于更新客户端。它需要两段数据:客户端的 nameredirect URL。redirect URL 是用户在批准或拒绝授权请求后将被重定向到的位置。该路由将返回更新后的客户端实例:

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}

此路由用于删除客户端:

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

请求令牌

重定向以进行授权

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

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 值,则仅当用户以前未为请求的范围授权对使用的应用程序的访问时,才会提示用户进行授权。

[!注意]
请记住,/oauth/authorize 路由已经由 Passport 定义。您不需要手动定义此路由。

批准请求

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

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

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

有时您可能希望跳过授权提示,例如在授权第一方客户端时。您可以通过覆盖默认模型并定义 skipsAuthorization 方法来实现此目的。

将授权码转换为访问令牌

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

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,
        '无效的状态值。'
    );
 
    $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 属性包含访问令牌到期前的秒数。

[!注意]
/oauth/authorize 路由一样,/oauth/token 路由由 Passport 为您定义。无需手动定义此路由。

JSON API

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

GET /oauth/tokens

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

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

DELETE /oauth/tokens/{token-id}

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

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

刷新令牌

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

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 的服务容器进行解析:

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 命令可以为您完成此操作:

# 清除已撤销和过期的令牌以及授权码...
php artisan passport:purge
 
# 仅清除超过 6 小时的过期令牌...
php artisan passport:purge --hours=6
 
# 仅清除已撤销的令牌和授权码...
php artisan passport:purge --revoked
 
# 仅清除过期的令牌和授权码...
php artisan passport:purge --expired

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

use Laravel\Support\Facades\Schedule;
 
Schedule::command('passport:purge')->hourly();

带有 PKCE 的授权码授予

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

创建客户端

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

php artisan passport:client --public

请求令牌

代码验证器和代码挑战

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

代码验证器应该是一个随机字符串,长度在 43 到 128 个字符之间,包含字母、数字和 "-"".""_""~" 字符,如 RFC 7636 规范 (opens in a new tab) 中所定义。

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

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

重定向以进行授权

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

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 请求以请求访问令牌。该请求应包括当用户批准授权请求时您的应用程序颁发的授权码以及最初生成的代码验证器:

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

密码授予令牌

[!警告]
我们不再建议使用密码授予令牌。相反,您应该选择 OAuth2 服务器当前推荐的授予类型 (opens in a new tab)

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

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

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

创建密码授予客户端

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

php artisan passport:client --password

请求令牌

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

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

[!注意]
请记住,默认情况下访问令牌的有效期较长。但是,如果需要,您可以配置您的最大访问令牌有效期

请求所有范围

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

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

隐式授予令牌

[!警告]
我们不再建议使用隐式授予令牌。相反,您应该选择 OAuth2 服务器当前推荐的授予类型 (opens in a new tab)

隐式授予类似于授权码授予;然而,令牌在不交换授权码的情况下返回给客户端。此授予最常用于无法安全存储客户端凭据的 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);
});

[!注意]
请记住,/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'];

个人访问令牌

有时,您的用户可能希望为自己颁发访问令牌,而无需经过典型的授权码重定向流程。允许用户通过您的应用程序的用户界面为自己颁发令牌对于允许用户试用您的 API 或作为一种更简单的颁发访问令牌的方法可能是有用的。

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

创建个人访问客户端

在您的应用程序可以颁发个人访问令牌之前,您需要创建一个个人访问客户端。您可以通过使用 passport:client Artisan 命令的 --personal 选项来完成此操作。如果您已经运行了 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 (opens in a new tab) 来演示对端点的 HTTP 请求。

JSON API 由 webauth 中间件保护;因此,它只能从您自己的应用程序中调用。它不能从外部源调用。

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' => 'Place orders',
        'check-status' => 'Check order status',
    ]);
}

默认范围

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

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 参数应该是一个以空格分隔的范围列表:

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。您的 Web 应用程序、移动应用程序、第三方应用程序以及您可能在各种包管理器上发布的任何 SDK 都可以使用相同的 API。

通常,如果您想从您的 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);
    });

自定义 Cookie 名称

如果需要,您可以使用 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 方法