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 生命周期,可以使用 tokensExpireInrefreshTokensExpireInpersonalAccessTokensExpireIn 方法。这些方法应在应用 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 定义的路由。要实现这一点,首先需要在应用 AppServiceProviderregister 方法中添加 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.approvepassport.authorizations.deny 路由都要求提供 stateclient_idauth_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_tokenrefresh_tokenexpires_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_tokenrefresh_tokenexpires_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.approvepassport.device.authorizations.deny 路由要求提供 stateclient_idauth_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_codeuser_codeverification_uriintervalexpires_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_tokenrefresh_tokenexpires_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_tokenrefresh_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 只能分配给通过 passwordclient_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'));

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

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