跳至内容

Laravel Passport

简介

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

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 注册的路由,方法是在应用程序的 AppServiceProviderregister 方法中添加 Passport::ignoreRoutes

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。

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 演示如何向端点发出 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 参数。如果 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 的客户端。您可以使用 passport:client Artisan 命令和 --public 选项来执行此操作。

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 参数,就像在标准授权码授予中一样。

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

创建密码授权客户端

在您的应用程序能够通过密码授予颁发令牌之前,您需要创建一个密码授予客户端。您可以使用 passport:client Artisan 命令和 --password 选项来执行此操作。如果您已经运行了 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',
]);
}

默认作用域

如果客户端未请求任何特定范围,您可以配置 Passport 服务器以使用 setDefaultScope 方法将默认范围附加到令牌。通常,您应该从应用程序的 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。相同的 API 可以被您的 Web 应用程序、移动应用程序、第三方应用程序以及您可能在各种包管理器上发布的任何 SDK 使用。

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

use Laravel\Passport\Http\Middleware\CreateFreshApiToken;
 
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
CreateFreshApiToken::class,
]);
})
exclamation

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

此中间件将 laravel_token cookie 附加到您的传出响应。此 cookie 包含 Passport 将用于对来自 JavaScript 应用程序的 API 请求进行身份验证的加密 JWT。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);
}