Skip to content
全部文档

Laravel Passport ​

简介 ​

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

WARNING

本文档假定你已熟悉 OAuth2。若尚不了解 OAuth2,建议先熟悉其通用术语与特性后再继续阅读。

Passport 还是 Sanctum? ​

开始之前,你可能需要判断应用更适合 Laravel Passport 还是 Laravel Sanctum。若应用必须支持 OAuth2,则应使用 Laravel Passport。

但若你要为单页应用、移动应用做认证,或签发 API 令牌,应使用 Laravel Sanctum。Laravel Sanctum 不支持 OAuth2,但提供更简单的 API 认证开发体验。

安装 ​

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

shell
php artisan install:api --passport

该命令会发布并运行数据库迁移,创建应用存储 OAuth2 客户端与访问令牌所需的表,同时生成用于签发安全访问令牌的加密密钥。

此外,该命令还会询问你是否希望将 Passport Client 模型的主键设为 UUID,而不是自增整数。

运行 install:api 命令后,为 App\Models\User 模型添加 Laravel\Passport\HasApiTokens trait。该 trait 会提供若干辅助方法,用于检查已认证用户的令牌与作用域:

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\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;
}

最后,在应用的 config/auth.php 中定义 api 认证守卫,并将 driver 设为 passport。这样应用在认证传入的 API 请求时会使用 Passport 的 TokenGuard:

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

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

部署 Passport ​

首次将 Passport 部署到应用服务器时,通常需要运行 passport:keys 命令。该命令会生成 Passport 签发访问令牌所需的加密密钥。生成的密钥通常不会纳入版本控制:

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

配置 ​

客户端密钥哈希 ​

若希望客户端密钥在存入数据库时被哈希,应在 App\Providers\AppServiceProvider 的 boot 方法中调用 Passport::hashClientSecrets:

use Laravel\Passport\Passport;

Passport::hashClientSecrets();

启用后,客户端密钥仅在创建后立即对用户可见。由于明文密钥不会存入数据库,一旦丢失将无法找回。

令牌生命周期 ​

默认情况下,Passport 签发有效期为一年的长期访问令牌。若要配置更长或更短的生命周期,可使用 tokensExpireIn、refreshTokensExpireIn 与 personalAccessTokensExpireIn 方法,通常应在 App\Providers\AppServiceProvider 的 boot 方法中调用:

php
/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Passport::tokensExpireIn(now()->addDays(15));
    Passport::refreshTokensExpireIn(now()->addDays(30));
    Passport::personalAccessTokensExpireIn(now()->addMonths(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 方法中配置:

use App\Models\Passport\AuthCode;
use App\Models\Passport\Client;
use App\Models\Passport\PersonalAccessClient;
use App\Models\Passport\RefreshToken;
use App\Models\Passport\Token;
php
/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Passport::useTokenModel(Token::class);
    Passport::useRefreshTokenModel(RefreshToken::class);
    Passport::useAuthCodeModel(AuthCode::class);
    Passport::useClientModel(Client::class);
    Passport::usePersonalAccessClientModel(PersonalAccessClient::class);
}

覆盖路由 ​

有时你可能希望自定义 Passport 定义的路由。为此,先在 AppServiceProvider 的 register 方法中添加 Passport::ignoreRoutes,忽略 Passport 注册的路由:

use Laravel\Passport\Passport;
php
/**
 * 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 是多数开发者熟悉的方式。使用授权码时,客户端应用会将用户重定向到你的服务器,由用户批准或拒绝向客户端签发访问令牌的请求。

管理客户端 ​

需要与你应用 API 交互的开发者,须通过创建「client」在你的应用中注册其应用。通常需提供应用名称,以及用户批准授权请求后你的应用可重定向到的 URI。

passport:client 命令 ​

创建客户端最简单的方式是使用 passport:client Artisan 命令。该命令可用于创建第一方客户端或测试 OAuth2 功能。运行 passport:client 时,Passport 会提示输入客户端信息,并返回 client ID 与 secret:

shell
php artisan passport:client

重定向 URL

若希望客户端支持多个重定向 URI,可在 passport:client 提示输入 URI 时以逗号分隔列表指定。包含逗号的 URI 应进行 URI 编码:

shell
http://example.com/callback,http://examplefoo.com/callback

JSON API ​

由于应用用户无法使用 client 命令,Passport 提供了可用于创建客户端的 JSON API,省去你手动编写创建、更新与删除客户端控制器的麻烦。

不过,你需要将 Passport 的 JSON API 与自己的前端搭配,为用户提供管理客户端的控制台。下面我们将回顾所有用于管理客户端的 API 端点。为方便演示,我们使用 Axios 向这些端点发起 HTTP 请求。

该 JSON API 受 web 与 auth 中间件保护,因此只能从你自己的应用内调用,无法从外部来源调用。

GET /oauth/clients ​

该路由返回已认证用户的所有客户端,主要用于列出用户的全部客户端,以便编辑或删除:

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

POST /oauth/clients ​

该路由用于创建新客户端,需要两段数据:客户端的 name 与 redirect URL。用户批准或拒绝授权请求后,将重定向到该 redirect URL。

创建客户端时会签发 client ID 与 client secret,请求应用的访问令牌时会用到这些值。创建客户端路由会返回新的客户端实例:

js
const data = {
    name: 'Client Name',
    redirect: 'http://example.com/callback'
};

axios.post('/oauth/clients', data)
    .then(response => {
        console.log(response.data);
    })
    .catch (response => {
        // List errors on response...
    });

PUT /oauth/clients/{client-id} ​

该路由用于更新客户端,需要两段数据:客户端的 name 与 redirect URL。用户批准或拒绝授权请求后,将重定向到该 redirect URL。路由会返回更新后的客户端实例:

js
const data = {
    name: 'New Client Name',
    redirect: 'http://example.com/callback'
};

axios.put('/oauth/clients/' + clientId, data)
    .then(response => {
        console.log(response.data);
    })
    .catch (response => {
        // List errors on response...
    });

DELETE /oauth/clients/{client-id} ​

该路由用于删除客户端:

js
axios.delete('/oauth/clients/' + clientId)
    .then(response => {
        // ...
    });

请求令牌 ​

重定向以进行授权 ​

客户端创建后,开发者可使用 client ID 与 secret 向你的应用请求授权码与访问令牌。首先,消费方应用应向应用的 /oauth/authorize 路由发起重定向请求,例如:

use Illuminate\Http\Request;
use Illuminate\Support\Str;

Route::get('/redirect', function (Request $request) {
    $request->session()->put('state', $state = Str::random(40));

    $query = http_build_query([
        'client_id' => 'client-id',
        'redirect_uri' => 'http://third-party-app.com/callback',
        'response_type' => 'code',
        'scope' => '',
        'state' => $state,
        // 'prompt' => '', // "none", "consent", or "login"
    ]);

    return redirect('http://passport-app.test/oauth/authorize?'.$query);
});

可使用 prompt 参数指定 Passport 应用的认证行为。

若 prompt 为 none,当用户尚未在 Passport 应用中认证时,Passport 将始终抛出认证错误。若为 consent,即使消费方应用此前已获全部作用域,Passport 仍会显示授权批准界面。若为 login,即使已有会话,Passport 也会提示用户重新登录。

若未提供 prompt,仅当用户此前未就所请求作用域授权消费方应用访问时,才会提示授权。

INFO

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

批准请求 ​

收到授权请求时,Passport 会根据 prompt 参数(如有)自动响应,并可能向用户展示模板以批准或拒绝授权。若用户批准,将重定向回消费方应用指定的 redirect_uri,且 redirect_uri 必须与创建客户端时指定的 redirect URL 一致。

若要自定义授权批准界面,可使用 vendor:publish Artisan 命令发布 Passport 视图。发布后的视图位于 resources/views/vendor/passport 目录:

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

有时你可能希望跳过授权提示,例如授权第一方客户端时。可扩展 Client 模型并定义 skipsAuthorization 方法。若 skipsAuthorization 返回 true,客户端将被批准并立即重定向回 redirect_uri,除非消费方应用在重定向授权时显式设置了 prompt 参数:

php
<?php

namespace App\Models\Passport;

use Laravel\Passport\Client as BaseClient;

class Client extends BaseClient
{
    /**
     * Determine if the client should skip the authorization prompt.
     */
    public function skipsAuthorization(): bool
    {
        return $this->firstParty();
    }
}

将授权码转换为访问令牌 ​

若用户批准授权请求,将重定向回消费方应用。消费方应先校验 state 参数与重定向前存储的值是否一致。若一致,应向你的应用发送 POST 请求以获取访问令牌,请求中应包含用户批准授权时你的应用签发的授权码:

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;

Route::get('/callback', function (Request $request) {
    $state = $request->session()->pull('state');

    throw_unless(
        strlen($state) > 0 && $state === $request->state,
        InvalidArgumentException::class,
        'Invalid state value.'
    );

    $response = Http::asForm()->post('http://passport-app.test/oauth/token', [
        'grant_type' => 'authorization_code',
        'client_id' => 'client-id',
        'client_secret' => 'client-secret',
        'redirect_uri' => 'http://third-party-app.com/callback',
        'code' => $request->code,
    ]);

    return $response->json();
});

该 /oauth/token 路由会返回包含 access_token、refresh_token 与 expires_in 的 JSON 响应。expires_in 表示访问令牌过期前的秒数。

INFO

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

JSON API ​

Passport 还提供用于管理已授权访问令牌的 JSON API。你可将其与自己的前端搭配,为用户提供管理访问令牌的控制台。为方便演示,我们使用 Axios 向这些端点发起 HTTP 请求。该 JSON API 受 web 与 auth 中间件保护,因此只能从你自己的应用内调用。

GET /oauth/tokens ​

该路由返回已认证用户创建的所有已授权访问令牌,主要用于列出用户的全部令牌以便撤销:

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

DELETE /oauth/tokens/{token-id} ​

该路由可用于撤销已授权的访问令牌及其相关的 refresh token:

js
axios.delete('/oauth/tokens/' + tokenId);

刷新令牌 ​

若应用签发短期访问令牌,用户需通过签发访问令牌时提供的 refresh token 来刷新访问令牌:

use Illuminate\Support\Facades\Http;

$response = Http::asForm()->post('http://passport-app.test/oauth/token', [
    'grant_type' => 'refresh_token',
    'refresh_token' => 'the-refresh-token',
    'client_id' => 'client-id',
    'client_secret' => 'client-secret',
    'scope' => '',
]);

return $response->json();

该 /oauth/token 路由会返回包含 access_token、refresh_token 与 expires_in 的 JSON 响应。expires_in 表示访问令牌过期前的秒数。

撤销令牌 ​

可在 Laravel\Passport\TokenRepository 上使用 revokeAccessToken 方法撤销令牌;可在 Laravel\Passport\RefreshTokenRepository 上使用 revokeRefreshTokensByAccessTokenId 方法撤销令牌的 refresh token。这些类可通过 Laravel 的服务容器解析:

use Laravel\Passport\TokenRepository;
use Laravel\Passport\RefreshTokenRepository;

$tokenRepository = app(TokenRepository::class);
$refreshTokenRepository = app(RefreshTokenRepository::class);

// Revoke an access token...
$tokenRepository->revokeAccessToken($tokenId);

// Revoke all of the token's refresh tokens...
$refreshTokenRepository->revokeRefreshTokensByAccessTokenId($tokenId);

清理令牌 ​

令牌被撤销或过期后,你可能希望从数据库中清理。Passport 内置的 passport:purge Artisan 命令可完成此操作:

shell
# Purge revoked and expired tokens and auth codes...
php artisan passport:purge

# Only purge tokens expired for more than 6 hours...
php artisan passport:purge --hours=6

# Only purge revoked tokens and auth codes...
php artisan passport:purge --revoked

# Only purge expired tokens and auth codes...
php artisan passport:purge --expired

也可在应用的 routes/console.php 中配置定时任务,按计划自动清理令牌:

use Illuminate\Support\Facades\Schedule;

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

带 PKCE 的授权码授权 ​

带「Proof Key for Code Exchange」(PKCE)的授权码授权,是单页应用或移动应用安全访问 API 的方式。当无法保证 client secret 机密存储,或需降低授权码被攻击者截获的风险时,应使用此授权。用「code verifier」与「code challenge」的组合,在将授权码换取访问令牌时替代 client secret。

创建客户端 ​

应用要通过带 PKCE 的授权码授权签发令牌前,需创建启用 PKCE 的客户端。可使用带 --public 选项的 passport:client Artisan 命令:

shell
php artisan passport:client --public

请求令牌 ​

Code Verifier 与 Code Challenge ​

由于此授权不提供 client secret,开发者需生成 code verifier 与 code challenge 的组合以请求令牌。

code verifier 应为 43 至 128 个字符的随机字符串,包含字母、数字及 "-"、"."、"_"、"~" 字符,详见 RFC 7636 规范。

code challenge 应为使用 URL 与文件名安全字符的 Base64 编码字符串,应移除末尾的 '=' 字符,且不得包含换行、空白或其他额外字符。

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

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

重定向以进行授权 ​

客户端创建后,可使用 client ID 与生成的 code verifier、code challenge 向你的应用请求授权码与访问令牌。首先,消费方应用应向应用的 /oauth/authorize 路由发起重定向请求:

use Illuminate\Http\Request;
use Illuminate\Support\Str;

Route::get('/redirect', function (Request $request) {
    $request->session()->put('state', $state = Str::random(40));

    $request->session()->put(
        'code_verifier', $code_verifier = Str::random(128)
    );

    $codeChallenge = strtr(rtrim(
        base64_encode(hash('sha256', $code_verifier, true))
    , '='), '+/', '-_');

    $query = http_build_query([
        'client_id' => 'client-id',
        'redirect_uri' => 'http://third-party-app.com/callback',
        'response_type' => 'code',
        'scope' => '',
        'state' => $state,
        'code_challenge' => $codeChallenge,
        'code_challenge_method' => 'S256',
        // 'prompt' => '', // "none", "consent", or "login"
    ]);

    return redirect('http://passport-app.test/oauth/authorize?'.$query);
});

将授权码转换为访问令牌 ​

若用户批准授权请求,将重定向回消费方应用。消费方应校验 state 参数与重定向前存储的值,与标准授权码授权相同。

若 state 参数一致,消费方应向你的应用发送 POST 请求以获取访问令牌,请求中应包含用户批准授权时签发的授权码,以及最初生成的 code verifier:

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;

Route::get('/callback', function (Request $request) {
    $state = $request->session()->pull('state');

    $codeVerifier = $request->session()->pull('code_verifier');

    throw_unless(
        strlen($state) > 0 && $state === $request->state,
        InvalidArgumentException::class
    );

    $response = Http::asForm()->post('http://passport-app.test/oauth/token', [
        'grant_type' => 'authorization_code',
        'client_id' => 'client-id',
        'redirect_uri' => 'http://third-party-app.com/callback',
        'code_verifier' => $codeVerifier,
        'code' => $request->code,
    ]);

    return $response->json();
});

密码授权令牌 ​

WARNING

我们不再推荐使用密码授权令牌。应选择 OAuth2 Server 当前推荐的授权类型。

OAuth2 密码授权允许你的其他第一方客户端(如移动应用)使用邮箱/用户名与密码获取访问令牌,从而无需用户走完 OAuth2 授权码重定向流程即可安全地向第一方客户端签发访问令牌。

要启用密码授权,在 App\Providers\AppServiceProvider 的 boot 方法中调用 enablePasswordGrant:

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

创建密码授权客户端 ​

应用要通过密码授权签发令牌前,需先创建密码授权客户端。可使用带 --password 选项的 passport:client Artisan 命令完成。若已运行过 passport:install 命令,则无需再运行此命令:

shell
php artisan passport:client --password

请求令牌 ​

启用授权并创建密码授权客户端后,可向 /oauth/token 发送 POST 请求(携带用户邮箱与密码)以获取访问令牌。该路由已由 Passport 注册,无需手动定义。请求成功时,服务器 JSON 响应将包含 access_token 与 refresh_token:

use Illuminate\Support\Facades\Http;

$response = Http::asForm()->post('http://passport-app.test/oauth/token', [
    'grant_type' => 'password',
    'client_id' => 'client-id',
    'client_secret' => 'client-secret',
    'username' => 'taylor@laravel.com',
    'password' => 'my-password',
    'scope' => '',
]);

return $response->json();

INFO

请记住,访问令牌默认长期有效。如有需要,可配置最大访问令牌生命周期。

请求所有作用域 ​

使用密码授权或客户端凭证授权时,你可能希望令牌拥有应用支持的全部作用域。可请求 * 作用域;若请求 *,令牌实例的 can 方法将始终返回 true。该作用域仅可分配给通过 password 或 client_credentials 授权签发的令牌:

use Illuminate\Support\Facades\Http;

$response = Http::asForm()->post('http://passport-app.test/oauth/token', [
    'grant_type' => 'password',
    'client_id' => 'client-id',
    'client_secret' => 'client-secret',
    'username' => 'taylor@laravel.com',
    'password' => 'my-password',
    'scope' => '*',
]);

自定义用户提供者 ​

若应用使用多个认证用户提供者,可在通过 artisan passport:client --password 创建客户端时提供 --provider 选项,指定密码授权客户端使用的用户提供者。名称须与 config/auth.php 中定义的提供者一致。随后可用中间件保护路由,确保仅该守卫指定提供者的用户被授权。

自定义用户名字段 ​

使用密码授权认证时,Passport 将可认证模型的 email 属性作为「username」。可在模型上定义 findForPassport 方法自定义此行为:

php
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Passport\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, Notifiable;

    /**
     * Find the user instance for the given username.
     */
    public function findForPassport(string $username): User
    {
        return $this->where('username', $username)->first();
    }
}

自定义密码验证 ​

使用密码授权认证时,Passport 使用模型的 password 属性验证密码。若模型没有 password 属性或需自定义验证逻辑,可在模型上定义 validateForPassportPasswordGrant 方法:

php
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Hash;
use Laravel\Passport\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, Notifiable;

    /**
     * Validate the password of the user for the Passport password grant.
     */
    public function validateForPassportPasswordGrant(string $password): bool
    {
        return Hash::check($password, $this->password);
    }
}

隐式授权令牌 ​

WARNING

我们不再推荐使用隐式授权令牌。应选择 OAuth2 Server 当前推荐的授权类型。

隐式授权与授权码授权类似,但令牌直接返回客户端而无需交换授权码。常用于无法安全存储客户端凭证的 JavaScript 或移动应用。要启用,在 App\Providers\AppServiceProvider 的 boot 方法中调用 enableImplicitGrant:

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

启用授权并创建隐式客户端后,开发者可使用 client ID 向你的应用请求访问令牌。消费方应用应向应用的 /oauth/authorize 路由发起重定向请求,例如:

use Illuminate\Http\Request;

Route::get('/redirect', function (Request $request) {
    $request->session()->put('state', $state = Str::random(40));

    $query = http_build_query([
        'client_id' => 'client-id',
        'redirect_uri' => 'http://third-party-app.com/callback',
        'response_type' => 'token',
        'scope' => '',
        'state' => $state,
        // 'prompt' => '', // "none", "consent", or "login"
    ]);

    return redirect('http://passport-app.test/oauth/authorize?'.$query);
});

INFO

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

客户端凭证授权令牌 ​

客户端凭证授权适用于机器对机器认证,例如在通过 API 执行维护任务的定时作业中使用。

应用要通过客户端凭证授权签发令牌前,需创建客户端凭证授权客户端。可使用 passport:client Artisan 命令的 --client 选项:

shell
php artisan passport:client --client

接下来,要使用此授权类型,请为 CheckClientCredentials 中间件注册别名。可在应用的 bootstrap/app.php 中定义中间件别名:

use Laravel\Passport\Http\Middleware\CheckClientCredentials;

->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'client' => CheckClientCredentials::class
    ]);
})

然后将中间件附加到路由:

Route::get('/orders', function (Request $request) {
    ...
})->middleware('client');

要将路由访问限制为特定作用域,可在将 client 中间件附加到路由时提供以逗号分隔的所需作用域列表:

Route::get('/orders', function (Request $request) {
    ...
})->middleware('client:check-status,your-scope');

获取令牌 ​

使用此授权类型获取令牌时,向 oauth/token 端点发起请求:

use Illuminate\Support\Facades\Http;

$response = Http::asForm()->post('http://passport-app.test/oauth/token', [
    'grant_type' => 'client_credentials',
    'client_id' => 'client-id',
    'client_secret' => 'client-secret',
    'scope' => 'your-scope',
]);

return $response->json()['access_token'];

个人访问令牌 ​

有时用户希望不经典型授权码重定向流程而自行签发访问令牌。允许用户通过应用 UI 自行签发令牌,便于试用 API,或作为更简单的签发方式。

INFO

若应用主要用 Passport 签发个人访问令牌,可考虑使用 Laravel Sanctum——Laravel 轻量级第一方 API 访问令牌库。

创建个人访问客户端 ​

应用要签发个人访问令牌前,需创建个人访问客户端。可执行带 --personal 选项的 passport:client Artisan 命令。若已运行 passport:install,则无需再运行此命令:

shell
php artisan passport:client --personal

创建个人访问客户端后,将客户端的 ID 与明文密钥写入应用的 .env 文件:

ini
PASSPORT_PERSONAL_ACCESS_CLIENT_ID="client-id-value"
PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET="unhashed-client-secret-value"

管理个人访问令牌 ​

创建个人访问客户端后,可在 App\Models\User 模型实例上使用 createToken 为指定用户签发令牌。createToken 第一个参数为令牌名称,第二个可选参数为作用域数组:

use App\Models\User;

$user = User::find(1);

// Creating a token without scopes...
$token = $user->createToken('Token Name')->accessToken;

// Creating a token with scopes...
$token = $user->createToken('My Token', ['place-orders'])->accessToken;

JSON API ​

Passport 还提供用于管理个人访问令牌的 JSON API。你可将其与自己的前端搭配,为用户提供管理个人访问令牌的控制台。下面我们将回顾所有用于管理个人访问令牌的 API 端点。为方便演示,我们使用 Axios 向这些端点发起 HTTP 请求。

该 JSON API 受 web 与 auth 中间件保护,因此只能从你自己的应用内调用,无法从外部来源调用。

GET /oauth/scopes ​

该路由返回为应用定义的所有作用域。可用此路由列出用户可为个人访问令牌分配的作用域:

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

GET /oauth/personal-access-tokens ​

该路由返回已认证用户创建的所有个人访问令牌,主要用于列出用户的全部令牌以便编辑或撤销:

js
axios.get('/oauth/personal-access-tokens')
    .then(response => {
        console.log(response.data);
    });

POST /oauth/personal-access-tokens ​

该路由用于创建新的个人访问令牌,需要两段数据:令牌的 name 以及应分配给令牌的 scopes:

js
const data = {
    name: 'Token Name',
    scopes: []
};

axios.post('/oauth/personal-access-tokens', data)
    .then(response => {
        console.log(response.data.accessToken);
    })
    .catch (response => {
        // List errors on response...
    });

DELETE /oauth/personal-access-tokens/{token-id} ​

该路由可用于撤销个人访问令牌:

js
axios.delete('/oauth/personal-access-tokens/' + tokenId);

保护路由 ​

通过中间件 ​

Passport 包含用于验证传入请求访问令牌的认证守卫。将 api 守卫配置为使用 passport 驱动后,只需在需要有效访问令牌的路由上指定 auth:api 中间件:

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

WARNING

若使用客户端凭证授权,应使用client 中间件保护路由,而不是 auth:api 中间件。

多个认证守卫 ​

若应用认证多种可能使用完全不同 Eloquent 模型的用户,通常需为每种用户提供者类型定义守卫配置,以保护面向特定提供者的请求。例如,config/auth.php 中可有如下守卫配置:

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

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

以下路由将使用 api-customers 守卫(使用 customers 用户提供者)认证传入请求:

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

INFO

关于在 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 接受作用域名称与描述数组;描述将显示在授权批准界面上:

php
/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Passport::tokensCan([
        'place-orders' => 'Place orders',
        'check-status' => 'Check order status',
    ]);
}

默认作用域 ​

若客户端未请求特定作用域,可使用 defaultScopes 方法配置 Passport 服务器为令牌附加默认作用域,通常应在 App\Providers\AppServiceProvider 的 boot 方法中调用:

use Laravel\Passport\Passport;

Passport::tokensCan([
    'place-orders' => 'Place orders',
    'check-status' => 'Check order status',
]);

Passport::setDefaultScope([
    'check-status',
    'place-orders',
]);

INFO

Passport 的默认作用域不适用于由用户生成的个人访问令牌。

为令牌分配作用域 ​

请求授权码时 ​

使用授权码授权请求访问令牌时,消费方应将所需作用域作为 scope 查询参数,以空格分隔:

Route::get('/redirect', function () {
    $query = http_build_query([
        'client_id' => 'client-id',
        'redirect_uri' => 'http://example.com/callback',
        'response_type' => 'code',
        'scope' => 'place-orders check-status',
    ]);

    return redirect('http://passport-app.test/oauth/authorize?'.$query);
});

签发个人访问令牌时 ​

若使用 App\Models\User 模型的 createToken 方法签发个人访问令牌,可将所需作用域数组作为第二个参数传入:

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

检查作用域 ​

Passport 提供两个中间件,用于验证传入请求是否由已授予指定作用域的令牌认证。开始时,请在应用的 bootstrap/app.php 中定义以下中间件别名:

use Laravel\Passport\Http\Middleware\CheckForAnyScope;
use Laravel\Passport\Http\Middleware\CheckScopes;

->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'scopes' => CheckScopes::class,
        'scope' => CheckForAnyScope::class,
    ]);
})

检查是否拥有全部作用域 ​

可将 scopes 中间件分配给路由,验证传入请求的访问令牌是否拥有全部所列作用域:

Route::get('/orders', function () {
    // Access token has both "check-status" and "place-orders" scopes...
})->middleware(['auth:api', 'scopes:check-status,place-orders']);

检查是否拥有任一作用域 ​

可将 scope 中间件分配给路由,验证传入请求的访问令牌是否拥有所列作用域中的至少一个:

Route::get('/orders', function () {
    // Access token has either "check-status" or "place-orders" scope...
})->middleware(['auth:api', 'scope:check-status,place-orders']);

在令牌实例上检查作用域 ​

访问令牌认证的请求进入应用后,仍可在已认证的 App\Models\User 实例上使用 tokenCan 检查令牌是否拥有指定作用域:

use Illuminate\Http\Request;

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

其他作用域方法 ​

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

use Laravel\Passport\Passport;

Passport::scopeIds();

scopes 方法返回所有已定义作用域的 Laravel\Passport\Scope 实例数组:

Passport::scopes();

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

Passport::scopesFor(['place-orders', 'check-status']);

可使用 hasScope 方法判断指定作用域是否已定义:

Passport::hasScope('place-orders');

使用 JavaScript 消费你的 API ​

构建 API 时,能从 JavaScript 应用消费自己的 API 非常有用。这使应用与对外共享的 API 一致,同一 API 可被 Web 应用、移动应用、第三方应用及你发布到各包管理器的 SDK 使用。

通常,若要从 JavaScript 应用消费 API,需手动将访问令牌传给应用并在每次请求中携带。Passport 提供中间件可自动处理:在 bootstrap/app.php 中将 CreateFreshApiToken 中间件追加到 web 中间件组即可:

use Laravel\Passport\Http\Middleware\CreateFreshApiToken;

->withMiddleware(function (Middleware $middleware) {
    $middleware->web(append: [
        CreateFreshApiToken::class,
    ]);
})

WARNING

请确保 CreateFreshApiToken 中间件位于中间件栈的最后。

该中间件会在出站响应中附加 laravel_token Cookie,其中包含 Passport 用于认证 JavaScript 应用 API 请求的加密 JWT。JWT 生命周期等于 session.lifetime 配置值。浏览器会在后续请求中自动发送该 Cookie,因此无需显式传递访问令牌即可请求应用 API:

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

如有需要,可使用 Passport::cookie 方法自定义 laravel_token Cookie 名称,通常应在 App\Providers\AppServiceProvider 的 boot 方法中调用:

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

CSRF 保护 ​

使用此认证方式时,请求须包含有效的 CSRF 令牌头。Laravel 默认的 JavaScript 脚手架包含 Axios 实例,会在同源请求中自动使用加密的 XSRF-TOKEN cookie 值发送 X-XSRF-TOKEN 头。

INFO

若选择发送 X-CSRF-TOKEN 而非 X-XSRF-TOKEN,需使用 csrf_token() 提供的未加密令牌。

事件 ​

Passport 在签发访问令牌与 refresh token 时会触发事件。你可监听这些事件以在数据库中清理或撤销其他访问令牌:

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

测试 ​

Passport 的 actingAs 方法可指定当前认证用户及其作用域。第一个参数为用户实例,第二个为应授予用户令牌的作用域数组:

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

test('servers can be created', function () {
    Passport::actingAs(
        User::factory()->create(),
        ['create-servers']
    );

    $response = $this->post('/api/create-server');

    $response->assertStatus(201);
});
php
use App\Models\User;
use Laravel\Passport\Passport;

public function test_servers_can_be_created(): void
{
    Passport::actingAs(
        User::factory()->create(),
        ['create-servers']
    );

    $response = $this->post('/api/create-server');

    $response->assertStatus(201);
}

Passport 的 actingAsClient 方法可指定当前认证客户端及其作用域。第一个参数为客户端实例,第二个为应授予客户端令牌的作用域数组:

php
use Laravel\Passport\Client;
use Laravel\Passport\Passport;

test('orders can be retrieved', function () {
    Passport::actingAsClient(
        Client::factory()->create(),
        ['check-status']
    );

    $response = $this->get('/api/orders');

    $response->assertStatus(200);
});
php
use Laravel\Passport\Client;
use Laravel\Passport\Passport;

public function test_orders_can_be_retrieved(): void
{
    Passport::actingAsClient(
        Client::factory()->create(),
        ['check-status']
    );

    $response = $this->get('/api/orders');

    $response->assertStatus(200);
}