Laravel Passport

简介

Laravel 通过传统的登录表单已经让用户认证变得很简单,但是 API 认证怎么实现?API 通常使用令牌(token)进行认证并且在请求之间不维护会话(Session)状态。Laravel 官方扩展包 Laravel Passport 让 API 认证变得轻而易举,Passport 基于 Alex Bilbie 维护的 League OAuth2 server,可以在数分钟内为 Laravel 应用提供完整的 OAuth2 服务器实现。

注:该文档建立在你已经很熟悉 OAuth2 的基础之上,如果你还不知道什么是 OAuth2,请先阅读官方文档


升级 Passport

升级到 Passport 的新主要版本时,请务必仔细阅读升级指南。


安装

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

$ composer require laravel/passport

Passport Service Provider为框架注册了自己的数据库迁移目录,所以在注册服务提供者之后(Laravel 5.5之后会自动注册服务提供者)需要迁移数据库,Passport 迁移将会为应用生成用于存放客户端和访问令牌的数据表:

$ php artisan migrate

接下来,需要运行 passport:install 命令,该命令将会创建生成安全访问令牌(token)所需的加密键,此外,该命令还会创建“personal access”和“password grant”客户端用于生成访问令牌:

$ php artisan passport:install

运行完passport:install命令后,添加 Laravel\Passport\HasApiTokens trait 到 App\User 模型,该 trait 将会为模型类提供一些辅助函数用于检查认证用户的 token 和 scope:

<?php
namespace App;
use Laravel\Passport\HasApiTokens;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
    use HasApiTokens, Notifiable;
}

接下来,需要在 AuthServiceProviderboot 方法中调用 Passport::routes 方法,该方法将会为创建访问 token、撤销访问token、客户端以及私人访问令牌注册必要的路由:

<?php
namespace App\Providers;
use Laravel\Passport\Passport;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        'App\Model' => 'App\Policies\ModelPolicy',
    ];
    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();
        Passport::routes();
    }
}

最后,在配置文件 config/auth.php 中,需要设置 api 认证 guard 的 driver 选项为 passport。这将告知应用在认证输入的 API 请求时使用 Passport 的 TokenGuard:

'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],
​
    'api' => [
        'driver' => 'passport',
        'provider' => 'users',
        'hash' => false,
    ],
],

客户端 UUID

我们可以在passport:install命令后面跟上--uuids选项。此选项将告诉 Passport ,我们希望使用 UUID 而不是自动递增的整数作为 PassportClient模型的主键值。运行该命令后,我们将获得有关禁用 Passport 的默认迁移的附加说明:

$ php artisan passport:install --uuids

前端快速入门

注:如果要使用 Passport Vue 组件,前端 JavaScript 必须使用 Vue 框架,这些组件同时也使用了 Bootstrap CSS 框架。不过,即使你不使用这些工具,这些组件同样可以为你实现自己的前端组件提供有价值的参考。

Passport 附带了 JSON API 以便用户创建客户端和私人访问令牌(access token)。不过,考虑到编写前端代码与这些 API 交互是一件很花费时间的事,Passport 还预置了 Vue 组件作为示例以供使用(或者作为自己实现的参考)。

要发布 Passport Vue 组件,可以使用 vendor:publish 命令:

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

发布后的组件位于 resources/assets/js/components 目录下,组件发布之后,还需要将它们注册到 resources/assets/js/app.js 文件:

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

注:在 Laravel 5.7.19 之前的版本,在注册组件时附加 .default 后缀会导致控制台错误。有关此次更改的说明,请参考 Laravel Mix v4.0.0 版本发行说明。

注册完组件后,确保运行 npm run dev 来重新编译前端资源。重新编译前端资源后,就可以将这些组件放到应用的某个模板中以便创建客户端和私人访问令牌:

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

部署 Passport

第一次部署 Passport 到生产服务器时,可能需要运行 passport:keys 命令。这个命令生成 Passport 需要的密钥以便生成访问令牌,生成的密钥将不会存放在源代码控制中:

$ php artisan passport:keys

如果必要的话,可以定义 Passport 密钥的加载路径,这可以通过使用 Passport::loadKeysFrom 方法来实现:

/**
 * Register any authentication / authorization services.
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();
    Passport::routes();
    Passport::loadKeysFrom('/secret-keys/oauth');
}

此外,我们还可以使用 php artisan vendor:publish --tag=passport-config 发布 Passport 的配置文件,它将会从环境变量中加载加密密钥选项:

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 

默认情况下,Passport 使用整型列来存储 user_id,如果你的应用使用了不同的列类型来标识用户(例如 UUID),需要在发布默认迁移之后对其进行修改。


配置

客户端哈希密钥

如果存储在数据库中,当我们想客户的密钥被散列,应该在AppServiceProviderboot方法中调用Passport::hashClientSecrets方法:

Passport::hashClientSecrets();

启用后,我们的所有客户端密钥将在创建客户端时显示一次。由于纯文本客户端密钥值永远不会存储在数据库中,因此如果丢失则无法恢复。

令牌生命周期

默认情况下,Passport 颁发的访问令牌(access token)是长期有效的,如果想要配置生命周期短一点的令牌,可以使用 tokensExpireInrefreshTokensExpireInpersonalAccessTokensExpireIn 方法,这些方法需要在 AuthServiceProviderboot 方法中调用:

/**
 * Register any authentication / authorization services.
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();

    Passport::routes();

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

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

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

覆盖默认模型

我们可以按需扩展 Passport 底层使用的方法:

use Laravel\Passport\Client as PassportClient;

class Client extends PassportClient
{
    // ...
}

然后,通过 Passport 类告知 Passport 使用自定义的方法:

use App\Models\Passport\Client;
use App\Models\Passport\Token;
use App\Models\Passport\AuthCode;
use App\Models\Passport\PersonalAccessClient;
/**
 * Register any authentication / authorization services.
 *
 * @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 是大多数开发者熟悉的方式。使用授权码的时候,客户端应用会将用户重定向到我们的服务器,服务器可以通过或拒绝发布访问令牌到客户端的请求。

管理客户端

首先,开发者构建和 Laravel 应用 API 交互的应用时,需要通过创建一个“客户端”将他们的应用注册到 Laravel 应用。通常,这包括提供应用的名称以及用户授权请求通过后重定向到的 URL。

passport:client 命令

创建客户端最简单的方式就是使用 Artisan 命令 passport:client,该命令可用于创建你自己的客户端以方便测试 OAuth2 功能。当你运行 client 命令时,Passport 会提示你输入更多关于客户端的信息,并且为你生成 client ID 和 secret:

$ php artisan passport:client

重定向 URL

如果我们想要将客户端的多个重定向网址列入白名单,可以在 passport:client 命令交互提示中使用一个通过逗号分隔的列表来指定它们:

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

注:任何包含逗号的 URL 都需要被编码。

JSON API

由于第三方应用开发者不能直接使用 Laravel 服务端提供的 client 命令,为此,Passport 提供了一个 JSON API 用于创建客户端,这省去了我们手动编写控制器用于创建、更新以及删除客户端的麻烦。

不过,我们需要配对 Passport 的 JSON API 和自己的前端以便为第三方开发者提供一个可以管理他们自己客户端的后台,下面,我们来概览下所有用于管理客户端的 API,为了方便起见,我们将会使用Axios 来演示发送 HTTP 请求到 API:

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

注:如果你不想要自己实现整个客户端管理前端,可以使用前端快速上手教程在数分钟内搭建拥有完整功能的前端。

GET /oauth/clients

这个路由为认证用户返回所有客户端,这在展示用户客户端列表时很有用,可以让用户很容易编辑或删除客户端:

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

POST /oauth/clients

这个路由用于创建新的客户端,要求传入两个数据:客户端的 name 和 redirect URL, redirect URL 是用户授权请求通过或拒绝后重定向到的位置。

当客户端被创建后,会附带一个 client ID 和 secret,这两个值会在请求访问令牌时用到。客户端创建路由会返回新的客户端实例:

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 是用户授权请求通过或拒绝后重定向到的位置。该路由将会返回更新后的客户端实例:

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}

这个路由用于删除客户端:

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

请求 token

授权重定向

客户端被创建后,开发者就可以使用相应的 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://example.com/callback',
        'response_type' => 'code',
        'scope' => '',
        'state' => $state,
    ]);
    return redirect('http://your-app.com/oauth/authorize?'.$query);
});

注:/oauth/authorize 路由已经通过 Passport::routes 方法定义了,不需要手动定义这个路由。

通过授权请求

接收授权请求的时候,Passport 会自动显示一个视图模板给用户从而允许他们通过或拒绝授权请求(如上图所示),如果用户通过请求,就会被重定向回第三方应用指定的 redirect_uri,这个 redirect_uri 必须和客户端创建时指定的 redirect URL 一致。

如果想要自定义授权通过界面,可以使用 Artisan 命令 vendor:publish 发布 Passport 的视图模板,发布的视图位于 resources/views/vendor/passport:

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

有时候我们可能希望跳过授权提示页面,比如像我们这个示例这样,认证请求发起方和第三方授权通过方位于同一个应用中,这可以通过在客户端模型中定义 skipsAuthorization 方法来完成。如果 skipsAuthorization 方法返回 true 则客户端将会被授权通过,用户将直接被重定向到 redirect_uri 指定的页面:

<?php
namespace App\Models\Passport;
use Laravel\Passport\Client as BaseClient;
class Client extends BaseClient
{
    /**
     * Determine if the client should skip the authorization prompt.
     *
     * @return bool
     */
    public function skipsAuthorization()
    {
        return $this->firstParty();
    }
}

将授权码转化为访问令牌

如果用户通过了授权请求,会被重定向回客户端应用。客户端应用首先会验证之前传递给授权服务方的 state 参数,如果该参数与之前传递的参数值匹配,则客户端会发起一个 POST 请求到服务端来请求一个访问令牌。这个请求应该包含用户通过授权请求时指定的授权码。在这个例子中,我们会使用 Guzzle HTTP 库来生成 POST 请求:

Route::get('/auth/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://blog.test/oauth/token', [
        'form_params' => [
            'grant_type' => 'authorization_code',
            'client_id' => '3',  // your client id
            'client_secret' => 'z0xmgDStS67fRt0A9qlExA6zqSY7WupZskIm31Gt',  // your client secret
            'redirect_uri' => 'http://blog.test/auth/callback',
            'code' => $request->code,
        ],
    ]);
​
    return json_decode((string) $response->getBody(), true);
});

/oauth/token 路由会返回一个包含 token_typeaccess_tokenrefresh_tokenexpires_in 属性的 JSON 响应。expires_in 属性包含访问令牌的过期时间(s):

注:和 /oauth/authorize 路由一样, /oauth/token 路由已经通过 Passport::routes 方法定义过了,不需要手动定义这个路由。默认情况下,该路由使用 ThrottleRequests 中间件设置对访问频率进行限制。

JSON API

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

GET /oauth/tokens

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

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

DELETE /oauth/tokens/{token-id}

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

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

刷新令牌

如果应用颁发的是短期有效的访问令牌,那么用户需要通过访问令牌颁发时提供的 refresh_token 刷新访问令牌,在本例中,我们使用 Guzzle HTTP 库来刷新令牌:

$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 属性包含访问令牌过期时间(s)。

撤销令牌

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

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

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

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

清除令牌

当令牌已撤销或者已过期,我们可能想要从数据库中清除它们。Passport 通过提供以下命令帮你处理令牌的清除:

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

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

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

我们还可以在控制台 Kernel 类中配置调度任务来定时自动清理令牌:

/**
 * Define the application's command schedule.
 *
 * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
 * @return void
 */
protected function schedule(Schedule $schedule)
{
    $schedule->command('passport:purge')->hourly();
}

通过 PKCE 颁发授权码

通过“Proof Key for Code Exchange”(PKCE)颁发授权码是一个对单页面应用或者原生应用进行认证以便访问 API 接口的安全方式。这种颁发方式的适用场景是当你不能保证客户端密钥被安全存储,或者为了降低攻击者拦截授权码的威胁。在这种模式下,当通过授权码获取访问令牌时,「验证码」和「质询码」的组合将替换客户端密钥。

创建客户端

在应用可以通过基于 PKCE 颁发的授权码颁发令牌前,你需要创建一个启用 PCKE 的客户端,这可以通过调用带有 --public 选项的 passport:client 命令来完成:

$ php artisan passport:client --public

请求令牌

验证码 & 授权码

由于这个认证授权没有提供客户端密钥,所以开发者需要生成一个代码验证和代码质询的组合以便请求令牌。

验证码是一个长度介于 43 和 128 之间的、包含字母、数字、以及 -、_、~ 字符的随机字符串(遵循 RFC 7636 标准)。

质询码是一个包含 URL 和文件名安全字符的 Base64 加密字符串。末尾的 = 应该被移除,也不能出现换行符、空白字符、或者其他额外字符。

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

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

认证重定向

客户端被创建之后,可以在应用中使用客户端 ID 和生成的验证码、质询码来请求授权码和访问令牌。首先,客户端应用应该发起一个指向服务端 oauth/authorize 路由的重定向请求:

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 参数与重定向到服务端应用前保存的值是否相等,和我们在标准授权码颁发令牌所做的一样。

如果状态参数值匹配,则客户端应用会发起一个 POST 请求到服务端应用来获取访问令牌,该请求需要包含上一步颁发的授权码:

Route::get('/auth/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://blog.test/oauth/token', [
        'form_params' => [
            'grant_type' => 'authorization_code',
            'client_id' => 'client-id',
            'redirect_uri' => 'http://blog.test/auth/callback',
            'code_verifier' => $codeVerifier,
            'code' => $request->code,
        ],
    ]);

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

密码授权令牌

OAuth2 密码授权允许你的其他第一方客户端,例如移动应用,使用邮箱地址/用户名+密码获取访问令牌。这使得我们可以安全地颁发访问令牌给第一方客户端而不必要求你的用户走整个 OAuth2 授权码重定向流程。

创建一个密码发放客户端

在应用可以通过密码授权颁发令牌之前,需要创建一个密码授权客户端,我们可以通过使用带 --password 选项的 passport:client 命令来实现。如果你已经运行了 passport:install 命令,则不必再运行这个命令:

$ php artisan passport:client --password

这里我们使用一开始通过 passport:install 命令创建的记录作为测试记录。

请求令牌

创建完密码授权客户端后,可以通过发送 POST 请求到 /oauth/token 路由(带上用户邮箱地址和密码)获取访问令牌。这个路由已经通过 Passport::routes 方法注册过了,不需要手动定义。如果请求成功,就可以从服务器返回的 JSON 响应中获取 access_tokenrefresh_token

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

注:记住,访问令牌默认长期有效,不过,如果需要的话我们也可以配置访问令牌的最长生命周期。

请求所有域

使用密码授权的时候,你可能想要对应用所支持的所有域进行令牌授权,这可以通过请求 * 域来实现。如果你请求的是 * 域,则令牌实例上的 can 方法总是返回 true,这个域只会分配给使用 password 或 client_credentials 授权的令牌:

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

自定义用户提供程序

如果我们的应用程序使用多个身份验证用户提供程序,我们可以通过在artisan passport:client --password命令创建客户端时提供--provider选项来指定密码授予客户端使用哪个用户provider。给定的提供程序名称应与config/auth.php配置文件中定义的有效提供程序匹配。然后,我们可以使用中间件保护您的路由,以确保只有来自guard指定provider的用户才能获得授权。

自定义用户名字段

当使用密码授权进行认证的时候,Passport 会使用模型的 email 属性作为「用户名」。不过,你可以通过在模型上定义 findForPassport 方法来自定义这一默认行为:

<?php
namespace App;
use Laravel\Passport\HasApiTokens;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
    use HasApiTokens, Notifiable;
    /**
     * Find the user instance for the given username.
     *
     * @param  string  $username
     * @return \App\User
     */
    public function findForPassport($username)
    {
        return $this->where('username', $username)->first();
    }
}

自定义密码验证

当使用密码授权进行认证时,Passport 会使用模型的 password 属性来验证给定密码。如果你的模型没有 password 属性或者你希望自定义密码验证逻辑,可以在模型中定义一个 validateForPassportPasswordGrant 方法:

<?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;
    /**
    * Validate the password of the user for the Passport password grant.
    *
    * @param  string $password
    * @return bool
    */
    public function validateForPassportPasswordGrant($password)
    {
        return Hash::check($password, $this->password);
    }
}

隐式授权令牌

隐式授权和授权码授权有点相似,不过,无需获取授权码,令牌就会返回给客户端。这种授权通常应用于 JavaScript 或移动应用这些客户端凭证不能被安全存储的地方。要启用该授权,在 AuthServiceProvider 中调用 enableImplicitGrant 方法即可:

/**
 * Register any authentication / authorization services.
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();
    Passport::routes();
    Passport::enableImplicitGrant();
}

授权启用后,开发者就可以使用他们的 client ID 从应用中请求访问令牌,第三方应用需要像这样发送重定向请求到应用的 /oauth/authorize 路由:

Route::get('/redirect', function () {
    $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);
});

注:/oauth/authorize 路由已经在 Passport::routes 方法中定义过了,无需再手动定义这个路由。


客户端凭证授权令牌

客户端凭证授权适用于机器对机器的认证,例如,我们可以在调度任务中使用这种授权来通过 API 执行维护任务。

在应用可以通过客户端凭证授权颁发令牌之前,需要先创建一个客户端凭证授权客户端,这可以通过在运行 passport:client 命令时添加 --client 选项来实现:

$ php artisan passport:client --client

要使用这个方法,需要在 app/Http/Kernel.php 中添加新的中间件 CheckClientCredentials 到 $routeMiddleware:

use Laravel\Passport\Http\Middleware\CheckClientCredentials;
protected $routeMiddleware = [
    '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

$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 或者作为更简单的颁发访问令牌方式时会很有用。

创建一个私人访问客户端

在我们的应用可以颁发私人访问令牌之前,需要创建一个私人访问客户端。可以通过带 --personal 选项的 passport:client 命令来实现,如果已经运行过了 passport:install 命令,则不必再运行此命令:

$ php artisan passport:client --personal

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

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

接下来,我们应该在AuthServiceProvider中的boot方法里调用 Passport::personalAccessClientIdPassport::personalAccessClientSecret 来注册这些值:

/**
 * Register any authentication / authorization services.
 *
 * @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 方法接收令牌名称作为第一个参数,以及一个可选的域数组作为第二个参数:

$user = App\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 请求到 API。

JSON API 由 webauth 中间件守卫,因此,只能在自己的应用中调用,不能从外部应用调用。

注:如果我们不想要实现自己的私人访问令牌前端,可以使用前端快速上手教程在数分钟内打造拥有完整功能的前端。

GET /oauth/scopes

这个路由会返回应用所定义的所有域。你可以使用这个路由来列出用户可以分配给私人访问令牌的所有域:

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

GET /oauth/personal-access-tokens

这个路由会返回该认证用户所创建的所有私人访问令牌,这在列出用户的所有令牌以便编辑或删除时很有用:

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

POST /oauth/personal-access-tokens

这个路由会创建一个新的私人访问令牌,该路由要求传入两个参数:令牌的 name 和需要分配到这个令牌的 scopes:

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}

这个路由可以用于删除私人访问令牌:

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

路由保护

通过中间件

Passport 提供了一个认证守卫 用于验证输入请求的访问令牌,当你使用 passport 驱动配置好 api guard 后,只需要在所有路由上指定需要传入有效访问令牌的 auth:api 中间件即可:

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

多重认证保护

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

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

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

以下路由将利用使用customers用户提供程序的api-customers守卫来验证传入的请求:

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

传递访问令牌

调用被 Passport 保护的路由时,应用 API 的消费者需要在请求的 Authorization 头中指定它们的访问令牌作为 Bearer 令牌。例如:

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

**

令牌作用域

作用域(Scope)允许 API 客户端在请求账户授权的时候请求特定的权限集合。例如,如果你在构建一个电子商务应用,不是所有的 API 消费者都需要下订单的能力,取而代之地,你可以让这些消费者只请求访问订单物流状态的权限,换句话说,作用域允许你的应用用户限制第三方应用自身可以执行的操作。

定义作用域

我们可以在 AuthServiceProviderboot 方法中使用 Passport::tokensCan 方法定义 API 的作用域。 tokensCan 方法接收作用域名称数组和作用域描述,作用域描述可以是任何你想要在授权通过页面展示给用户的东西:

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

默认作用域

如果一个客户端没有请求任何指定域,可以通过 setDefaultScope 方法配置 Passport 服务端添加一个默认的域到令牌。通常,我们在 AuthServiceProvider 的 boot 方法中调用这个方法:

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

分配作用域到令牌

请求授权码

当使用授权码请求访问令牌时,消费者应该指定他们期望的作用域作为 scope 查询字符串参数, 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://your-app.com/oauth/authorize?'.$query);
});

发布私人访问令牌

如果你使用 User 模型的 createToken 方法颁发私人访问令牌,可以传递期望的作用域数组作为该方法的第二个参数:

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

检查作用域

Passport 提供了两个可用于验证输入请求是否经过已发放作用域的令牌认证的中间件。开始使用之前,添加如下中间件到 app/Http/Kernel.php 文件的 $routeMiddleware 属性:

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

检查所有作用域

scopes 中间件会分配给一个用于验证输入请求的访问令牌拥有所有列出作用域的路由:

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

检查任意作用域

scope 中间件会分配给一个用于验证输入请求的访问令牌拥有至少一个列出作用域的路由:

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

检查令牌实例上的作用域

当一个访问令牌认证过的请求进入应用后,你仍然可以使用经过认证的 User 实例上的 tokenCan 方法来检查这个令牌是否拥有给定作用域:

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

额外的域方法

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

Laravel\Passport\Passport::scopeIds();

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

Laravel\Passport\Passport::scopes();

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

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

我们可以使用 hasScope 方法判断给定域是否被定义过:

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

使用 JavaScript 消费 API

构建 API 时,能够从你的 JavaScript 应用消费你自己的 API 非常有用。这种 API 开发方式允许你自己的应用消费你和其他人分享的同一个 API,这个 API 可以被你的 Web 应用消费,也可以被你的移动应用消费,还可以被第三方应用消费,以及任何你可能发布在多个包管理器上的 SDK 消费。

通常,如果我们想要从 JavaScript 应用消费自己的 API,需要手动发送访问令牌到应用并在应用的每一个请求中传递它。不过,Passport 提供了一个中间件用于处理这一操作。你所需要做的只是在 app/Http/Kernel.php 文件中添加这个中间件 CreateFreshApiToken 到 web 中间件组:

'web' => [
    // Other middleware...
    \Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,
],

注:你需要确保 EncryptCookies 中间件位于 CreateFreshApiToken 中间件之前执行。

这个 Passport 中间件将会附加 laravel_token Cookie 到输出响应,这个 Cookie 包含加密过的JWT,Passport 将使用这个 JWT 来认证来自 JavaScript 应用的 API 请求,现在,你可以发送请求到应用的 API,而不必显示传递访问令牌:

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

如果需要的话,你可以使用 Passport::cookie 方法自定义 laravel_token Cookie 的名称。通常,我们在 AuthServiceProviderboot 方法中调用相应的方法:

/**
 * Register any authentication / authorization services.
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();
    Passport::routes();
    Passport::cookie('custom_name');
}

CSRF 保护

使用这种认证方法时,需要确保在请求头中包含了有效的 CSRF 令牌。默认的 Laravel JavaScript 脚手架代码包含了 Axios 实例,它会自动使用加密的 XSRF-TOKEN Cookie 值在同一个请求域下发送 X-XSRF-TOKEN 请求头。

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


事件

Passport 会在颁发访问令牌和刷新令牌时触发事件,你可以使用这些事件来处理或撤销数据库中的其它访问令牌,你可以在应用的 EventServiceProvider 中添加监听器到这些事件:

/**
 * The event listener mappings for the application.
 *
 * @var array
 */
protected $listen = [
    'Laravel\Passport\Events\AccessTokenCreated' => [
        'App\Listeners\RevokeOldTokens',
    ],
    'Laravel\Passport\Events\RefreshTokenCreated' => [
        'App\Listeners\PruneOldTokens',
    ],
];

测试

Passport 的 actingAs 方法可用于指定当前认证用户及其作用域,传递给 actingAs 方法的第一个参数是用户实例,第二个参数是授权给用户令牌的作用域数组:

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 方法可用于指定当前的认证客户端及其作用域,该方法的第一个参数是客户端实例,第二个参数是分配给客户端令牌的作用域数组:

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

查看笔记

扫码一下
查看教程更方便