API认证(Passport)


1、简介

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

注:本文档假设你已经很熟悉 OAuth2,如果你对 OAuth2 一无所知,那么在开始学习本文档之前,先要去熟悉OAuth2的一些术语和特性(参考阮一峰博客:理解OAuth 2.0)。

2、安装

使用 Composer 包管理器安装 Passport:

composer require laravel/passport

接下来,在配置文件 config/app.phpproviders 数组中注册 Passport 服务提供者:

Laravel\Passport\PassportServiceProvider::class,

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

php artisan migrate
注:如果你不准备使用 Passport 默认的迁移,需要在AppServiceProviderregister方法中调用Passport::ignoreMigrations方法。你可以使用php artisan vendor:publish --tag=passport-migrations导出默认迁移。

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

php artisan 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方法,该方法将会注册发布/撤销访问令牌、客户端以及私人访问令牌所必需的路由:

<?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
{
    /**
     * 应用的策略映射关系.
     *
     * @var array
     * @translator laravelacademy.org
     */
    protected $policies = [
        'App\Model' => 'App\Policies\ModelPolicy',
    ];

    /**
     * 注册任意认证/授权服务.
     *
     * @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',
    ],
],

前端快速上手

注:为了使用 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')
);

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

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

注册完成后,记得运行 npm run dev 来重新编译前端资源。重新编译之后,就可以将它们放到应用的某个模板中以便创建客户端和私人访问令牌:

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

3、配置

令牌生命周期

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

use Carbon\Carbon;

/**
 * 注册所有认证/授权服务.
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();

    Passport::routes();
    Passport::tokensExpireIn(Carbon::now()->addDays(15));
    Passport::refreshTokensExpireIn(Carbon::now()->addDays(30));
}

4、颁发访问令牌

通过授权码使用 OAuth2 是大多数开发者属性的方式。使用授权码的时候,客户端应用会将用户重定向到你的服务器,服务器将会通过或决绝颁发访问令牌到客户端的请求。

管理客户端

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

passport:client命令

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

php artisan passport:client

JSON API

由于用户不能使用 client 命令,Passport提供了一个 JSON API 用于创建客户端,这省去了你手动编写控制器用于创建、更新以及删除客户端的麻烦。

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

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

GET /oauth/clients

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

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

POST /oauth/clients

这个路由用于创建新的客户端,要求传入两个数据:客户端的 nameredirect 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 => {
        // 列出响应错误信息...
    });

PUT /oauth/clients/{client-id}

这个路由用于更新客户端,要求传入两个参数:客户端的 nameredirect 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 => {
         // 列出响应错误信息...
    });

DELETE /oauth/clients/{client-id}

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

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

请求令牌

授权重定向

客户端被创建后,开发者就可以使用对应的client ID和secret从应用请求授权码和访问令牌。首先,消费者应用要发起一个重定向请求到应用的 /oauth/authorize 路由:

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

    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

将授权码转化为访问令牌

如果用户通过了授权请求,会被重定向回消费者应用。消费者接下来会发送一个 POST 请求到应用来请求访问令牌。这个请求应该包含用户通过授权请求时指定的授权码。在这个例子中,我们会使用 Guzzle HTTP库来生成 POST 请求:

Route::get('/callback', function (Request $request) {
    $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 属性包含访问令牌的过期时间(s)。

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

刷新令牌

如果应用颁发的是短期有效的访问令牌,那么用户需要通过访问令牌颁发时提供的 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)。

5、密码发放令牌

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

创建一个密码发放客户端

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

php artisan passport:client --password

请求令牌

创建完密码发放客户端后,可以通过发送 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 发放的令牌:

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

6、隐式发放令牌

隐式发放令牌和通过授权码发放类似;不过,令牌不需要兑换授权码就会返回给客户端。这种发放主要用于 JavaScript 或移动应用中客户端登录认证信息不能保存时,要启用这种发放,在 AuthServiceProvider 中调用 enableImplicitGrant 方法:

/**
 * 注册任意认证/授权服务.
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();

    Passport::routes();

    Passport::enableImplicitGrant();
}

发放被启用后,开发者就可以使用相应的 client ID 和 secret 从应用请求访问令牌了。消费者应用需要像这样发起重定向请求到/oauth/authorize路由:

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

    return redirect('http://your-app.com/oauth/authorize?'.$query);
});
记住:/oauth/authorize 路由已经在Passport::routes方法中定义了,不需要再手动定义了。

7、客户端证书发放令牌

客户端证书(用户登录凭证)发送适用于机器对机器的认证,例如,你可以在调度任务中使用这种发放通过API执行维护任务。要获取令牌,需要发起请求到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',
    ],
]);

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

8、私人访问令牌

有时候,你的用户可能想要颁发访问令牌给自己而不走典型的授权码重定向流程。允许用户通过应用的 UI 颁发令牌给自己在用户体验你的 API 或者作为更简单的颁发访问令牌方式时会很有用。

注:私人访问令牌总是一直有效的,它们的生命周期在使用 tokensExpireInrefreshTokensExpireIn 方法时不会修改。

创建一个私人访问客户端

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

php artisan passport:client --personal

管理私人访问令牌

创建好私人访问客户端之后,就可以使用 User 模型实例上的 createToken 方法为给定用户颁发令牌。 createToken 方法接收令牌名称作为第一个参数,以及一个可选的域数组作为第二个参数:

$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 请求到 API。

注:如果你不想要实现自己的私人访问令牌前端,可以使用前端快速上手教程在数分钟内打造拥有完整功能的前端。
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 => {
        // 列出响应错误信息...
    });

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

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

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

9、路由保护

通过中间件

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

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

传递访问令牌

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

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

10、令牌作用域

定义作用域

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

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

use Laravel\Passport\Passport;

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

分配作用域到令牌

请求授权码时

当使用授权码请求访问令牌时,消费者应该指定他们期望的作用域作为 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')) {
        //
    }
});

11、使用JavaScript消费API

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

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

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

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

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

使用认证的这个方法时,Axios 会自动发送 X-CSRF-TOKEN 头,此外,默认的 Laravel JavaScript 脚手架会告知 Axios 发送 X-Requested-With 头:

window.axios.defaults.headers.common = {
    'X-Requested-With': 'XMLHttpRequest',
};
注:如果你使用的是其它JavaScript框架,需要确保每个请求都被配置为发送 X-CSRF-TOKENX-Requested-With 请求头。

12、事件

Passport 会在颁发访问令牌和刷新令牌时触发事件,我们可以使用这些事件来修剪或撤销数据库中的访问令牌。你可以在应用的EventServiceProvider中附加监听器到这些事件:

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

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

13、测试

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

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

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

    $response->assertStatus(200);
}

点赞 取消点赞 收藏 取消收藏

<< 上一篇: 用户认证

>> 下一篇: 用户授权