跳到内容

Laravel Passport

简介

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

exclamation

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

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 trait 添加到你的 App\Models\User 模型中。此 trait 将为你的模型提供一些帮助方法,使你可以检查已验证用户的令牌和作用域

<?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 方法中调用

/**
* Bootstrap any application services.
*/
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 版本时,请务必仔细查看 升级指南

配置

客户端密钥哈希

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

use Laravel\Passport\Passport;
 
Passport::hashClientSecrets();

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

令牌有效期

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

/**
* Bootstrap any application services.
*/
public function boot(): void
{
Passport::tokensExpireIn(now()->addDays(15));
Passport::refreshTokensExpireIn(now()->addDays(30));
Passport::personalAccessTokensExpireIn(now()->addMonths(6));
}
exclamation

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;
 
/**
* 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::ignoreRoutes 添加到你的应用程序的 AppServiceProviderregister 方法中来忽略 Passport 注册的路由

use Laravel\Passport\Passport;
 
/**
* Register any application services.
*/
public function register(): void
{
Passport::ignoreRoutes();
}

然后,你可以将 Passport 在 其路由文件 中定义的路由复制到你的应用程序的 routes/web.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 和密钥

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,你可以使用该 API 创建客户端。这使你免去了手动编写控制器来创建、更新和删除客户端的麻烦。

不过,您需要将 Passport 的 JSON API 与您自己的前端结合使用,以便为您的用户提供一个管理客户端的仪表板。下面,我们将回顾所有用于管理客户端的 API 端点。为了方便起见,我们将使用 Axios 来演示如何向这些端点发送 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 => {
// List errors on 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 => {
// List errors on 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 值,则仅当用户之前未授权使用应用程序的请求范围时,才会提示用户进行授权。

lightbulb

请记住,/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

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

<?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 请求以请求访问令牌。请求应包含当用户批准授权请求时由您的应用程序颁发的授权码

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_tokenrefresh_tokenexpires_in 属性。expires_in 属性包含访问令牌过期的秒数。

lightbulb

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

JSON API

Passport 还包括一个用于管理授权访问令牌的 JSON API。您可以将其与您自己的前端配对,以便为您的用户提供一个管理访问令牌的仪表板。为了方便起见,我们将使用 Axios 来演示如何向这些端点发送 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 路由将返回一个 JSON 响应,其中包含 access_tokenrefresh_tokenexpires_in 属性。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);
 
// Revoke an access token...
$tokenRepository->revokeAccessToken($tokenId);
 
// Revoke all of the token's refresh tokens...
$refreshTokenRepository->revokeRefreshTokensByAccessTokenId($tokenId);

清除令牌

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

# 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 文件中配置计划作业,以按计划自动修剪您的令牌

use Illuminate\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 规范中所定义。

代码挑战应为带有 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 参数,就像在标准授权码授权中一样。

如果状态参数匹配,则使用者应向您的应用程序发出 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();
});

密码授权令牌

exclamation

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

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

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

/**
* Bootstrap any application services.
*/
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' => '[email protected]',
'password' => 'my-password',
'scope' => '',
]);
 
return $response->json();
lightbulb

请记住,默认情况下,访问令牌的有效期很长。但是,您可以根据需要配置最大访问令牌生存期

请求所有作用域

使用密码授权或客户端凭据授权时,您可能希望为应用程序支持的所有范围授权令牌。您可以通过请求 * 范围来执行此操作。如果您请求 * 范围,则令牌实例上的 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' => '[email protected]',
'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;
 
/**
* 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
 
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);
}
}

隐式授权令牌

exclamation

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

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

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

请记住,/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或作为更简单的一般颁发访问令牌的方法可能很有用。

lightbulb

如果您的应用程序主要使用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);
 
// Creating a token without scopes...
$token = $user->createToken('Token Name')->accessToken;
 
// Creating a token with scopes...
$token = $user->createToken('My Token', ['place-orders'])->accessToken;

JSON API

Passport还包括一个JSON API,用于管理个人访问令牌。您可以将其与您自己的前端配对,为您的用户提供一个用于管理个人访问令牌的仪表板。下面,我们将回顾所有用于管理个人访问令牌的API端点。为方便起见,我们将使用Axios来演示如何向端点发出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 => {
// List errors on 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');
exclamation

如果您正在使用客户端凭据授权,则应使用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');
lightbulb

有关将多个用户提供程序与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方法接受范围名称和范围描述的数组。 范围描述可以是您想要的任何内容,并将在授权批准屏幕上显示给用户。

/**
* Bootstrap any application services.
*/
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',
]);
lightbulb

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 () {
// Access token has both "check-status" and "place-orders" scopes...
})->middleware(['auth:api', 'scopes:check-status,place-orders']);

检查任何范围

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

Route::get('/orders', function () {
// Access token has either "check-status" or "place-orders" scope...
})->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,
]);
})
exclamation

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

此中间件会将laravel_token cookie附加到您的传出响应中。此cookie包含一个加密的JWT,Passport将使用该JWT来验证来自您的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方法中调用此方法。

/**
* Bootstrap any application services.
*/
public function boot(): void
{
Passport::cookie('custom_name');
}

CSRF 保护

当使用此身份验证方法时,您需要确保在请求中包含有效的CSRF令牌标头。默认的Laravel JavaScript脚手架包含一个Axios实例,该实例将自动使用加密的XSRF-TOKEN cookie值在同源请求上发送X-XSRF-TOKEN标头。

lightbulb

如果您选择发送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);
}