Laravel Passport
介绍
Laravel Passport 可以在几分钟内为您的 Laravel 应用程序提供完整的 OAuth2 服务器实现。Passport 构建在由 Andy Millington 和 Simon Hamp 维护的 League OAuth2 服务器 之上。
NOTE
本文档假设您已经熟悉 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 客户端和访问令牌。该命令还将创建生成安全访问令牌所需的加密密钥。
运行 install:api
命令后,将 Laravel\Passport\HasApiTokens
trait 和 Laravel\Passport\Contracts\OAuthenticatable
接口添加到您的 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\Contracts\OAuthenticatable;
use Laravel\Passport\HasApiTokens;
class User extends Authenticatable implements OAuthenticatable
{
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 的新主要版本时,务必仔细查看升级指南。
配置
令牌有效期
默认情况下,Passport 发放的访问令牌有效期为一年。如果您希望配置更长或更短的令牌有效期,可以使用 tokensExpireIn
、refreshTokensExpireIn
和 personalAccessTokensExpireIn
方法。这些方法应从应用程序的 App\Providers\AppServiceProvider
类的 boot
方法中调用:
use Carbon\CarbonInterval;
/**
* 启动任何应用程序服务。
*/
public function boot(): void
{
Passport::tokensExpireIn(CarbonInterval::days(15));
Passport::refreshTokensExpireIn(CarbonInterval::days(30));
Passport::personalAccessTokensExpireIn(CarbonInterval::months(6));
}
WARNING
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\DeviceCode;
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::useDeviceCodeModel(DeviceCode::class)
}
覆盖路由
有时您可能希望自定义 Passport 定义的路由。要实现这一点,您首先需要通过在应用程序的 AppServiceProvider
的 register
方法中添加 Passport::ignoreRoutes
来忽略 Passport 注册的路由:
use Laravel\Passport\Passport;
/**
* 注册任何应用程序服务。
*/
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 路由...
});
授权码授权
通过授权码使用 OAuth2 是大多数开发人员熟悉的 OAuth2 方式。使用授权码时,客户端应用程序将用户重定向到您的服务器,在那里他们将批准或拒绝向客户端发放访问令牌的请求。
要开始,我们需要指示 Passport 如何返回我们的“授权”视图。
所有授权视图的渲染逻辑都可以使用 Laravel\Passport\Passport
类提供的适当方法进行自定义。通常,您应该从应用程序的 App\Providers\AppServiceProvider
类的 boot
方法中调用此方法:
use Laravel\Passport\Passport;
/**
* 启动任何应用程序服务。
*/
public function boot(): void
{
// 通过提供视图名称...
Passport::authorizationView('auth.oauth.authorize');
// 通过提供闭包...
Passport::authorizationView(fn ($parameters) => Inertia::render('Auth/OAuth/Authorize', [
'request' => $parameters['request'],
'authToken' => $parameters['authToken'],
'client' => $parameters['client'],
'user' => $parameters['user'],
'scopes' => $parameters['scopes'],
]));
}
Passport 将自动定义返回此视图的 /oauth/authorize
路由。您的 auth.oauth.authorize
模板应包含一个表单,该表单向 passport.authorizations.approve
路由发出 POST 请求以批准授权,并向 passport.authorizations.deny
路由发出 DELETE 请求以拒绝授权。passport.authorizations.approve
和 passport.authorizations.deny
路由期望 state
、client_id
和 auth_token
字段。
管理客户端
构建需要与您的应用程序的 API 交互的应用程序的开发人员需要通过创建“客户端”来注册他们的应用程序。通常,这包括提供其应用程序的名称和一个 URI,您的应用程序可以在用户批准其授权请求后重定向到该 URI。
第一方客户端
创建客户端的最简单方法是使用 passport:client
Artisan 命令。此命令可用于创建第一方客户端或测试您的 OAuth2 功能。当您运行 passport:client
命令时,Passport 将提示您提供有关客户端的更多信息,并为您提供客户端 ID 和密钥:
php artisan passport:client
如果您希望为客户端允许多个重定向 URI,可以在 passport:client
命令提示您输入 URI 时使用逗号分隔的列表指定它们。任何包含逗号的 URI 应进行 URI 编码:
https://third-party-app.com/callback,https://example.com/oauth/redirect
第三方客户端
由于您的应用程序用户将无法使用 passport:client
命令,您可以使用 Laravel\Passport\ClientRepository
类的 createAuthorizationCodeGrantClient
方法为给定用户注册客户端:
use App\Models\User;
use Laravel\Passport\ClientRepository;
$user = User::find($userId);
// 创建属于给定用户的 OAuth 应用客户端...
$client = app(ClientRepository::class)->createAuthorizationCodeGrantClient(
user: $user,
name: 'Example App',
redirectUris: ['https://third-party-app.com/callback'],
confidential: false,
enableDeviceFlow: true
);
// 检索属于用户的所有 OAuth 应用客户端...
$clients = $user->oauthApps()->get();
createAuthorizationCodeGrantClient
方法返回一个 Laravel\Passport\Client
实例。您可以将 $client->id
显示为客户端 ID,将 $client->plainSecret
显示为客户端密钥给用户。
请求令牌
重定向以进行授权
创建客户端后,开发人员可以使用其客户端 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' => 'your-client-id',
'redirect_uri' => 'https://third-party-app.com/callback',
'response_type' => 'code',
'scope' => 'user:read orders:create',
'state' => $state,
// 'prompt' => '', // "none", "consent", or "login"
]);
return redirect('https://passport-app.test/oauth/authorize?'.$query);
});
prompt
参数可用于指定 Passport 应用程序的认证行为。
如果 prompt
值为 none
,则如果用户尚未在 Passport 应用程序中认证,Passport 将始终抛出认证错误。如果值为 consent
,即使所有作用域先前已授予消费应用程序,Passport 也将始终显示授权批准屏幕。当值为 login
时,Passport 应用程序将始终提示用户重新登录应用程序,即使他们已经有现有会话。
如果未提供 prompt
值,用户将仅在他们尚未为请求的作用域先前授权访问消费应用程序时被提示进行授权。
NOTE
请记住,/oauth/authorize
路由已由 Passport 定义。您无需手动定义此路由。
批准请求
在接收授权请求时,Passport 将根据 prompt
参数的值(如果存在)自动响应,并可能向用户显示一个模板,允许他们批准或拒绝授权请求。如果他们批准请求,他们将被重定向回消费应用程序指定的 redirect_uri
。redirect_uri
必须与创建客户端时指定的 redirect
URL 匹配。
有时您可能希望跳过授权提示,例如在授权第一方客户端时。您可以通过扩展 Client
模型并定义一个 skipsAuthorization
方法来实现这一点。如果 skipsAuthorization
返回 true
,则客户端将被批准,用户将立即被重定向回 redirect_uri
,除非消费应用程序在重定向进行授权时显式设置了 prompt
参数:
<?php
namespace App\Models\Passport;
use Illuminate\Contracts\Auth\Authenticatable;
use Laravel\Passport\Client as BaseClient;
class Client extends BaseClient
{
/**
* 确定客户端是否应跳过授权提示。
*
* @param \Laravel\Passport\Scope[] $scopes
*/
public function skipsAuthorization(Authenticatable $user, array $scopes): 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('https://passport-app.test/oauth/token', [
'grant_type' => 'authorization_code',
'client_id' => 'your-client-id',
'client_secret' => 'your-client-secret',
'redirect_uri' => 'https://third-party-app.com/callback',
'code' => $request->code,
]);
return $response->json();
});
此 /oauth/token
路由将返回一个包含 access_token
、refresh_token
和 expires_in
属性的 JSON 响应。expires_in
属性包含访问令牌过期的秒数。
NOTE
与 /oauth/authorize
路由一样,/oauth/token
路由由 Passport 为您定义。无需手动定义此路由。
管理令牌
您可以使用 Laravel\Passport\HasApiTokens
trait 的 tokens
方法检索用户授权的令牌。例如,这可以用于为您的用户提供一个仪表板,以跟踪他们与第三方应用程序的连接:
use App\Models\User;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Date;
use Laravel\Passport\Token;
$user = User::find($userId);
// 检索用户的所有有效令牌...
$tokens = $user->tokens()
->where('revoked', false)
->where('expires_at', '>', Date::now())
->get();
// 检索用户与第三方 OAuth 应用客户端的所有连接...
$connections = $tokens->load('client')
->reject(fn (Token $token) => $token->client->firstParty())
->groupBy('client_id')
->map(fn (Collection $tokens) => [
'client' => $tokens->first()->client,
'scopes' => $tokens->pluck('scopes')->flatten()->unique()->values()->all(),
'tokens_count' => $tokens->count(),
])
->values();
刷新令牌
如果您的应用程序发放短期访问令牌,用户将需要通过发放访问令牌时提供的刷新令牌来刷新其访问令牌:
use Illuminate\Support\Facades\Http;
$response = Http::asForm()->post('https://passport-app.test/oauth/token', [
'grant_type' => 'refresh_token',
'refresh_token' => 'the-refresh-token',
'client_id' => 'your-client-id',
'client_secret' => 'your-client-secret', // 仅对机密客户端必需...
'scope' => 'user:read orders:create',
]);
return $response->json();
此 /oauth/token
路由将返回一个包含 access_token
、refresh_token
和 expires_in
属性的 JSON 响应。expires_in
属性包含访问令牌过期的秒数。
撤销令牌
您可以使用 Laravel\Passport\Token
模型的 revoke
方法撤销令牌。您可以使用 Laravel\Passport\RefreshToken
模型的 revoke
方法撤销令牌的刷新令牌:
use Laravel\Passport\Passport;
use Laravel\Passport\Token;
$token = Passport::token()->find($tokenId);
// 撤销访问令牌...
$token->revoke();
// 撤销令牌的刷新令牌...
$token->refreshToken?->revoke();
// 撤销用户的所有令牌...
User::find($userId)->tokens()->each(function (Token $token) {
$token->revoke();
$token->refreshToken?->revoke();
});
清除令牌
当令牌被撤销或过期时,您可能希望从数据库中清除它们。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 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', $codeVerifier, 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', $codeVerifier = Str::random(128)
);
$codeChallenge = strtr(rtrim(
base64_encode(hash('sha256', $codeVerifier, true))
, '='), '+/', '-_');
$query = http_build_query([
'client_id' => 'your-client-id',
'redirect_uri' => 'https://third-party-app.com/callback',
'response_type' => 'code',
'scope' => 'user:read orders:create',
'state' => $state,
'code_challenge' => $codeChallenge,
'code_challenge_method' => 'S256',
// 'prompt' => '', // "none", "consent", or "login"
]);
return redirect('https://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('https://passport-app.test/oauth/token', [
'grant_type' => 'authorization_code',
'client_id' => 'your-client-id',
'redirect_uri' => 'https://third-party-app.com/callback',
'code_verifier' => $codeVerifier,
'code' => $request->code,
]);
return $response->json();
});
设备授权
OAuth2 设备授权允许无浏览器或输入受限的设备(如电视和游戏机)通过交换“设备码”来获取访问令牌。使用设备流时,设备客户端将指示用户使用辅助设备(如计算机或智能手机)连接到您的服务器,在那里他们将输入提供的“用户码”并批准或拒绝访问请求。
要开始,我们需要指示 Passport 如何返回我们的“用户码”和“授权”视图。
所有授权视图的渲染逻辑都可以使用 Laravel\Passport\Passport
类提供的适当方法进行自定义。通常,您应该从应用程序的 App\Providers\AppServiceProvider
类的 boot
方法中调用此方法。
use Laravel\Passport\Passport;
/**
* 启动任何应用程序服务。
*/
public function boot(): void
{
// 通过提供视图名称...
Passport::deviceUserCodeView('auth.oauth.device.user-code');
Passport::deviceAuthorizationView('auth.oauth.device.authorize');
// 通过提供闭包...
Passport::deviceUserCodeView(fn ($parameters) => Inertia::render('Auth/OAuth/Device/UserCode'));
Passport::deviceAuthorizationView(fn ($parameters) => Inertia::render('Auth/OAuth/Device/Authorize', [
'request' => $parameters['request'],
'authToken' => $parameters['authToken'],
'client' => $parameters['client'],
'user' => $parameters['user'],
'scopes' => $parameters['scopes'],
]));
// ...
}
Passport 将自动定义返回这些视图的路由。您的 auth.oauth.device.user-code
模板应包含一个表单,该表单向 passport.device.authorizations.authorize
路由发出 GET 请求。passport.device.authorizations.authorize
路由期望一个 user_code
查询参数。
您的 auth.oauth.device.authorize
模板应包含一个表单,该表单向 passport.device.authorizations.approve
路由发出 POST 请求以批准授权,并向 passport.device.authorizations.deny
路由发出 DELETE 请求以拒绝授权。passport.device.authorizations.approve
和 passport.device.authorizations.deny
路由期望 state
、client_id
和 auth_token
字段。
创建设备授权客户端
在您的应用程序可以通过设备授权发放令牌之前,您需要创建一个启用设备流的客户端。您可以使用 passport:client
Artisan 命令和 --device
选项来完成此操作。此命令将创建一个第一方设备流启用的客户端,并为您提供客户端 ID 和密钥:
php artisan passport:client --device
此外,您可以使用 ClientRepository
类的 createDeviceAuthorizationGrantClient
方法为给定用户注册第三方客户端:
use App\Models\User;
use Laravel\Passport\ClientRepository;
$user = User::find($userId);
$client = app(ClientRepository::class)->createDeviceAuthorizationGrantClient(
user: $user,
name: 'Example Device',
confidential: false,
);
请求令牌
请求设备码
创建客户端后,开发人员可以使用其客户端 ID 从您的应用程序请求设备码。首先,消费设备应向您的应用程序的 /oauth/device/code
路由发出 POST
请求以请求设备码:
use Illuminate\Support\Facades\Http;
$response = Http::asForm()->post('https://passport-app.test/oauth/device/code', [
'client_id' => 'your-client-id',
'scope' => 'user:read orders:create',
]);
return $response->json();
这将返回一个包含 device_code
、user_code
、verification_uri
、interval
和 expires_in
属性的 JSON 响应。expires_in
属性包含设备码过期的秒数。interval
属性包含消费设备在轮询 /oauth/token
路由时应等待的秒数,以避免速率限制错误。
NOTE
请记住,/oauth/device/code
路由已由 Passport 定义。您无需手动定义此路由。
显示验证 URI 和用户码
一旦获得设备码请求,消费设备应指示用户使用另一台设备访问提供的 verification_uri
并输入 user_code
以批准授权请求。
轮询令牌请求
由于用户将使用单独的设备来授予(或拒绝)访问,消费设备应轮询您的应用程序的 /oauth/token
路由以确定用户何时响应请求。消费设备应使用请求设备码时提供的 JSON 响应中的最小轮询 interval
以避免速率限制错误:
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Sleep;
$interval = 5;
do {
Sleep::for($interval)->seconds();
$response = Http::asForm()->post('https://passport-app.test/oauth/token', [
'grant_type' => 'urn:ietf:params:oauth:grant-type:device_code',
'client_id' => 'your-client-id',
'client_secret' => 'your-client-secret', // 仅对机密客户端必需
'device_code' => 'the-device-code',
]);
if ($response->json('error') === 'slow_down') {
$interval += 5;
}
} while (in_array($response->json('error'), ['authorization_pending', 'slow_down']));
return $response->json();
如果用户已批准授权请求,这将返回一个包含 access_token
、refresh_token
和 expires_in
属性的 JSON 响应。expires_in
属性包含访问令牌过期的秒数。
密码授权
WARNING
我们不再推荐使用密码授权令牌。相反,您应该选择当前由 OAuth2 服务器推荐的授权类型。
OAuth2 密码授权允许您的其他第一方客户端(如移动应用程序)使用电子邮件地址/用户名和密码获取访问令牌。这允许您安全地向第一方客户端发放访问令牌,而无需用户通过整个 OAuth2 授权码重定向流程。
要启用密码授权,请在应用程序的 App\Providers\AppServiceProvider
类的 boot
方法中调用 enablePasswordGrant
方法:
/**
* 启动任何应用程序服务。
*/
public function boot(): void
{
Passport::enablePasswordGrant();
}
创建密码授权客户端
在您的应用程序可以通过密码授权发放令牌之前,您需要创建一个密码授权客户端。您可以使用 passport:client
Artisan 命令和 --password
选项来完成此操作。
php artisan passport:client --password
请求令牌
启用授权并创建密码授权客户端后,您可以通过向 /oauth/token
路由发出 POST
请求并提供用户的电子邮件地址和密码来请求访问令牌。请记住,此路由已由 Passport 注册,因此无需手动定义。如果请求成功,您将从服务器的 JSON 响应中收到 access_token
和 refresh_token
:
use Illuminate\Support\Facades\Http;
$response = Http::asForm()->post('https://passport-app.test/oauth/token', [
'grant_type' => 'password',
'client_id' => 'your-client-id',
'client_secret' => 'your-client-secret', // 仅对机密客户端必需
'username' => 'taylor@laravel.com',
'password' => 'my-password',
'scope' => 'user:read orders:create',
]);
return $response->json();
NOTE
请记住,访问令牌默认是长期有效的。但是,如果需要,您可以自由配置最大访问令牌有效期。
请求所有作用域
使用密码授权或客户端凭证授权时,您可能希望为应用程序支持的所有作用域授权令牌。您可以通过请求 *
作用域来实现这一点。如果您请求 *
作用域,令牌实例上的 can
方法将始终返回 true
。此作用域只能分配给使用 password
或 client_credentials
授权发放的令牌:
use Illuminate\Support\Facades\Http;
$response = Http::asForm()->post('https://passport-app.test/oauth/token', [
'grant_type' => 'password',
'client_id' => 'your-client-id',
'client_secret' => 'your-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);
}
}
隐式授权
WARNING
我们不再推荐使用隐式授权令牌。相反,您应该选择当前由 OAuth2 服务器推荐的授权类型。
隐式授权类似于授权码授权;然而,令牌在不交换授权码的情况下返回给客户端。此授权最常用于 JavaScript 或移动应用程序,其中客户端凭证无法安全存储。要启用授权,请在应用程序的 App\Providers\AppServiceProvider
类的 boot
方法中调用 enableImplicitGrant
方法:
/**
* 启动任何应用程序服务。
*/
public function boot(): void
{
Passport::enableImplicitGrant();
}
在您的应用程序可以通过隐式授权发放令牌之前,您需要创建一个隐式授权客户端。您可以使用 passport:client
Artisan 命令和 --implicit
选项来完成此操作。
php artisan passport:client --implicit
启用授权并创建隐式客户端后,开发人员可以使用其客户端 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' => 'your-client-id',
'redirect_uri' => 'https://third-party-app.com/callback',
'response_type' => 'token',
'scope' => 'user:read orders:create',
'state' => $state,
// 'prompt' => '', // "none", "consent", or "login"
]);
return redirect('https://passport-app.test/oauth/authorize?'.$query);
});
NOTE
请记住,/oauth/authorize
路由已由 Passport 定义。您无需手动定义此路由。
客户端凭证授权
客户端凭证授权适用于机器对机器的身份验证。例如,您可以在执行维护任务的计划作业中使用此授权。
在您的应用程序可以通过客户端凭证授权发放令牌之前,您需要创建一个客户端凭证授权客户端。您可以使用 passport:client
Artisan 命令的 --client
选项来完成此操作:
php artisan passport:client --client
接下来,将 Laravel\Passport\Http\Middleware\EnsureClientIsResourceOwner
中间件分配给一个路由:
use Laravel\Passport\Http\Middleware\EnsureClientIsResourceOwner;
Route::get('/orders', function (Request $request) {
// 访问令牌有效且客户端是资源所有者...
})->middleware(EnsureClientIsResourceOwner::class);
要将路由访问限制为特定范围,您可以向 using
方法提供所需范围的列表:
Route::get('/orders', function (Request $request) {
// 访问令牌有效,客户端是资源所有者,并且具有 "servers:read" 和 "servers:create" 范围...
})->middleware(EnsureClientIsResourceOwner::using('servers:read', 'servers:create');
检索令牌
要使用此授权类型检索令牌,请向 oauth/token
端点发出请求:
use Illuminate\Support\Facades\Http;
$response = Http::asForm()->post('https://passport-app.test/oauth/token', [
'grant_type' => 'client_credentials',
'client_id' => 'your-client-id',
'client_secret' => 'your-client-secret',
'scope' => 'servers:read servers:create',
]);
return $response->json()['access_token'];
个人访问令牌
有时,您的用户可能希望在不经过典型的授权代码重定向流程的情况下向自己发放访问令牌。允许用户通过您的应用程序的 UI 向自己发放令牌可以帮助用户实验您的 API,或者作为发放访问令牌的一种更简单的方法。
NOTE
如果您的应用程序主要使用 Passport 发放个人访问令牌,请考虑使用 Laravel Sanctum,Laravel 的轻量级第一方库,用于发放 API 访问令牌。
创建个人访问客户端
在您的应用程序可以发放个人访问令牌之前,您需要创建一个个人访问客户端。您可以通过执行 passport:client
Artisan 命令并使用 --personal
选项来完成此操作。如果您已经运行了 passport:install
命令,则无需运行此命令:
php artisan passport:client --personal
自定义用户提供者
如果您的应用程序使用多个 身份验证用户提供者,您可以在通过 artisan passport:client --personal
命令创建客户端时提供 --provider
选项来指定个人访问授权客户端使用哪个用户提供者。给定的提供者名称应与应用程序的 config/auth.php
配置文件中定义的有效提供者匹配。然后,您可以 使用中间件保护您的路由,以确保只有来自守卫指定提供者的用户被授权。
管理个人访问令牌
一旦您创建了个人访问客户端,您可以使用 App\Models\User
模型实例上的 createToken
方法为给定用户发放令牌。createToken
方法接受令牌名称作为第一个参数,并接受一个可选的 范围 数组作为第二个参数:
use App\Models\User;
use Illuminate\Support\Facades\Date;
use Laravel\Passport\Token;
$user = User::find($userId);
// 创建没有范围的令牌...
$token = $user->createToken('My Token')->accessToken;
// 创建具有范围的令牌...
$token = $user->createToken('My Token', ['user:read', 'orders:create'])->accessToken;
// 创建具有所有范围的令牌...
$token = $user->createToken('My Token', ['*'])->accessToken;
// 检索属于用户的所有有效个人访问令牌...
$tokens = $user->tokens()
->with('client')
->where('revoked', false)
->where('expires_at', '>', Date::now())
->get()
->filter(fn (Token $token) => $token->client->hasGrantType('personal_access'));
保护路由
通过中间件
Passport 包含一个 身份验证守卫,它将验证传入请求的访问令牌。一旦您将 api
守卫配置为使用 passport
驱动程序,您只需在任何需要有效访问令牌的路由上指定 auth:api
中间件:
Route::get('/user', function () {
// 只有 API 认证的用户可以访问此路由...
})->middleware('auth:api');
WARNING
如果您使用 客户端凭证授权,您应该使用 Laravel\Passport\Http\Middleware\EnsureClientIsResourceOwner 中间件 来保护您的路由,而不是 auth:api
中间件。
多个身份验证守卫
如果您的应用程序对不同类型的用户进行身份验证,这些用户可能使用完全不同的 Eloquent 模型,您可能需要为应用程序中的每种用户提供者类型定义一个守卫配置。这允许您保护特定用户提供者的请求。例如,给定以下守卫配置 config/auth.php
配置文件:
'guards' => [
'api' => [
'driver' => 'passport',
'provider' => 'users',
],
'api-customers' => [
'driver' => 'passport',
'provider' => 'customers',
],
],
以下路由将使用 api-customers
守卫,该守卫使用 customers
用户提供者来验证传入请求:
Route::get('/customer', function () {
// ...
})->middleware('auth:api-customers');
传递访问令牌
在调用由 Passport 保护的路由时,您的应用程序的 API 消费者应在请求的 Authorization
头中将其访问令牌指定为 Bearer
令牌。例如,使用 Http
Facade:
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([
'user:read' => '检索用户信息',
'orders:create' => '下订单',
'orders:read:status' => '检查订单状态',
]);
}
默认范围
如果客户端未请求任何特定范围,您可以配置您的 Passport 服务器以使用 defaultScopes
方法将默认范围附加到令牌。通常,您应该从应用程序的 App\Providers\AppServiceProvider
类的 boot
方法中调用此方法:
use Laravel\Passport\Passport;
Passport::tokensCan([
'user:read' => '检索用户信息',
'orders:create' => '下订单',
'orders:read:status' => '检查订单状态',
]);
Passport::defaultScopes([
'user:read',
'orders:create',
]);
将范围分配给令牌
请求授权代码时
在使用授权代码授权请求访问令牌时,消费者应将其所需的范围指定为 scope
查询字符串参数。scope
参数应为范围的空格分隔列表:
Route::get('/redirect', function () {
$query = http_build_query([
'client_id' => 'your-client-id',
'redirect_uri' => 'https://third-party-app.com/callback',
'response_type' => 'code',
'scope' => 'user:read orders:create',
]);
return redirect('https://passport-app.test/oauth/authorize?'.$query);
});
发放个人访问令牌时
如果您使用 App\Models\User
模型的 createToken
方法发放个人访问令牌,您可以将所需范围的数组作为方法的第二个参数传递:
$token = $user->createToken('My Token', ['orders:create'])->accessToken;
检查范围
Passport 包含两个中间件,可用于验证传入请求是否使用已授予给定范围的令牌进行身份验证。
检查所有范围
Laravel\Passport\Http\Middleware\CheckToken
中间件可以分配给路由,以验证传入请求的访问令牌是否具有所有列出的范围:
use Laravel\Passport\Http\Middleware\CheckToken;
Route::get('/orders', function () {
// 访问令牌具有 "orders:read" 和 "orders:create" 范围...
})->middleware(['auth:api', CheckToken::using('orders:read', 'orders:create');
检查任何范围
Laravel\Passport\Http\Middleware\CheckTokenForAnyScope
中间件可以分配给路由,以验证传入请求的访问令牌是否具有至少一个列出的范围:
use Laravel\Passport\Http\Middleware\CheckTokenForAnyScope;
Route::get('/orders', function () {
// 访问令牌具有 "orders:read" 或 "orders:create" 范围...
})->middleware(['auth:api', CheckTokenForAnyScope::using('orders:read', 'orders:create');
检查令牌实例上的范围
一旦访问令牌认证的请求进入您的应用程序,您仍然可以使用经过身份验证的 App\Models\User
实例上的 tokenCan
方法检查令牌是否具有给定范围:
use Illuminate\Http\Request;
Route::get('/orders', function (Request $request) {
if ($request->user()->tokenCan('orders:create')) {
// ...
}
});
其他范围方法
scopeIds
方法将返回所有定义的 ID / 名称的数组:
use Laravel\Passport\Passport;
Passport::scopeIds();
scopes
方法将返回所有定义的范围作为 Laravel\Passport\Scope
实例的数组:
Passport::scopes();
scopesFor
方法将返回与给定 ID / 名称匹配的 Laravel\Passport\Scope
实例的数组:
Passport::scopesFor(['user:read', 'orders:create']);
您可以使用 hasScope
方法确定是否已定义给定范围:
Passport::hasScope('orders:create');
SPA 身份验证
在构建 API 时,能够从您的 JavaScript 应用程序消费自己的 API 是非常有用的。这种 API 开发方法允许您的应用程序消费与您共享给世界的相同 API。相同的 API 可以被您的 Web 应用程序、移动应用程序、第三方应用程序以及您可能在各种包管理器上发布的任何 SDK 消费。
通常,如果您想从 JavaScript 应用程序消费您的 API,您需要手动将访问令牌发送到应用程序,并在每个请求中传递它。然而,Passport 包含一个中间件,可以为您处理此问题。您只需将 CreateFreshApiToken
中间件附加到应用程序的 bootstrap/app.php
文件中的 web
中间件组:
use Laravel\Passport\Http\Middleware\CreateFreshApiToken;
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
CreateFreshApiToken::class,
]);
})
WARNING
您应确保 CreateFreshApiToken
中间件是中间件堆栈中列出的最后一个中间件。
此中间件将 laravel_token
cookie 附加到您的传出响应中。此 cookie 包含一个加密的 JWT,Passport 将使用它来验证来自您的 JavaScript 应用程序的 API 请求。JWT 的生命周期等于您的 session.lifetime
配置值。现在,由于浏览器将自动发送 cookie 与所有后续请求,您可以在不显式传递访问令牌的情况下向应用程序的 API 发出请求:
axios.get("/api/user").then((response) => {
console.log(response.data);
});
自定义 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('orders can be created', function () {
Passport::actingAs(
User::factory()->create(),
['orders:create']
);
$response = $this->post('/api/orders');
$response->assertStatus(201);
});
use App\Models\User;
use Laravel\Passport\Passport;
public function test_orders_can_be_created(): void
{
Passport::actingAs(
User::factory()->create(),
['orders:create']
);
$response = $this->post('/api/orders');
$response->assertStatus(201);
}
Passport 的 actingAsClient
方法可用于指定当前认证的客户端及其范围。传递给 actingAsClient
方法的第一个参数是客户端实例,第二个参数是应授予客户端令牌的范围数组:
use Laravel\Passport\Client;
use Laravel\Passport\Passport;
test('servers can be retrieved', function () {
Passport::actingAsClient(
Client::factory()->create(),
['servers:read']
);
$response = $this->get('/api/servers');
$response->assertStatus(200);
});
use Laravel\Passport\Client;
use Laravel\Passport\Passport;
public function test_servers_can_be_retrieved(): void
{
Passport::actingAsClient(
Client::factory()->create(),
['servers:read']
);
$response = $this->get('/api/servers');
$response->assertStatus(200);
}