Laravel Passport 在几分钟内为您的 Laravel 应用程序提供完整的 OAuth2 服务器实现。Passport 基于 League OAuth2 服务器 由 Andy Millington 和 Simon Hamp 维护。
[!注意]
本文档假设您已熟悉 OAuth2。如果您对 OAuth2 一无所知,请考虑熟悉 OAuth2 的一般术语和特性再继续。
在开始之前,您可能希望确定您的应用程序是否更适合使用 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: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 会签发一年后过期的长期访问令牌。如果您想配置更长或更短的令牌生命周期,您可以使用 tokensExpireIn, refreshTokensExpireIn, 和 personalAccessTokensExpireIn 方法。这些方法应该从您的应用程序的 App\Providers\AppServiceProvider 类的 boot 方法中调用:
use Carbon\CarbonInterval;
/**
* Bootstrap any application services.
*/
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;
use Laravel\Passport\Passport;
/**
* 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::useDeviceCodeModel(DeviceCode::class);
}有时您可能希望自定义由 Passport 定义的路由。为此,您首先需要通过将 Passport::ignoreRoutes 添加到应用程序的 AppServiceProvider 的 register 方法中,来忽略由 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 的方式。在使用授权码时,客户端应用程序会将用户重定向到您的服务器,用户将在那里批准或拒绝向客户端颁发访问令牌的请求。
要开始,我们需要指示 Passport 如何返回我们的“授权”视图。
所有授权视图的渲染逻辑都可以使用通过 Laravel\Passport\Passport 类提供的相应方法进行自定义。通常,你应该在你的应用程序的 App\Providers\AppServiceProvider 类的 boot 方法中调用此方法:
use Inertia\Inertia;
use Laravel\Passport\Passport;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
// By providing a view name...
Passport::authorizationView('auth.oauth.authorize');
// By providing a closure...
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。
创建客户端最简单的方式是使用 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 命令,您可以使用 createAuthorizationCodeGrantClient 方法所属的 Laravel\Passport\ClientRepository 类,以便为给定用户注册客户端:
use App\Models\User;
use Laravel\Passport\ClientRepository;
$user = User::find($userId);
// Creating an OAuth app client that belongs to the given user...
$client = app(ClientRepository::class)->createAuthorizationCodeGrantClient(
user: $user,
name: 'Example App',
redirectUris: ['https://third-party-app.com/callback'],
confidential: false,
enableDeviceFlow: true
);
// Retrieving all the OAuth app clients that belong to the user...
$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
{
/**
* Determine if the client should skip the authorization prompt.
*
* @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 路由将返回一个 JSON 响应,包含 access_token、refresh_token 和 expires_in 属性。此 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);
// Retrieving all of the valid tokens for the user...
$tokens = $user->tokens()
->where('revoked', false)
->where('expires_at', `>`, Date::now())
->get();
// Retrieving all the user's connections to third-party OAuth app clients...
$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', // Required for confidential clients only...
'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);
// Revoke an access token...
$token->revoke();
// Revoke the token's refresh token...
$token->refreshToken?->revoke();
// Revoke all of the user's tokens...
User::find($userId)->tokens()->each(function (Token $token) {
$token->revoke();
$token->refreshToken?->revoke();
});当令牌被撤销或过期时,你可能希望从数据库中清除它们。Passport 附带的 passport:purge Artisan 命令可以为你完成此操作:
# Purge revoked and expired tokens, auth codes, and device codes...
php artisan passport:purge
# Only purge tokens expired for more than 6 hours...
php artisan passport:purge --hours=6
# Only purge revoked tokens, auth codes, and device codes...
php artisan passport:purge --revoked
# Only purge expired tokens, auth codes, and device codes...
php artisan passport:purge --expired您还可以配置一个调度任务在您应用程序的routes/console.php文件中,以便定期自动修剪您的令牌:
use Illuminate\Support\Facades\Schedule;
Schedule::command('passport:purge')->hourly();带有“代码交换证明密钥”(PKCE)的授权码授权是一种安全的方式,用于认证单页应用程序或移动应用程序以访问您的API。此授权应在您无法保证客户端密钥能够机密存储时使用,或为了减轻授权码被攻击者拦截的威胁。在用授权码交换访问令牌时,“代码验证器”和“代码挑战”的组合取代了客户端密钥。
在您的应用程序可以通过带有 PKCE 的授权码授权颁发令牌之前,您需要创建一个启用了 PKCE 的客户端。您可以使用带有 --public 选项的 passport:client Artisan 命令来完成此操作:
php artisan passport:client --public由于此授权许可不提供客户端密钥,开发者将需要生成一个代码验证器和代码质询的组合,以便请求令牌。
代码验证器应该是一个长度在 43 到 128 个字符之间的随机字符串 包含字母、数字和 "-",".","_","~" 字符,正如 RFC 7636 规范 中所定义。
代码挑战应该是一个包含URL和文件名安全字符的Base64编码字符串。末尾的'='字符应该被移除,并且不应该有换行符、空格或其他额外字符。
$encoded = base64_encode(hash('sha256', $codeVerifier, true));
$codeChallenge = strtr(rtrim($encoded, '='), '+/', '-_');一旦客户端创建成功,您可以使用客户端 ID 以及生成的 code verifier 和 code challenge 来从您的应用程序请求 authorization code 和 access token。首先,消费应用程序应该发起一个重定向请求到您的应用程序的 /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 Inertia\Inertia;
use Laravel\Passport\Passport;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
// By providing a view name...
Passport::deviceUserCodeView('auth.oauth.device.user-code');
Passport::deviceAuthorizationView('auth.oauth.device.authorize');
// By providing a closure...
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此外,您可以使用 createDeviceAuthorizationGrantClient 方法在 ClientRepository 类上来注册一个属于给定用户的第三方客户端:
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 从您的应用请求设备码。首先,消费设备应发起一个 POST 请求到您的应用的 /oauth/device/code 路由以请求设备码:
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 定义。您无需手动定义此路由。
一旦获取到设备代码请求,使用设备应指示用户使用另一设备并访问提供的 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', // Required for confidential clients only...
'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();如果用户已批准授权请求,这将返回一个 JSON 响应,其中包含 access_token、refresh_token 和 expires_in 属性。expires_in 属性包含访问令牌到期前的秒数。
[!WARNING]
我们不再推荐使用密码授权令牌。相反,您应该选择OAuth2 Server 当前推荐的一种授权类型。
OAuth2 密码授权允许您的其他第一方客户端,例如移动应用程序,使用电子邮件地址/用户名和密码获取访问令牌。这使您能够安全地向您的第一方客户端颁发访问令牌,而无需您的用户经历整个 OAuth2 授权码重定向流程。
要启用密码授权,在你的应用程序的 App\Providers\AppServiceProvider 类的 boot 方法中调用 enablePasswordGrant 方法:
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Passport::enablePasswordGrant();
}在您的应用程序可以通过密码授权颁发令牌之前,您需要创建一个密码授权客户端。您可以使用 passport:client Artisan 命令并带上 --password 选项来完成此操作。
php artisan passport:client --passwordOnce you have created a password grant client and enabled the client grant type, you may request an access token by issuing aPOSTrequest to the/oauth/tokenroute with the user's email address and and password. Remember, this route is already registered by Passport so there is no need to define it manually. If the request is successful, you will receive anaccess_tokenandrefresh_token` in the JSON response from the server:
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', // Required for confidential clients only...
'username' => 'taylor@laravel.com',
'password' => 'my-password',
'scope' => 'user:read orders:create',
]);
return $response->json();[!NOTE]
请记住,访问令牌默认情况下是长寿命的。但是,您可以自由地配置您的最大访问令牌生命周期如果需要。
当使用 password 授权或 client credentials 授权时,您可能希望授权令牌以支持您的应用程序支持的所有作用域。您可以通过请求 * 作用域来实现此操作。如果您请求 * 作用域,令牌实例上的 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', // Required for confidential clients only...
'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\Bridge\Client;
use Laravel\Passport\Contracts\OAuthenticatable;
use Laravel\Passport\HasApiTokens;
class User extends Authenticatable implements OAuthenticatable
{
use HasApiTokens, Notifiable;
/**
* Find the user instance for the given username.
*/
public function findForPassport(string $username, Client $client): 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\Contracts\OAuthenticatable;
use Laravel\Passport\HasApiTokens;
class User extends Authenticatable implements OAuthenticatable
{
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);
}
}[!警告]
我们不再推荐使用隐式授权令牌。相反,您应该选择 OAuth2 服务器当前推荐的一种授权类型。
隐式授权类似于授权码授权;但是,令牌会在不交换授权码的情况下返回给客户端。此授权最常用于无法安全存储客户端凭据的 JavaScript 或移动应用程序。要启用此授权,请在应用程序的 App\Providers\AppServiceProvider 类的 boot 方法中调用 enableImplicitGrant 方法:
/**
* Bootstrap any application services.
*/
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 定义。您无需手动定义此路由。
客户端凭据授权适用于机器到机器认证。例如,您可能会在计划作业中使用此授权,该作业通过 API 执行维护任务。
在你的应用可以通过客户端凭据授权发放令牌之前,你需要创建一个客户端凭据授权客户端。你可以使用 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) {
// Access token is valid and the client is resource owner...
})->middleware(EnsureClientIsResourceOwner::class);为了将路由的访问限制在特定的作用域,你可以将所需作用域的列表提供给 using 方法:
Route::get('/orders', function (Request $request) {
// Access token is valid, the client is resource owner, and has both "servers:read" and "servers:create" scopes...
})->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 可能很有用,或者可以作为一种更简单的方式来颁发访问令牌。
[!注意]
如果您的应用程序主要使用 Passport 来颁发个人访问令牌,请考虑使用 Laravel Sanctum,这是 Laravel 用于颁发 API 访问令牌的轻量级第一方库。
在你的应用程序能够发放个人访问令牌之前,你需要创建一个个人访问客户端。你 H可以通过执行带 --personal 选项的 passport:client Artisan 命令来完成此操作。如果你已经运行了 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);
// Creating a token without scopes...
$token = $user->createToken('My Token')->accessToken;
// Creating a token with scopes...
$token = $user->createToken('My Token', ['user:read', 'orders:create'])->accessToken;
// Creating a token with all scopes...
$token = $user->createToken('My Token', ['*'])->accessToken;
// Retrieving all the valid personal access tokens that belong to the user...
$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 () {
// Only API authenticated users may access this route...
})->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');[!NOTE]
有关将多个用户提供程序与 Passport 配合使用的更多信息,请查阅 个人访问令牌文档 和 密码授权文档。
当调用由 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 方法接受一个包含作用域名称和作用域描述的数组。作用域描述可以是您希望的任何内容,并将显示在用户授权批准屏幕上:
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Passport::tokensCan([
'user:read' => 'Retrieve the user info',
'orders:create' => 'Place orders',
'orders:read:status' => 'Check order status',
]);
}如果客户端未请求任何特定作用域,你可以配置你的 Passport 服务器,使其使用 defaultScopes 方法将默认作用域附加到令牌。通常,你应该在你的应用程序的 App\Providers\AppServiceProvider 类的 boot 方法中调用此方法:
use Laravel\Passport\Passport;
Passport::tokensCan([
'user:read' => 'Retrieve the user info',
'orders:create' => 'Place orders',
'orders:read:status' => 'Check order 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 () {
// Access token has both "orders:read" and "orders:create" scopes...
})->middleware(['auth:api', CheckToken::using('orders:read', 'orders:create')]);Laravel\Passport\Http\Middleware\CheckTokenForAnyScope 中间件可以被分配给一个路由,以验证传入请求的访问令牌是否具有至少一个所列出的范围:
use Laravel\Passport\Http\Middleware\CheckTokenForAnyScope;
Route::get('/orders', function () {
// Access token has either "orders:read" or "orders:create" scope...
})->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');在构建 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): void {
$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);
});如果需要,您可以使用 Passport::cookie 方法自定义 laravel_token cookie 的名称。通常,此方法应在您的应用程序的 App\Providers\AppServiceProvider 类的 boot 方法中调用:
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Passport::cookie('custom_name');
}当使用这种认证方式时,你需要确保在你的请求中包含一个有效的 CSRF 令牌头部。随骨架应用和所有入门套件一起提供的默认 Laravel JavaScript 脚手架包含一个 Axios 实例,它会自动使用加密的 XSRF-TOKEN cookie 值在同源请求中发送一个 X-XSRF-TOKEN 头部。
[!注意]
如果您选择发送X-CSRF-TOKEN头而不是X-XSRF-TOKEN,您将需要使用由csrf_token()提供的未加密令牌。
Passport 在颁发访问令牌和刷新令牌时会触发事件。你可以 监听这些事件 来修剪或撤销数据库中的其他访问令牌:
| Event Name |
|---|
Laravel\Passport\Events\AccessTokenCreated |
Laravel\Passport\Events\AccessTokenRevoked |
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);
}