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\AppServiceProviderboot 方法中调用:

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\AppServiceProviderboot 方法中调用 Passport::hashClientSecrets

use Laravel\Passport\Passport;

Passport::hashClientSecrets();

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

令牌生命周期

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

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\AppServiceProviderboot 方法中配置:

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 定义的路由。为此,先在 AppServiceProviderregister 方法中添加 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 受 webauth 中间件保护,因此只能从你自己的应用内调用,无法从外部来源调用。

GET /oauth/clients

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

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

POST /oauth/clients

该路由用于创建新客户端,需要两段数据:客户端的 nameredirect 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}

该路由用于更新客户端,需要两段数据:客户端的 nameredirect 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 应用的认证行为。

promptnone,当用户尚未在 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_tokenrefresh_tokenexpires_in 的 JSON 响应。expires_in 表示访问令牌过期前的秒数。

INFO

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

JSON API

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

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_tokenrefresh_tokenexpires_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\AppServiceProviderboot 方法中调用 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_tokenrefresh_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。该作用域仅可分配给通过 passwordclient_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\AppServiceProviderboot 方法中调用 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 受 webauth 中间件保护,因此只能从你自己的应用内调用,无法从外部来源调用。

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\AppServiceProviderboot 方法中使用 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\AppServiceProviderboot 方法中调用:

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\AppServiceProviderboot 方法中调用:

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