Skip to content

Laravel Passport ​

简介 ​

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

NOTE

本文档假设你已经熟悉 OAuth2。如果你对 OAuth2 还不了解,建议先了解 OAuth2 的通用术语和特性,再继续阅读。

Passport 还是 Sanctum? ​

开始之前,你可能需要先判断你的应用更适合使用 Laravel Passport 还是 Laravel Sanctum。如果你的应用明确需要支持 OAuth2,那么应使用 Laravel Passport。

但如果你只是想为单页应用、移动应用,或一般的 API token 场景提供认证,则应使用 Laravel Sanctum。Laravel Sanctum 不支持 OAuth2,但它提供了简单得多的 API 认证开发体验。

安装 ​

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

shell
php artisan install:api --passport

这个命令会发布并运行数据库迁移,用于创建应用存储 OAuth2 client 和 access token 所需的数据表。该命令还会创建生成安全 access token 所需的加密密钥。

运行 install:api 命令后,请将 Laravel\Passport\HasApiTokens trait 和 Laravel\Passport\Contracts\OAuthenticatable 接口添加到你的 App\Models\User 模型中。这个 trait 会为模型提供一些辅助方法,使你可以检查已认证用户的 token 与 scope:

php
<?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 认证 guard,并将其 driver 选项设置为 passport。这样应用在认证传入 API 请求时就会使用 Passport 的 TokenGuard:

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

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

部署 Passport ​

首次将 Passport 部署到应用服务器时,你很可能需要运行 passport:keys 命令。这个命令会生成 Passport 用来创建 access token 的加密密钥。生成的密钥通常不会纳入源码控制:

shell
php artisan passport:keys

如果需要,你还可以定义 Passport 应从哪个路径加载密钥。你可以使用 Passport::loadKeysFrom 方法完成此操作。通常,这个方法应在应用 App\Providers\AppServiceProvider 类的 boot 方法中调用:

php
/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Passport::loadKeysFrom(__DIR__.'/../secrets/oauth');
}

从环境变量加载密钥 ​

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

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

配置文件发布后,你可以通过环境变量来加载应用的加密密钥:

ini
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 的新主版本时,务必仔细阅读升级指南。

配置 ​

Token 生命周期 ​

默认情况下,Passport 签发的 access token 生命周期较长,会在一年后过期。如果你希望配置更长或更短的 token 生命周期,可以使用 tokensExpireIn、refreshTokensExpireIn 和 personalAccessTokensExpireIn 方法。这些方法应在应用 App\Providers\AppServiceProvider 类的 boot 方法中调用:

php
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 列是只读的,仅用于展示。当签发 token 时,Passport 会将过期信息存储在签名并加密后的 token 内部。如果你需要让某个 token 失效,应当撤销它。

覆盖默认模型 ​

你可以自由扩展 Passport 内部使用的模型,只需定义自己的模型并继承对应的 Passport 模型:

php
use Laravel\Passport\Client as PassportClient;

class Client extends PassportClient
{
    // ...
}

定义模型后,你可以通过 Laravel\Passport\Passport 类告诉 Passport 使用你的自定义模型。通常,你应在应用 App\Providers\AppServiceProvider 类的 boot 方法中完成这项配置:

php
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 定义的路由。要实现这一点,首先需要在应用 AppServiceProvider 的 register 方法中添加 Passport::ignoreRoutes,以忽略 Passport 默认注册的路由:

php
use Laravel\Passport\Passport;

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

然后,你可以将 Passport 在其路由文件中定义的路由复制到应用的 routes/web.php 文件中,并按需要修改:

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

Authorization Code Grant ​

大多数开发者熟悉的 OAuth2 用法,通常就是通过 authorization code 实现的。在使用 authorization code 时,client 应用会将用户重定向到你的服务器,在那里用户可以批准或拒绝向 client 签发 access token 的请求。

首先,我们需要告诉 Passport 应该如何返回“授权”视图。

所有授权视图的渲染逻辑都可以通过 Laravel\Passport\Passport 类提供的相应方法自定义。通常,你应在应用 App\Providers\AppServiceProvider 类的 boot 方法中调用这个方法:

php
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 字段。

管理 Clients ​

开发需要与应用 API 交互的应用时,开发者需要通过创建一个“client”来将他们的应用注册到你的系统中。通常,这意味着需要提供应用名称,以及当用户批准授权请求后,你的应用可重定向回去的 URI。

First-Party Clients ​

创建 client 的最简单方式是使用 passport:client Artisan 命令。这个命令可用于创建 first-party client,或测试 OAuth2 功能。运行 passport:client 命令时,Passport 会提示你输入更多 client 信息,并返回 client ID 和 secret:

shell
php artisan passport:client

如果你希望允许一个 client 使用多个重定向 URI,可以在 passport:client 命令询问 URI 时,使用逗号分隔列表来指定。任何本身包含逗号的 URI 都应先进行 URI 编码:

shell
https://third-party-app.com/callback,https://example.com/oauth/redirect

Third-Party Clients ​

由于你的应用用户无法直接使用 passport:client 命令,因此可以使用 Laravel\Passport\ClientRepository 类的 createAuthorizationCodeGrantClient 方法,为指定用户注册 client:

php
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 作为 client ID 展示给用户,并将 $client->plainSecret 作为 client secret 展示给用户。

请求 Tokens ​

重定向进行授权 ​

创建 client 后,开发者就可以使用 client ID 和 secret,从你的应用中请求 authorization code 和 access token。首先,使用方应用应当将用户重定向到你的应用的 /oauth/authorize 路由,例如:

php
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,即使使用方应用之前已经获得了所有所请求的 scopes,Passport 也总会显示授权批准界面。当值为 login 时,即使用户已经拥有现有会话,Passport 也总会要求用户重新登录。

如果未提供 prompt 值,那么只有当用户此前尚未为所请求 scopes 授权给使用方应用时,Passport 才会提示用户进行授权。

NOTE

请记住,/oauth/authorize 路由已经由 Passport 预定义,无需手动定义。

批准请求 ​

在收到授权请求时,Passport 会根据 prompt 参数(如果存在)的值自动作出响应,并可能向用户展示一个模板,让其批准或拒绝授权请求。如果用户批准请求,则会被重定向回使用方应用指定的 redirect_uri。该 redirect_uri 必须与创建 client 时指定的 redirect URL 一致。

有时你可能希望跳过授权提示,例如在授权 first-party client 时。你可以通过扩展 Client 模型并定义 skipsAuthorization 方法来实现。如果 skipsAuthorization 返回 true,则 client 会被自动批准,并立即将用户重定向回 redirect_uri,除非使用方应用在发起授权重定向时显式设置了 prompt 参数:

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

将 Authorization Code 换成 Access Token ​

如果用户批准了授权请求,他们会被重定向回使用方应用。使用方应首先校验 state 参数,确保它与重定向前保存的值一致。如果 state 参数匹配,则使用方应向你的应用发起一个 POST 请求,以请求 access token。该请求应包含用户批准授权请求时由你的应用签发的 authorization code:

php
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 属性包含 access token 距离过期还剩的秒数。

NOTE

与 /oauth/authorize 路由一样,/oauth/token 路由也已经由 Passport 预定义,无需手动定义。

管理 Tokens ​

你可以使用 Laravel\Passport\HasApiTokens trait 提供的 tokens 方法,获取用户已授权的 tokens。例如,这可用于为用户提供一个仪表板,以跟踪他们与第三方应用的连接关系:

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

刷新 Tokens ​

如果你的应用签发的是短生命周期的 access token,用户就需要通过 access token 签发时一同返回的 refresh token 来刷新 token:

php
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 路由会返回一个 JSON 响应,其中包含 access_token、refresh_token 和 expires_in 属性。expires_in 属性表示 access token 距离过期还剩的秒数。

撤销 Tokens ​

你可以通过 Laravel\Passport\Token 模型上的 revoke 方法撤销 token。你也可以通过 Laravel\Passport\RefreshToken 模型上的 revoke 方法撤销 token 对应的 refresh token:

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

清理 Tokens ​

当 token 已被撤销或过期时,你可能希望将其从数据库中清理掉。Passport 附带的 passport:purge Artisan 命令可以帮你完成这件事:

shell
# 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 文件中配置一个计划任务,从而按计划自动清理 token:

php
use Illuminate\Support\Facades\Schedule;

Schedule::command('passport:purge')->hourly();

含 PKCE 的 Authorization Code Grant ​

带有 “Proof Key for Code Exchange”(PKCE)的 Authorization Code grant 是一种更安全的方式,可用于认证单页应用或移动应用来访问你的 API。当你无法保证 client secret 可以被机密存储,或需要缓解 authorization code 被攻击者截获的风险时,应使用这种授权方式。它会使用“code verifier”和“code challenge”的组合来替代 client secret,以便将 authorization code 换取 access token。

创建 Client ​

在应用能够通过带 PKCE 的 authorization code grant 签发 token 之前,你需要先创建一个启用了 PKCE 的 client。你可以通过 passport:client Artisan 命令并配合 --public 选项来完成:

shell
php artisan passport:client --public

请求 Tokens ​

Code Verifier 与 Code Challenge ​

由于这种授权方式不会提供 client secret,因此开发者必须生成一组 code verifier 和 code challenge,用于请求 token。

根据 RFC 7636 规范,code verifier 应是长度 43 到 128 个字符之间的随机字符串,可包含字母、数字以及 "-"、 "."、"_"、"~" 字符。

code challenge 应是一个经过 Base64 编码且仅包含 URL 与文件名安全字符的字符串。结尾的 '=' 字符应去除,并且不能包含换行、空白或其他额外字符。

php
$encoded = base64_encode(hash('sha256', $codeVerifier, true));

$codeChallenge = strtr(rtrim($encoded, '='), '+/', '-_');

重定向进行授权 ​

创建 client 后,你就可以使用 client ID 以及生成好的 code verifier 与 code challenge,从应用请求 authorization code 和 access token。首先,使用方应用应将用户重定向到你的应用的 /oauth/authorize 路由:

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

将 Authorization Code 换成 Access Token ​

如果用户批准了授权请求,他们会被重定向回使用方应用。使用方应像标准 Authorization Code Grant 那样,校验 state 参数与重定向前保存的值是否一致。

如果 state 参数匹配,则使用方应向你的应用发起一个 POST 请求,以请求 access token。该请求应包含用户批准授权请求时由你的应用签发的 authorization code,以及最初生成的 code verifier:

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

Device Authorization Grant ​

OAuth2 的 device authorization grant 允许无浏览器或输入能力受限的设备,例如电视和游戏主机,通过交换“device code”来获取 access token。在 device flow 中,设备 client 会要求用户使用另一台设备,例如电脑或手机,访问你的服务器,在那里输入提供的“user code”,并批准或拒绝访问请求。

首先,我们需要告诉 Passport 应如何返回“user code”视图和“authorization”视图。

所有授权视图的渲染逻辑都可以通过 Laravel\Passport\Passport 类提供的相应方法进行自定义。通常,你应在应用 App\Providers\AppServiceProvider 类的 boot 方法中调用这些方法。

php
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 字段。

创建 Device Authorization Grant Client ​

在应用能够通过 device authorization grant 签发 token 之前,你需要先创建一个启用了 device flow 的 client。你可以通过带有 --device 选项的 passport:client Artisan 命令来完成。这个命令会创建一个启用了 device flow 的 first-party client,并返回 client ID 与 secret:

shell
php artisan passport:client --device

此外,你也可以在 ClientRepository 类上使用 createDeviceAuthorizationGrantClient 方法,为指定用户注册一个 third-party client:

php
use App\Models\User;
use Laravel\Passport\ClientRepository;

$user = User::find($userId);

$client = app(ClientRepository::class)->createDeviceAuthorizationGrantClient(
    user: $user,
    name: 'Example Device',
    confidential: false,
);

请求 Tokens ​

请求 Device Code ​

创建 client 后,开发者就可以使用 client ID 从你的应用中请求 device code。首先,使用方设备应向你的应用的 /oauth/device/code 路由发起一个 POST 请求,以请求 device code:

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

它会返回一个 JSON 响应,其中包含 device_code、user_code、verification_uri、interval 和 expires_in 属性。expires_in 表示 device code 还剩多少秒过期。interval 表示使用方设备在轮询 /oauth/token 路由时,应等待多少秒,以避免触发限流错误。

NOTE

请记住,/oauth/device/code 路由已经由 Passport 定义,无需手动定义。

显示 Verification URI 和 User Code ​

获取 device code 响应后,使用方设备应提示用户使用另一台设备访问返回的 verification_uri,并输入 user_code,以批准授权请求。

轮询 Token 请求 ​

由于用户会在另一台设备上批准(或拒绝)访问,使用方设备应轮询你的应用的 /oauth/token 路由,以判断用户何时响应请求。为了避免限流错误,使用方设备应遵循请求 device code 时 JSON 响应中返回的最小轮询 interval:

php
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 表示 access token 距离过期还剩的秒数。

Password Grant ​

WARNING

我们不再推荐使用 password grant token。相反,你应选择 OAuth2 Server 当前推荐的 grant type。

OAuth2 password grant 允许其他 first-party client(例如移动应用)使用邮箱地址 / 用户名和密码来获取 access token。这使你可以安全地为 first-party client 签发 access token,而无需让用户经过完整的 OAuth2 authorization code 重定向流程。

要启用 password grant,请在应用 App\Providers\AppServiceProvider 类的 boot 方法中调用 enablePasswordGrant 方法:

php
/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Passport::enablePasswordGrant();
}

创建 Password Grant Client ​

在应用能够通过 password grant 签发 token 之前,你需要先创建一个 password grant client。你可以通过带有 --password 选项的 passport:client Artisan 命令来完成:

shell
php artisan passport:client --password

请求 Tokens ​

启用该 grant 并创建好 password grant client 后,你就可以向 /oauth/token 路由发起 POST 请求,并携带用户的邮箱地址与密码,以请求 access token。请记住,这个路由已经由 Passport 注册,无需手动定义。如果请求成功,你将在服务端返回的 JSON 响应中收到 access_token 和 refresh_token:

php
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

请记住,access token 默认是长生命周期的。不过,如果需要,你可以自由地配置 access token 的最大生命周期。

请求所有 Scopes ​

当使用 password grant 或 client credentials grant 时,你可能希望授权 token 使用应用支持的所有 scope。你可以通过请求 * scope 来实现。如果你请求了 * scope,那么 token 实例上的 can方法将始终返回true。此 scope 只能分配给通过 password或client_credentials` grant 签发的 token:

php
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 Provider ​

如果你的应用使用了多个认证 user provider,那么在通过 artisan passport:client --password 命令创建 client 时,可以通过 --provider 选项指定 password grant client 所使用的 user provider。提供的 provider 名称必须与应用 config/auth.php 配置文件中定义的有效 provider 一致。之后,你就可以通过中间件保护路由,确保只有来自该 guard 指定 provider 的用户被授权。

自定义用户名字段 ​

在使用 password grant 进行认证时,Passport 默认会将可认证模型的 email 属性作为“用户名”。不过,你也可以通过在模型上定义 findForPassport 方法来自定义这一行为:

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

自定义密码验证 ​

在使用 password grant 进行认证时,Passport 默认会使用模型的 password 属性来验证密码。如果你的模型没有 password 属性,或你想自定义密码验证逻辑,可以在模型上定义 validateForPassportPasswordGrant 方法:

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

Implicit Grant ​

WARNING

我们不再推荐使用 implicit grant token。相反,你应选择 OAuth2 Server 当前推荐的 grant type。

Implicit grant 与 authorization code grant 类似;不过,它不会通过 authorization code 交换 token,而是直接把 token 返回给 client。这种授权方式最常用于 JavaScript 或移动应用,因为这些场景下 client 凭证无法被安全存储。要启用它,请在应用 App\Providers\AppServiceProvider 类的 boot 方法中调用 enableImplicitGrant 方法:

php
/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Passport::enableImplicitGrant();
}

在应用能够通过 implicit grant 签发 token 之前,你需要先创建一个 implicit grant client。你可以通过带有 --implicit 选项的 passport:client Artisan 命令来完成:

shell
php artisan passport:client --implicit

启用该 grant 并创建 implicit client 后,开发者就可以使用 client ID 从你的应用中请求 access token。使用方应用应将用户重定向到你的应用的 /oauth/authorize 路由,例如:

php
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 定义,无需手动定义。

Client Credentials Grant ​

Client credentials grant 适用于机器到机器的认证。例如,你可以在一个通过 API 执行维护任务的计划任务中使用这种 grant。

在应用能够通过 client credentials grant 签发 token 之前,你需要先创建一个 client credentials grant client。你可以使用 passport:client Artisan 命令的 --client 选项来完成:

shell
php artisan passport:client --client

接着,将 Laravel\Passport\Http\Middleware\EnsureClientIsResourceOwner 中间件分配给路由:

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

如果你想将访问限制到特定 scope,可以将所需的 scope 列表提供给 using 方法:

php
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'));

WARNING

底层 OAuth2 服务器为客户端凭据令牌将令牌的 sub 声明设置为客户端的标识符。默认情况下,Passport 为客户端使用 UUID,因此这不会与用户的整数主键冲突。但是,如果你将 Passport::$clientUuids 设置为 false,客户端凭据令牌可能会无意中解析出 ID 与客户端 ID 匹配的用户。在这种情况下,使用此中间件无法保证传入的令牌是客户端凭据令牌。

获取 Tokens ​

要通过这种 grant type 获取 token,请向 oauth/token 端点发起请求:

php
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'];

Personal Access Tokens ​

有时,用户可能希望跳过典型的 authorization code 重定向流程,直接给自己签发 access token。允许用户通过应用的 UI 为自己签发 token,对于让用户试验 API 很有帮助;或者,更普遍地说,这也是一种更简单的 access token 签发方式。

NOTE

如果你的应用使用 Passport 的主要目的是签发 personal access token,那么请考虑使用 Laravel Sanctum。它是 Laravel 为签发 API access token 提供的轻量级一方库。

创建 Personal Access Client ​

在应用能够签发 personal access token 之前,你需要先创建 personal access client。你可以通过带有 --personal 选项的 passport:client Artisan 命令来完成。如果你已经运行过 passport:install 命令,则不需要再运行此命令:

shell
php artisan passport:client --personal

自定义 User Provider ​

如果你的应用使用了多个认证 user provider,那么在通过 artisan passport:client --personal 命令创建 client 时,可以通过 --provider 选项指定 personal access grant client 所使用的 user provider。提供的 provider 名称必须与应用 config/auth.php 配置文件中定义的有效 provider 一致。之后,你就可以通过中间件保护路由,确保只有来自该 guard 指定 provider 的用户被授权。

管理 Personal Access Tokens ​

创建好 personal access client 后,你可以在 App\Models\User 模型实例上调用 createToken 方法,为指定用户签发 token。createToken 方法的第一个参数是 token 名称,第二个可选参数是 scopes 数组:

php
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 包含一个认证 guard,它会验证传入请求中的 access token。在将 api guard 配置为使用 passport driver 后,你只需要为需要有效 access token 的路由指定 auth:api 中间件即可:

php
Route::get('/user', function () {
    // Only API authenticated users may access this route...
})->middleware('auth:api');

WARNING

如果你使用的是 client credentials grant,则应当使用Laravel\Passport\Http\Middleware\EnsureClientIsResourceOwner 中间件来保护路由,而不是使用 auth:api 中间件。

多个认证 Guards ​

如果你的应用会认证不同类型的用户,而这些用户可能使用完全不同的 Eloquent 模型,那么你很可能需要为每种 user provider 类型定义对应的 guard 配置。这样你就可以保护那些只面向特定 user provider 的请求。例如,在 config/auth.php 配置文件中有如下 guard 配置:

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

    'api-customers' => [
        'driver' => 'passport',
        'provider' => 'customers',
    ],
],

下面这个路由会使用 api-customers guard(它使用 customers user provider)来认证传入请求:

php
Route::get('/customer', function () {
    // ...
})->middleware('auth:api-customers');

NOTE

关于在 Passport 中使用多个 user provider 的更多信息,请参考personal access tokens 文档和password grant 文档。

传递 Access Token ​

在调用受 Passport 保护的路由时,应用的 API 使用者应在请求的 Authorization header 中,将 access token 作为 Bearer token 提供。例如,在使用 Http Facade 时:

php
use Illuminate\Support\Facades\Http;

$response = Http::withHeaders([
    'Accept' => 'application/json',
    'Authorization' => "Bearer $accessToken",
])->get('https://passport-app.test/api/user');

return $response->json();

Token Scopes ​

Scope 允许 API client 在请求授权访问某个账号时,申请一组明确的权限。例如,如果你在构建一个电商应用,并非所有 API 使用方都需要具备下单能力。相反,你可以只允许这些使用方请求“查看订单发货状态”的授权。换句话说,scope 允许应用用户限制第三方应用代表自己可以执行的操作。

定义 Scopes ​

你可以在应用 App\Providers\AppServiceProvider 类的 boot 方法中,使用 Passport::tokensCan 方法定义 API 的 scope。tokensCan 方法接收一个 scope 名称到 scope 描述的数组。scope 描述可以是任意你希望显示给用户的内容,它会显示在授权批准界面中:

php
/**
 * 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',
    ]);
}

默认 Scope ​

如果某个 client 没有请求任何特定 scope,你可以使用 defaultScopes 方法,配置 Passport 服务器为该 token 自动附加默认 scope。通常,你应在应用 App\Providers\AppServiceProvider 类的 boot 方法中调用此方法:

php
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',
]);

为 Tokens 分配 Scopes ​

请求 Authorization Code 时 ​

通过 authorization code grant 请求 access token 时,使用方应通过查询字符串参数 scope 指定所需的 scope。scope 参数应是一个以空格分隔的 scope 列表:

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

签发 Personal Access Tokens 时 ​

如果你使用 App\Models\User 模型的 createToken 方法签发 personal access token,那么可以将所需 scope 数组作为该方法的第二个参数传入:

php
$token = $user->createToken('My Token', ['orders:create'])->accessToken;

检查 Scopes ​

Passport 提供了两个中间件,可用于校验传入请求是否是通过被授予指定 scope 的 token 完成认证的。

检查是否拥有全部 Scopes ​

可以将 Laravel\Passport\Http\Middleware\CheckToken 中间件分配给路由,以验证传入请求的 access token 是否拥有列出的全部 scopes:

php
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')]);

检查是否拥有任一 Scope ​

可以将 Laravel\Passport\Http\Middleware\CheckTokenForAnyScope 中间件分配给路由,以验证传入请求的 access token 是否至少拥有所列 scopes 中的一个:

php
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')]);

Scope 属性 ​

如果你的应用使用 controller middleware 属性,你可以使用 Laravel\Passport\Attributes\AuthorizeToken 属性作为 Passport scope middleware 的便捷快捷方式:

php
<?php

namespace App\Http\Controllers;

use Laravel\Passport\Attributes\AuthorizeToken;

#[AuthorizeToken('orders:read')]
#[AuthorizeToken('orders:create', only: ['store'])]
class OrderController
{
    #[AuthorizeToken(['orders:read', 'orders:create'], anyScope: true)]
    public function index()
    {
        // Access token 拥有 "orders:read" 或 "orders:create" scope 之一...
    }

    public function store()
    {
        // Access token 同时拥有 "orders:read" 和 "orders:create" scopes...
    }
}

默认情况下,AuthorizeToken 属性要求拥有所有给定的 scopes。如果你传递 anyScope: true,则当 token 拥有给定 scopes 中的至少一个时,请求就会被授权。

在 Token 实例上检查 Scopes ​

当一个通过 access token 认证的请求进入应用后,你仍然可以在已认证的 App\Models\User 实例上使用 tokenCan 方法来检查 token 是否拥有某个指定 scope:

php
use Illuminate\Http\Request;

Route::get('/orders', function (Request $request) {
    if ($request->user()->tokenCan('orders:create')) {
        // ...
    }
});

其他 Scope 方法 ​

scopeIds 方法会返回所有已定义 ID / 名称的数组:

php
use Laravel\Passport\Passport;

Passport::scopeIds();

scopes 方法会返回所有已定义 scope 的数组,数组元素是 Laravel\Passport\Scope 实例:

php
Passport::scopes();

scopesFor 方法会返回与给定 ID / 名称匹配的 Laravel\Passport\Scope 实例数组:

php
Passport::scopesFor(['user:read', 'orders:create']);

你还可以使用 hasScope 方法判断某个 scope 是否已经被定义:

php
Passport::hasScope('orders:create');

SPA 认证 ​

在构建 API 时,能够从你自己的 JavaScript 应用中消费自己的 API 通常非常有用。这样的 API 开发方式允许你的应用自身也消费你公开给外界的同一套 API。同一套 API 既可以被 Web 应用消费,也可以被移动应用、第三方应用,以及你发布到各种包管理器上的 SDK 消费。

通常,如果你想从 JavaScript 应用中消费 API,就需要手动向应用发送 access token,并在每个请求中附带它。不过,Passport 提供了一个中间件可以替你处理这件事。你只需要在应用 bootstrap/app.php 文件的 web 中间件组中追加 CreateFreshApiToken 中间件:

php
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,因此你可以在不显式传递 access token 的情况下调用应用 API:

js
axios.get('/api/user')
    .then(response => {
        console.log(response.data);
    });

如有需要,你可以使用 Passport::cookie 方法来自定义 laravel_token cookie 的名称。通常,这个方法应在应用 App\Providers\AppServiceProvider 类的 boot 方法中调用:

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

CSRF 保护 ​

在使用这种认证方式时,你需要确保请求中包含有效的 CSRF token header。Laravel 默认 skeleton 应用和所有 starter kit 自带的 JavaScript 脚手架都包含一个 Axios 实例,它会自动使用加密后的 XSRF-TOKEN cookie 值,在同源请求中发送 X-XSRF-TOKEN header。

NOTE

如果你选择发送 X-CSRF-TOKEN header,而不是 X-XSRF-TOKEN,那么需要使用 csrf_token() 提供的未加密 token。

事件 ​

Passport 在签发 access token 和 refresh token 时会触发事件。你可以监听这些事件,以清理或撤销数据库中的其他 access token:

Event Name
Laravel\Passport\Events\AccessTokenCreated
Laravel\Passport\Events\AccessTokenRevoked
Laravel\Passport\Events\RefreshTokenCreated

测试 ​

Passport 的 actingAs 方法可用于指定当前已认证用户及其 scopes。传给 actingAs 方法的第一个参数是用户实例,第二个参数是应授予该用户 token 的 scope 数组:

php
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);
});
php
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 方法可用于指定当前已认证 client 及其 scopes。传给 actingAsClient 方法的第一个参数是 client 实例,第二个参数是应授予该 client token 的 scope 数组:

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