Skip to content
赞助商
虚位以待
赞助商
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待

Laravel Passport

介绍

Laravel 已经使通过传统登录表单进行身份验证变得简单,但 API 呢?API 通常使用令牌来验证用户,并且在请求之间不维护会话状态。Laravel 使用 Laravel Passport 使 API 身份验证变得轻而易举,Passport 为您的 Laravel 应用程序提供了完整的 OAuth2 服务器实现,只需几分钟即可完成。Passport 构建在由 Andy Millington 和 Simon Hamp 维护的 League OAuth2 服务器 之上。

NOTE

本文档假设您已经熟悉 OAuth2。如果您对 OAuth2 一无所知,请考虑在继续之前熟悉 OAuth2 的一般术语和功能。

升级 Passport

在升级到 Passport 的新主要版本时,您需要仔细查看升级指南

安装

首先,通过 Composer 包管理器安装 Passport:

php
composer require laravel/passport "~9.0"

Passport 服务提供者会将其自己的数据库迁移目录注册到框架中,因此在安装包后,您应该迁移数据库。Passport 迁移将创建您的应用程序需要的表来存储客户端和访问令牌:

php
php artisan migrate

接下来,您应该运行 passport:install 命令。此命令将创建生成安全访问令牌所需的加密密钥。此外,该命令将创建“个人访问”和“密码授权”客户端,这些客户端将用于生成访问令牌:

php
php artisan passport:install

NOTE

如果您希望使用 UUID 作为 Passport Client 模型的主键值而不是自动递增整数,请使用 the uuids option 安装 Passport。

运行 passport:install 命令后,将 Laravel\Passport\HasApiTokens trait 添加到您的 App\User 模型中。此 trait 将为您的模型提供一些辅助方法,允许您检查经过身份验证的用户的令牌和范围:

php
<?php

namespace App;

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

class User extends Authenticatable
{
    use HasApiTokens, Notifiable;
}

接下来,您应该在 AuthServiceProviderboot 方法中调用 Passport::routes 方法。此方法将注册颁发访问令牌和撤销访问令牌、客户端和个人访问令牌所需的路由:

php
<?php

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
use Laravel\Passport\Passport;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * 应用程序的策略映射。
     *
     * @var array
     */
    protected $policies = [
        'App\Model' => 'App\Policies\ModelPolicy',
    ];

    /**
     * 注册任何身份验证/授权服务。
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        Passport::routes();
    }
}

最后,在您的 config/auth.php 配置文件中,您应该将 api 身份验证守卫的 driver 选项设置为 passport。这将指示您的应用程序在验证传入的 API 请求时使用 Passport 的 TokenGuard

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

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

客户端 UUID

您可以在运行 passport:install 命令时使用 --uuids 选项。此标志将指示 Passport 您希望使用 UUID 而不是自动递增整数作为 Passport Client 模型的主键值。在使用 --uuids 选项运行 passport:install 命令后,您将获得有关禁用 Passport 默认迁移的其他说明:

php
php artisan passport:install --uuids

前端快速入门

NOTE

为了使用 Passport Vue 组件,您必须使用 Vue JavaScript 框架。这些组件还使用 Bootstrap CSS 框架。但是,即使您不使用这些工具,这些组件也可以作为您自己前端实现的有价值的参考。

Passport 附带一个 JSON API,您可以使用它来允许用户创建客户端和个人访问令牌。然而,编写一个与这些 API 交互的前端可能会耗费时间。因此,Passport 还包括预构建的 Vue 组件,您可以将其用作示例实现或您自己实现的起点。

要发布 Passport Vue 组件,请使用 vendor:publish Artisan 命令:

php
php artisan vendor:publish --tag=passport-components

发布的组件将放置在您的 resources/js/components 目录中。发布组件后,您应该在 resources/js/app.js 文件中注册它们:

php
Vue.component(
    'passport-clients',
    require('./components/passport/Clients.vue').default
);

Vue.component(
    'passport-authorized-clients',
    require('./components/passport/AuthorizedClients.vue').default
);

Vue.component(
    'passport-personal-access-tokens',
    require('./components/passport/PersonalAccessTokens.vue').default
);

NOTE

在 Laravel v5.7.19 之前,注册组件时附加 .default 会导致控制台错误。对此更改的解释可以在 Laravel Mix v4.0.0 发布说明 中找到。

注册组件后,请确保运行 npm run dev 以重新编译您的资产。重新编译资产后,您可以将组件放入应用程序的模板之一,以开始创建客户端和个人访问令牌:

php
<passport-clients></passport-clients>
<passport-authorized-clients></passport-authorized-clients>
<passport-personal-access-tokens></passport-personal-access-tokens>

部署 Passport

首次将 Passport 部署到生产服务器时,您可能需要运行 passport:keys 命令。此命令生成 Passport 生成访问令牌所需的加密密钥。生成的密钥通常不保存在源代码控制中:

php
php artisan passport:keys

如果需要,您可以定义 Passport 密钥应从何处加载的路径。您可以使用 Passport::loadKeysFrom 方法来实现这一点:

php
/**
 * 注册任何身份验证/授权服务。
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();

    Passport::routes();

    Passport::loadKeysFrom('/secret-keys/oauth');
}

此外,您可以使用 php artisan vendor:publish --tag=passport-config 发布 Passport 的配置文件,这将提供从环境变量加载加密密钥的选项:

php
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 的默认迁移,您应该在 AppServiceProviderregister 方法中调用 Passport::ignoreMigrations 方法。您可以使用 php artisan vendor:publish --tag=passport-migrations 导出默认迁移。

配置

客户端密钥哈希

如果您希望在将客户端的密钥存储在数据库中时对其进行哈希处理,您应该在 AppServiceProviderboot 方法中调用 Passport::hashClientSecrets 方法:

php
Passport::hashClientSecrets();

启用后,所有客户端密钥将在创建客户端时仅显示一次。由于明文客户端密钥值从未存储在数据库中,因此无法在丢失时恢复。

令牌有效期

默认情况下,Passport 会颁发有效期为一年的长期访问令牌。如果您希望配置更长/更短的令牌有效期,可以使用 tokensExpireInrefreshTokensExpireInpersonalAccessTokensExpireIn 方法。这些方法应在 AuthServiceProviderboot 方法中调用:

php
/**
 * 注册任何身份验证/授权服务。
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();

    Passport::routes();

    Passport::tokensExpireIn(now()->addDays(15));

    Passport::refreshTokensExpireIn(now()->addDays(30));

    Passport::personalAccessTokensExpireIn(now()->addMonths(6));
}

NOTE

Passport 数据库表上的 expires_at 列是只读的,仅用于显示目的。在颁发令牌时,Passport 会将过期信息存储在签名和加密的令牌中。如果您需要使令牌无效,您应该撤销它。

覆盖默认模型

您可以自由扩展 Passport 内部使用的模型:

php
use Laravel\Passport\Client as PassportClient;

class Client extends PassportClient
{
    // ...
}

然后,您可以通过 Passport 类指示 Passport 使用您的自定义模型:

php
use App\Models\Passport\AuthCode;
use App\Models\Passport\Client;
use App\Models\Passport\PersonalAccessClient;
use App\Models\Passport\Token;

/**
 * 注册任何身份验证/授权服务。
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();

    Passport::routes();

    Passport::useTokenModel(Token::class);
    Passport::useClientModel(Client::class);
    Passport::useAuthCodeModel(AuthCode::class);
    Passport::usePersonalAccessClientModel(PersonalAccessClient::class);
}

颁发访问令牌

使用带有授权码的 OAuth2 是大多数开发人员熟悉 OAuth2 的方式。使用授权码时,客户端应用程序将用户重定向到您的服务器,在那里他们将批准或拒绝向客户端颁发访问令牌的请求。

管理客户端

首先,构建需要与您的应用程序的 API 交互的应用程序的开发人员需要通过创建“客户端”来注册他们的应用程序。通常,这包括提供他们应用程序的名称和一个 URL,您的应用程序可以在用户批准他们的授权请求后重定向到该 URL。

passport:client 命令

创建客户端的最简单方法是使用 passport:client Artisan 命令。此命令可用于为您的 OAuth2 功能创建自己的客户端进行测试。当您运行 client 命令时,Passport 将提示您提供有关客户端的更多信息,并为您提供客户端 ID 和密钥:

php
php artisan passport:client

重定向 URL

如果您希望为客户端允许多个重定向 URL,可以在 passport:client 命令提示您输入 URL 时使用逗号分隔的列表指定它们:

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

NOTE

任何包含逗号的 URL 必须进行编码。

JSON API

由于您的用户将无法使用 client 命令,Passport 提供了一个 JSON API,您可以使用它来创建客户端。这为您节省了手动编写控制器以创建、更新和删除客户端的麻烦。

但是,您需要将 Passport 的 JSON API 与您自己的前端配对,以为用户提供一个仪表板来管理他们的客户端。下面,我们将回顾管理客户端的所有 API 端点。为了方便起见,我们将使用 Axios 来演示向端点发出 HTTP 请求。

JSON API 由 webauth 中间件保护;因此,它只能从您自己的应用程序调用。无法从外部来源调用。

NOTE

如果您不想自己实现整个客户端管理前端,可以使用 frontend quickstart 在几分钟内拥有一个功能齐全的前端。

GET /oauth/clients

此路由返回经过身份验证的用户的所有客户端。这主要用于列出用户的所有客户端,以便他们可以编辑或删除它们:

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

POST /oauth/clients

此路由用于创建新客户端。它需要两条数据:客户端的 name 和一个 redirect URL。redirect URL 是用户在批准或拒绝授权请求后将被重定向到的地方。

创建客户端时,将为其分配一个客户端 ID 和客户端密钥。这些值将在请求访问令牌时使用。客户端创建路由将返回新的客户端实例:

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

axios.post('/oauth/clients', data)
    .then(response => {
        console.log(response.data);
    })
    .catch (response => {
        // 列出响应中的错误...
    });

PUT /oauth/clients/{client-id}

此路由用于更新客户端。它需要两条数据:客户端的 name 和一个 redirect URL。redirect URL 是用户在批准或拒绝授权请求后将被重定向到的地方。该路由将返回更新后的客户端实例:

php
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 => {
        // 列出响应中的错误...
    });

DELETE /oauth/clients/{client-id}

此路由用于删除客户端:

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

请求令牌

重定向以进行授权

创建客户端后,开发人员可以使用他们的客户端 ID 和密钥从您的应用程序请求授权码和访问令牌。首先,消费应用程序应向您的应用程序的 /oauth/authorize 路由发出重定向请求,如下所示:

php
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://example.com/callback',
        'response_type' => 'code',
        'scope' => '',
        'state' => $state,
    ]);

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

NOTE

请记住,/oauth/authorize 路由已由 Passport::routes 方法定义。您无需手动定义此路由。

批准请求

在接收授权请求时,Passport 将自动向用户显示一个模板,允许他们批准或拒绝授权请求。如果他们批准请求,他们将被重定向回消费应用程序指定的 redirect_uriredirect_uri 必须与创建客户端时指定的 redirect URL 匹配。

如果您想自定义授权批准屏幕,可以使用 vendor:publish Artisan 命令发布 Passport 的视图。发布的视图将放置在 resources/views/vendor/passport 中:

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

有时您可能希望跳过授权提示,例如在授权第一方客户端时。您可以通过扩展 Client 模型并定义一个 skipsAuthorization 方法来实现这一点。如果 skipsAuthorization 返回 true,则客户端将被批准,用户将立即被重定向回 redirect_uri

php
<?php

namespace App\Models\Passport;

use Laravel\Passport\Client as BaseClient;

class Client extends BaseClient
{
    /**
     * 确定客户端是否应跳过授权提示。
     *
     * @return bool
     */
    public function skipsAuthorization()
    {
        return $this->firstParty();
    }
}

将授权码转换为访问令牌

如果用户批准授权请求,他们将被重定向回消费应用程序。消费者应首先验证 state 参数与重定向前存储的值是否匹配。如果 state 参数匹配,消费者应向您的应用程序发出 POST 请求以请求访问令牌。请求应包括用户批准授权请求时由您的应用程序颁发的授权码。在此示例中,我们将使用 Guzzle HTTP 库发出 POST 请求:

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

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

    $http = new GuzzleHttp\Client;

    $response = $http->post('http://your-app.com/oauth/token', [
        'form_params' => [
            'grant_type' => 'authorization_code',
            'client_id' => 'client-id',
            'client_secret' => 'client-secret',
            'redirect_uri' => 'http://example.com/callback',
            'code' => $request->code,
        ],
    ]);

    return json_decode((string) $response->getBody(), true);
});

/oauth/token 路由将返回一个包含 access_tokenrefresh_tokenexpires_in 属性的 JSON 响应。expires_in 属性包含访问令牌过期的秒数。

NOTE

/oauth/authorize 路由一样,/oauth/token 路由由 Passport::routes 方法为您定义。无需手动定义此路由。默认情况下,此路由使用 ThrottleRequests 中间件的设置进行限流。

JSON API

Passport 还包括一个用于管理授权访问令牌的 JSON API。您可以将其与您自己的前端配对,为用户提供一个仪表板来管理访问令牌。为了方便起见,我们将使用 Axios 来演示向端点发出 HTTP 请求。JSON API 由 webauth 中间件保护;因此,它只能从您自己的应用程序调用。

GET /oauth/tokens

此路由返回经过身份验证的用户创建的所有授权访问令牌。这主要用于列出用户的所有令牌,以便他们可以撤销它们:

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

DELETE /oauth/tokens/{token-id}

此路由可用于撤销授权访问令牌及其相关的刷新令牌:

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

刷新令牌

如果您的应用程序颁发短期访问令牌,用户将需要通过颁发访问令牌时提供给他们的刷新令牌来刷新他们的访问令牌。在此示例中,我们将使用 Guzzle HTTP 库刷新令牌:

php
$http = new GuzzleHttp\Client;

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

return json_decode((string) $response->getBody(), true);

/oauth/token 路由将返回一个包含 access_tokenrefresh_tokenexpires_in 属性的 JSON 响应。expires_in 属性包含访问令牌过期的秒数。

撤销令牌

您可以使用 TokenRepository 上的 revokeAccessToken 方法撤销令牌。您可以使用 RefreshTokenRepository 上的 revokeRefreshTokensByAccessTokenId 方法撤销令牌的刷新令牌:

php
$tokenRepository = app('Laravel\Passport\TokenRepository');
$refreshTokenRepository = app('Laravel\Passport\RefreshTokenRepository');

// 撤销访问令牌...
$tokenRepository->revokeAccessToken($tokenId);

// 撤销所有令牌的刷新令牌...
$refreshTokenRepository->revokeRefreshTokensByAccessTokenId($tokenId);

清除令牌

当令牌被撤销或过期时,您可能希望从数据库中清除它们。Passport 附带一个可以为您执行此操作的命令:

php
# 清除已撤销和过期的令牌和授权码...
php artisan passport:purge

# 仅清除已撤销的令牌和授权码...
php artisan passport:purge --revoked

# 仅清除过期的令牌和授权码...
php artisan passport:purge --expired

您还可以在控制台 Kernel 类中配置一个计划任务以自动按计划修剪您的令牌:

php
/**
 * 定义应用程序的命令计划。
 *
 * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
 * @return void
 */
protected function schedule(Schedule $schedule)
{
    $schedule->command('passport:purge')->hourly();
}

带 PKCE 的授权码授权

带有“代码交换证明密钥”(PKCE)的授权码授权是一种安全的方式来验证单页应用程序或本机应用程序以访问您的 API。当您无法保证客户端密钥将被机密存储时,或者为了减轻授权码被攻击者拦截的威胁时,应使用此授权。代码验证器和代码挑战的组合在将授权码交换为访问令牌时替代客户端密钥。

创建客户端

在您的应用程序可以通过带有 PKCE 的授权码授权颁发令牌之前,您需要创建一个启用 PKCE 的客户端。您可以使用 passport:client 命令和 --public 选项来执行此操作:

php
php artisan passport:client --public

请求令牌

代码验证器和代码挑战

由于此授权不提供客户端密钥,开发人员需要生成代码验证器和代码挑战的组合以请求令牌。

代码验证器应为 43 到 128 个字符之间的随机字符串,包含字母、数字和 "-"".""_""~",如 RFC 7636 规范 中所定义。

代码挑战应为一个使用 URL 和文件名安全字符的 Base64 编码字符串。应删除尾随的 '=' 字符,并且不应存在换行符、空格或其他附加字符。

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

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

重定向以进行授权

创建客户端后,您可以使用客户端 ID 和生成的代码验证器和代码挑战从您的应用程序请求授权码和访问令牌。首先,消费应用程序应向您的应用程序的 /oauth/authorize 路由发出重定向请求:

php
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://example.com/callback',
        'response_type' => 'code',
        'scope' => '',
        'state' => $state,
        'code_challenge' => $codeChallenge,
        'code_challenge_method' => 'S256',
    ]);

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

将授权码转换为访问令牌

如果用户批准授权请求,他们将被重定向回消费应用程序。消费者应验证 state 参数与重定向前存储的值是否匹配,如标准授权码授权中所述。

如果 state 参数匹配,消费者应向您的应用程序发出 POST 请求以请求访问令牌。请求应包括用户批准授权请求时由您的应用程序颁发的授权码以及最初生成的代码验证器:

php
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 = (new GuzzleHttp\Client)->post('http://your-app.com/oauth/token', [
        'form_params' => [
            'grant_type' => 'authorization_code',
            'client_id' => 'client-id',
            'redirect_uri' => 'http://example.com/callback',
            'code_verifier' => $codeVerifier,
            'code' => $request->code,
        ],
    ]);

    return json_decode((string) $response->getBody(), true);
});

密码授权令牌

OAuth2 密码授权允许您的其他第一方客户端(例如移动应用程序)使用电子邮件地址/用户名和密码获取访问令牌。这允许您安全地向第一方客户端颁发访问令牌,而无需用户通过整个 OAuth2 授权码重定向流程。

创建密码授权客户端

在您的应用程序可以通过密码授权颁发令牌之前,您需要创建一个密码授权客户端。您可以使用 passport:client 命令和 --password 选项来执行此操作。如果您已经运行了 passport:install 命令,则无需运行此命令:

php
php artisan passport:client --password

请求令牌

创建密码授权客户端后,您可以通过向 /oauth/token 路由发出 POST 请求并提供用户的电子邮件地址和密码来请求访问令牌。请记住,此路由已由 Passport::routes 方法注册,因此无需手动定义。如果请求成功,您将收到服务器返回的 JSON 响应中的 access_tokenrefresh_token

php
$http = new GuzzleHttp\Client;

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

return json_decode((string) $response->getBody(), true);

NOTE

请记住,访问令牌默认是长期有效的。但是,如果需要,您可以自由配置最大访问令牌有效期

请求所有范围

使用密码授权或客户端凭证授权时,您可能希望为应用程序支持的所有范围授权令牌。您可以通过请求 * 范围来实现这一点。如果您请求 * 范围,令牌实例上的 can 方法将始终返回 true。此范围只能分配给使用 passwordclient_credentials 授权颁发的令牌:

php
$response = $http->post('http://your-app.com/oauth/token', [
    'form_params' => [
        '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 属性作为“用户名”。但是,您可以通过在模型上定义 findForPassport 方法来自定义此行为:

php
<?php

namespace App;

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

class User extends Authenticatable
{
    use HasApiTokens, Notifiable;

    /**
     * 查找给定用户名的用户实例。
     *
     * @param  string  $username
     * @return \App\User
     */
    public function findForPassport($username)
    {
        return $this->where('username', $username)->first();
    }
}

自定义密码验证

使用密码授权进行身份验证时,Passport 将使用模型的 password 属性来验证给定的密码。如果您的模型没有 password 属性,或者您希望自定义密码验证逻辑,可以在模型上定义 validateForPassportPasswordGrant 方法:

php
<?php

namespace App;

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;

    /**
     * 验证用户的密码以进行 Passport 密码授权。
     *
     * @param  string  $password
     * @return bool
     */
    public function validateForPassportPasswordGrant($password)
    {
        return Hash::check($password, $this->password);
    }
}

隐式授权令牌

隐式授权类似于授权码授权;然而,令牌在不交换授权码的情况下返回给客户端。此授权最常用于 JavaScript 或移动应用程序,其中客户端凭证无法安全存储。要启用授权,请在 AuthServiceProvider 中调用 enableImplicitGrant 方法:

php
/**
 * 注册任何身份验证/授权服务。
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();

    Passport::routes();

    Passport::enableImplicitGrant();
}

启用授权后,开发人员可以使用他们的客户端 ID 从您的应用程序请求访问令牌。消费应用程序应向您的应用程序的 /oauth/authorize 路由发出重定向请求,如下所示:

php
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://example.com/callback',
        'response_type' => 'token',
        'scope' => '',
        'state' => $state,
    ]);

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

NOTE

请记住,/oauth/authorize 路由已由 Passport::routes 方法定义。您无需手动定义此路由。

客户端凭证授权令牌

客户端凭证授权适用于机器对机器的身份验证。例如,您可以在执行 API 维护任务的计划任务中使用此授权。

在您的应用程序可以通过客户端凭证授权颁发令牌之前,您需要创建一个客户端凭证授权客户端。您可以使用 passport:client 命令的 --client 选项来执行此操作:

php
php artisan passport:client --client

接下来,要使用此授权类型,您需要将 CheckClientCredentials 中间件添加到 app/Http/Kernel.php 文件的 $routeMiddleware 属性中:

php
use Laravel\Passport\Http\Middleware\CheckClientCredentials;

protected $routeMiddleware = [
    'client' => CheckClientCredentials::class,
];

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

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

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

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

检索令牌

要使用此授权类型检索令牌,请向 oauth/token 端点发出请求:

php
$guzzle = new GuzzleHttp\Client;

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

return json_decode((string) $response->getBody(), true)['access_token'];

个人访问令牌

有时,您的用户可能希望在不通过典型的授权码重定向流程的情况下向自己颁发访问令牌。允许用户通过应用程序的 UI 向自己颁发令牌可以帮助用户试验您的 API,或者可以作为颁发访问令牌的一种更简单的方法。

创建个人访问客户端

在您的应用程序可以颁发个人访问令牌之前,您需要创建一个个人访问客户端。您可以使用 passport:client 命令和 --personal 选项来执行此操作。如果您已经运行了 passport:install 命令,则无需运行此命令:

php
php artisan passport:client --personal

创建个人访问客户端后,将客户端的 ID 和明文密钥值放入应用程序的 .env 文件中:

php
PASSPORT_PERSONAL_ACCESS_CLIENT_ID=client-id-value
PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET=unhashed-client-secret-value

接下来,您应该通过在 AuthServiceProviderboot 方法中放置对 Passport::personalAccessClientIdPassport::personalAccessClientSecret 的调用来注册这些值:

php
/**
 * 注册任何身份验证/授权服务。
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();

    Passport::routes();

    Passport::personalAccessClientId(
        config('passport.personal_access_client.id')
    );

    Passport::personalAccessClientSecret(
        config('passport.personal_access_client.secret')
    );
}

管理个人访问令牌

创建个人访问客户端后,您可以使用 User 模型实例上的 createToken 方法为给定用户颁发令牌。createToken 方法接受令牌名称作为其第一个参数,并接受一个可选的范围数组作为其第二个参数:

php
$user = App\User::find(1);

// 创建没有范围的令牌...
$token = $user->createToken('Token Name')->accessToken;

// 创建具有范围的令牌...
$token = $user->createToken('My Token', ['place-orders'])->accessToken;

JSON API

Passport 还包括一个用于管理个人访问令牌的 JSON API。您可以将其与您自己的前端配对,为用户提供一个仪表板来管理个人访问令牌。下面,我们将回顾管理个人访问令牌的所有 API 端点。为了方便起见,我们将使用 Axios 来演示向端点发出 HTTP 请求。

JSON API 由 webauth 中间件保护;因此,它只能从您自己的应用程序调用。无法从外部来源调用。

NOTE

如果您不想自己实现个人访问令牌前端,可以使用 frontend quickstart 在几分钟内拥有一个功能齐全的前端。

GET /oauth/scopes

此路由返回为您的应用程序定义的所有范围。您可以使用此路由列出用户可以分配给个人访问令牌的范围:

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

GET /oauth/personal-access-tokens

此路由返回经过身份验证的用户创建的所有个人访问令牌。这主要用于列出用户的所有令牌,以便他们可以编辑或撤销它们:

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

POST /oauth/personal-access-tokens

此路由创建新的个人访问令牌。它需要两条数据:令牌的 name 和应分配给令牌的 scopes

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

axios.post('/oauth/personal-access-tokens', data)
    .then(response => {
        console.log(response.data.accessToken);
    })
    .catch (response => {
        // 列出响应中的错误...
    });

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

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

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

保护路由

通过中间件

Passport 包含一个身份验证守卫,它将验证传入请求的访问令牌。一旦您将 api 守卫配置为使用 passport 驱动程序,您只需在任何需要有效访问令牌的路由上指定 auth:api 中间件:

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

多个身份验证守卫

如果您的应用程序验证不同类型的用户,这些用户可能使用完全不同的 Eloquent 模型,您可能需要为应用程序中的每个用户提供者类型定义一个守卫配置。这允许您保护特定用户提供者的请求。例如,给定以下守卫配置 config/auth.php 配置文件:

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

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

以下路由将使用 api-customers 守卫,该守卫使用 customers 用户提供者来验证传入请求:

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

NOTE

有关使用 Passport 的多个用户提供者的更多信息,请查阅密码授权文档

传递访问令牌

在调用由 Passport 保护的路由时,您的应用程序的 API 消费者应在请求的 Authorization 头中将其访问令牌指定为 Bearer 令牌。例如,使用 Guzzle HTTP 库时:

php
$response = $client->request('GET', '/api/user', [
    'headers' => [
        'Accept' => 'application/json',
        'Authorization' => 'Bearer '.$accessToken,
    ],
]);

令牌范围

范围允许您的 API 客户端在请求授权访问帐户时请求特定的权限集。例如,如果您正在构建一个电子商务应用程序,并非所有 API 消费者都需要下订单的能力。相反,您可以允许消费者仅请求授权访问订单发货状态。换句话说,范围允许应用程序的用户限制第三方应用程序可以代表他们执行的操作。

定义范围

您可以在 AuthServiceProviderboot 方法中使用 Passport::tokensCan 方法定义 API 的范围。tokensCan 方法接受一个范围名称和范围描述的数组。范围描述可以是您希望的任何内容,并将显示给用户在授权批准屏幕上:

php
use Laravel\Passport\Passport;

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

默认范围

如果客户端未请求任何特定范围,您可以配置 Passport 服务器以使用 setDefaultScope 方法将默认范围附加到令牌。通常,您应该在 AuthServiceProviderboot 方法中调用此方法:

php
use Laravel\Passport\Passport;

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

为令牌分配范围

请求授权码时

在使用授权码授权请求访问令牌时,消费者应将其所需的范围指定为 scope 查询字符串参数。scope 参数应为一个以空格分隔的范围列表:

php
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://your-app.com/oauth/authorize?'.$query);
});

颁发个人访问令牌时

如果您使用 User 模型的 createToken 方法颁发个人访问令牌,可以将所需范围的数组作为方法的第二个参数传递:

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

检查范围

Passport 包含两个中间件,可用于验证传入请求是否使用已授予给定范围的令牌进行身份验证。要开始使用,请将以下中间件添加到 app/Http/Kernel.php 文件的 $routeMiddleware 属性中:

php
'scopes' => \Laravel\Passport\Http\Middleware\CheckScopes::class,
'scope' => \Laravel\Passport\Http\Middleware\CheckForAnyScope::class,

检查所有范围

可以将 scopes 中间件分配给路由,以验证传入请求的访问令牌是否具有所有列出的范围:

php
Route::get('/orders', function () {
    // 访问令牌具有 "check-status" 和 "place-orders" 范围...
})->middleware(['auth:api', 'scopes:check-status,place-orders']);

检查任何范围

可以将 scope 中间件分配给路由,以验证传入请求的访问令牌是否具有至少一个列出的范围:

php
Route::get('/orders', function () {
    // 访问令牌具有 "check-status" 或 "place-orders" 范围...
})->middleware(['auth:api', 'scope:check-status,place-orders']);

在令牌实例上检查范围

一旦访问令牌经过身份验证的请求进入您的应用程序,您仍然可以使用经过身份验证的 User 实例上的 tokenCan 方法检查令牌是否具有给定范围:

php
use Illuminate\Http\Request;

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

其他范围方法

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

php
Laravel\Passport\Passport::scopeIds();

scopes 方法将返回所有定义的范围作为 Laravel\Passport\Scope 实例的数组:

php
Laravel\Passport\Passport::scopes();

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

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

您可以使用 hasScope 方法确定是否已定义给定范围:

php
Laravel\Passport\Passport::hasScope('place-orders');

使用 JavaScript 消费 API

构建 API 时,能够从 JavaScript 应用程序消费自己的 API 非常有用。这种 API 开发方法允许您的应用程序消费与您共享给世界的相同 API。相同的 API 可以被您的 Web 应用程序、移动应用程序、第三方应用程序以及您可能在各种包管理器上发布的任何 SDK 消费。

通常,如果您想从 JavaScript 应用程序消费 API,您需要手动将访问令牌发送到应用程序,并在每个请求中传递它。然而,Passport 包含一个中间件,可以为您处理此问题。您只需将 CreateFreshApiToken 中间件添加到 app/Http/Kernel.php 文件的 web 中间件组中:

php
'web' => [
    // 其他中间件...
    \Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,
],

NOTE

您应确保 CreateFreshApiToken 中间件是中间件堆栈中列出的最后一个中间件。

此 Passport 中间件将向您的传出响应附加一个 laravel_token cookie。此 cookie 包含一个加密的 JWT,Passport 将使用该 JWT 来验证来自 JavaScript 应用程序的 API 请求。JWT 的生命周期等于您的 session.lifetime 配置值。现在,您可以在不显式传递访问令牌的情况下向应用程序的 API 发出请求:

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

如果需要,您可以使用 Passport::cookie 方法自定义 laravel_token cookie 的名称。通常,此方法应在 AuthServiceProviderboot 方法中调用:

php
/**
 * 注册任何身份验证/授权服务。
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();

    Passport::routes();

    Passport::cookie('custom_name');
}

CSRF 保护

使用此身份验证方法时,您需要确保请求中包含有效的 CSRF 令牌头。默认的 Laravel JavaScript 脚手架包括一个 Axios 实例,该实例将自动使用加密的 XSRF-TOKEN cookie 值在同源请求中发送 X-XSRF-TOKEN 头。

NOTE

如果您选择发送 X-CSRF-TOKEN 头而不是 X-XSRF-TOKEN,您将需要使用 csrf_token() 提供的未加密令牌。

事件

Passport 在颁发访问令牌和刷新令牌时会引发事件。您可以使用这些事件来修剪或撤销数据库中的其他访问令牌。您可以在应用程序的 EventServiceProvider 中附加这些事件的监听器:

php
/**
 * 应用程序的事件监听器映射。
 *
 * @var array
 */
protected $listen = [
    'Laravel\Passport\Events\AccessTokenCreated' => [
        'App\Listeners\RevokeOldTokens',
    ],

    'Laravel\Passport\Events\RefreshTokenCreated' => [
        'App\Listeners\PruneOldTokens',
    ],
];

测试

Passport 的 actingAs 方法可用于指定当前经过身份验证的用户及其范围。传递给 actingAs 方法的第一个参数是用户实例,第二个参数是应授予用户令牌的范围数组:

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

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

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

    $response->assertStatus(201);
}

Passport 的 actingAsClient 方法可用于指定当前经过身份验证的客户端及其范围。传递给 actingAsClient 方法的第一个参数是客户端实例,第二个参数是应授予客户端令牌的范围数组:

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

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

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

    $response->assertStatus(200);
}