API 认证(Passport)
介绍
Laravel 已经使通过传统登录表单进行身份验证变得简单,但对于 API 呢?API 通常使用令牌来验证用户,并且在请求之间不维护会话状态。Laravel 使用 Laravel Passport 使 API 身份验证变得轻而易举,Passport 为您的 Laravel 应用程序提供了完整的 OAuth2 服务器实现,只需几分钟即可完成。Passport 构建在由 Andy Millington 和 Simon Hamp 维护的 League OAuth2 服务器 之上。
本文档假设您已经熟悉 OAuth2。如果您对 OAuth2 一无所知,请考虑在继续之前先熟悉 OAuth2 的一般术语和功能。
安装
首先,通过 Composer 包管理器安装 Passport:
composer require laravel/passport
Passport 服务提供者会向框架注册其自己的数据库迁移目录,因此在注册提供者后,您应该迁移数据库。Passport 迁移将创建您的应用程序需要的表来存储客户端和访问令牌:
php artisan migrate
如果您不打算使用 Passport 的默认迁移,您应该在 AppServiceProvider
的 register
方法中调用 Passport::ignoreMigrations
方法。您可以使用 php artisan vendor:publish --tag=passport-migrations
导出默认迁移。
接下来,您应该运行 passport:install
命令。此命令将创建生成安全访问令牌所需的加密密钥。此外,该命令将创建“个人访问”和“密码授权”客户端,这些客户端将用于生成访问令牌:
php artisan passport:install
运行此命令后,将 Laravel\Passport\HasApiTokens
trait 添加到您的 App\User
模型中。此 trait 将为您的模型提供一些辅助方法,允许您检查经过身份验证的用户的令牌和范围:
<?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;
}
接下来,您应该在 AuthServiceProvider
的 boot
方法中调用 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
*/
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
:
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'passport',
'provider' => 'users',
],
],
前端快速入门
为了使用 Passport Vue 组件,您必须使用 Vue JavaScript 框架。这些组件还使用 Bootstrap CSS 框架。但是,即使您不使用这些工具,这些组件也可以作为您自己前端实现的有价值的参考。
Passport 附带一个 JSON API,您可以使用它来允许用户创建客户端和个人访问令牌。然而,编写一个与这些 API 交互的前端可能会耗费时间。因此,Passport 还包括预构建的 Vue 组件,您可以将其用作示例实现或您自己实现的起点。
要发布 Passport Vue 组件,请使用 vendor:publish
Artisan 命令:
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>
部署 Passport
首次将 Passport 部署到生产服务器时,您可能需要运行 passport:keys
命令。此命令生成 Passport 生成访问令牌所需的加密密钥。生成的密钥通常不保存在源代码控制中:
php artisan passport:keys
配置
令牌有效期
默认情况下,Passport 颁发的访问令牌有效期为一年。如果您希望配置更长/更短的令牌有效期,可以使用 tokensExpireIn
和 refreshTokensExpireIn
方法。这些方法应在 AuthServiceProvider
的 boot
方法中调用:
/**
* 注册任何身份验证/授权服务。
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
Passport::routes();
Passport::tokensExpireIn(now()->addDays(15));
Passport::refreshTokensExpireIn(now()->addDays(30));
}
颁发访问令牌
使用 OAuth2 和授权码是大多数开发人员熟悉 OAuth2 的方式。使用授权码时,客户端应用程序将用户重定向到您的服务器,在那里他们将批准或拒绝向客户端颁发访问令牌的请求。
管理客户端
首先,构建需要与您的应用程序的 API 交互的应用程序的开发人员需要通过创建“客户端”来注册他们的应用程序。通常,这包括提供他们应用程序的名称和一个 URL,您的应用程序可以在用户批准他们的授权请求后重定向到该 URL。
passport:client
命令
创建客户端的最简单方法是使用 passport:client
Artisan 命令。此命令可用于为测试您的 OAuth2 功能创建您自己的客户端。当您运行 client
命令时,Passport 将提示您提供有关客户端的更多信息,并为您提供客户端 ID 和密钥:
php artisan passport:client
JSON API
由于您的用户将无法使用 client
命令,Passport 提供了一个 JSON API,您可以使用它来创建客户端。这为您节省了手动编写控制器以创建、更新和删除客户端的麻烦。
但是,您需要将 Passport 的 JSON API 与您自己的前端配对,以为用户提供一个仪表板来管理他们的客户端。下面,我们将回顾管理客户端的所有 API 端点。为了方便起见,我们将使用 Axios 来演示向端点发出 HTTP 请求。
如果您不想自己实现整个客户端管理前端,可以使用 前端快速入门 在几分钟内拥有一个功能齐全的前端。
GET /oauth/clients
此路由返回经过身份验证的用户的所有客户端。这主要用于列出用户的所有客户端,以便他们可以编辑或删除它们:
axios.get('/oauth/clients')
.then(response => {
console.log(response.data);
});
POST /oauth/clients
此路由用于创建新客户端。它需要两条数据:客户端的 name
和一个 redirect
URL。redirect
URL 是用户在批准或拒绝授权请求后将被重定向到的地方。
创建客户端时,将为其分配一个客户端 ID 和客户端密钥。这些值将在请求访问令牌时使用。客户端创建路由将返回新的客户端实例:
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 是用户在批准或拒绝授权请求后将被重定向到的地方。该路由将返回更新后的客户端实例:
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 => {
//
});
请求令牌
重定向以进行授权
创建客户端后,开发人员可以使用他们的客户端 ID 和密钥请求授权码和访问令牌。首先,消费应用程序应向您的应用程序的 /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 匹配。
如果您想自定义授权批准屏幕,可以使用 vendor:publish
Artisan 命令发布 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_token
、refresh_token
和 expires_in
属性的 JSON 响应。expires_in
属性包含访问令牌过期的秒数。
与 /oauth/authorize
路由一样,/oauth/token
路由由 Passport::routes
方法为您定义。无需手动定义此路由。
刷新令牌
如果您的应用程序颁发短期访问令牌,用户将需要通过颁发访问令牌时提供的刷新令牌来刷新他们的访问令牌。在此示例中,我们将使用 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_token
、refresh_token
和 expires_in
属性的 JSON 响应。expires_in
属性包含访问令牌过期的秒数。
密码授权令牌
OAuth2 密码授权允许您的其他第一方客户端(如移动应用程序)使用电子邮件地址/用户名和密码获取访问令牌。这允许您安全地向第一方客户端颁发访问令牌,而无需用户通过整个 OAuth2 授权码重定向流程。
创建密码授权客户端
在您的应用程序可以通过密码授权颁发令牌之前,您需要创建一个密码授权客户端。您可以使用带有 --password
选项的 passport:client
命令来执行此操作。如果您已经运行了 passport:install
命令,则无需运行此命令:
php artisan passport:client --password
请求令牌
创建密码授权客户端后,您可以通过向 /oauth/token
路由发出 POST
请求来请求访问令牌,并附上用户的电子邮件地址和密码。请记住,此路由已由 Passport::routes
方法注册,因此无需手动定义。如果请求成功,您将收到服务器返回的 JSON 响应中的 access_token
和 refresh_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',
'client_secret' => 'client-secret',
'username' => 'taylor@laravel.com',
'password' => 'my-password',
'scope' => '*',
],
]);
隐式授权令牌
隐式授权类似于授权码授权;然而,令牌在不交换授权码的情况下返回给客户端。此授权最常用于 JavaScript 或移动应用程序,其中客户端凭证无法安全存储。要启用授权,请在 AuthServiceProvider
中调用 enableImplicitGrant
方法:
/**
* 注册任何身份验证/授权服务。
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
Passport::routes();
Passport::enableImplicitGrant();
}
启用授权后,开发人员可以使用他们的客户端 ID 从您的应用程序请求访问令牌。消费应用程序应向您的应用程序的 /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
方法定义。您无需手动定义此路由。
客户端凭证授权令牌
客户端凭证授权适用于机器对机器的身份验证。例如,您可以在计划任务中使用此授权,该任务通过 API 执行维护任务。要使用此方法,您首先需要将新中间件添加到 app/Http/Kernel.php
中的 $routeMiddleware
:
use Laravel\Passport\Http\Middleware\CheckClientCredentials;
protected $routeMiddleware = [
'client' => CheckClientCredentials::class,
];
然后将此中间件附加到路由:
Route::get('/user', function(Request $request) {
...
})->middleware('client');
要检索令牌,请向 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,或者可以作为颁发访问令牌的一种更简单的方法。
个人访问令牌始终是长期有效的。使用 tokensExpireIn
或 refreshTokensExpireIn
方法时,其有效期不会被修改。
创建个人访问客户端
在您的应用程序可以颁发个人访问令牌之前,您需要创建一个个人访问客户端。您可以使用带有 --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 请求。
如果您不想自己实现个人访问令牌前端,可以使用 前端快速入门 在几分钟内拥有一个功能齐全的前端。
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);
保护路由
通过中间件
Passport 包含一个身份验证守卫,它将在传入请求时验证访问令牌。一旦您将 api
守卫配置为使用 passport
驱动程序,您只需在任何需要有效访问令牌的路由上指定 auth:api
中间件:
Route::get('/user', function () {
//
})->middleware('auth:api');
传递访问令牌
在调用由 Passport 保护的路由时,您的应用程序的 API 消费者应在请求的 Authorization
头中将其访问令牌指定为 Bearer
令牌。例如,使用 Guzzle HTTP 库时:
$response = $client->request('GET', '/api/user', [
'headers' => [
'Accept' => 'application/json',
'Authorization' => 'Bearer '.$accessToken,
],
]);
令牌范围
定义范围
范围允许您的 API 客户端在请求授权访问帐户时请求特定的权限集。例如,如果您正在构建一个电子商务应用程序,并非所有 API 消费者都需要下订单的能力。相反,您可以允许消费者仅请求授权访问订单发货状态。换句话说,范围允许您的应用程序用户限制第三方应用程序可以代表他们执行的操作。
您可以在 AuthServiceProvider
的 boot
方法中使用 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 () {
// 访问令牌具有 "check-status" 和 "place-orders" 范围...
})->middleware('scopes:check-status,place-orders');
检查任何范围
可以将 scope
中间件分配给路由,以验证传入请求的访问令牌是否具有至少一个列出的范围:
Route::get('/orders', function () {
// 访问令牌具有 "check-status" 或 "place-orders" 范围...
})->middleware('scope:check-status,place-orders');
在令牌实例上检查范围
一旦经过身份验证的请求进入您的应用程序,您仍然可以使用经过身份验证的 User
实例上的 tokenCan
方法检查令牌是否具有给定范围:
use Illuminate\Http\Request;
Route::get('/orders', function (Request $request) {
if ($request->user()->tokenCan('place-orders')) {
//
}
});
使用 JavaScript 消费 API
构建 API 时,能够从 JavaScript 应用程序消费自己的 API 非常有用。这种 API 开发方法允许您的应用程序消费与您共享给世界的相同 API。相同的 API 可以被您的 Web 应用程序、移动应用程序、第三方应用程序以及您可能在各种包管理器上发布的任何 SDK 消费。
通常,如果您想从 JavaScript 应用程序消费 API,您需要手动将访问令牌发送到应用程序,并在每次请求时传递给应用程序。然而,Passport 包含一个中间件,可以为您处理此问题。您只需将 CreateFreshApiToken
中间件添加到 app/Http/Kernel.php
文件中的 web
中间件组中:
'web' => [
// 其他中间件...
\Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,
],
此 Passport 中间件将向您的传出响应附加一个 laravel_token
cookie。此 cookie 包含一个加密的 JWT,Passport 将使用它来验证来自 JavaScript 应用程序的 API 请求。现在,您可以在不显式传递访问令牌的情况下向应用程序的 API 发出请求:
axios.get('/api/user')
.then(response => {
console.log(response.data);
});
使用此身份验证方法时,默认的 Laravel JavaScript 脚手架指示 Axios 始终发送 X-CSRF-TOKEN
和 X-Requested-With
头。然而,您应该确保在 HTML 元标记 中包含您的 CSRF 令牌:
window.axios.defaults.headers.common = {
'X-Requested-With': 'XMLHttpRequest',
};
如果您使用不同的 JavaScript 框架,您应该确保其配置为在每个传出请求中发送 X-CSRF-TOKEN
和 X-Requested-With
头。
事件
Passport 在颁发访问令牌和刷新令牌时会触发事件。您可以使用这些事件来修剪或撤销数据库中的其他访问令牌。您可以在应用程序的 EventServiceProvider
中附加这些事件的监听器:
/**
* 应用程序的事件监听器映射。
*
* @var array
*/
protected $listen = [
'Laravel\Passport\Events\AccessTokenCreated' => [
'App\Listeners\RevokeOldTokens',
],
'Laravel\Passport\Events\RefreshTokenCreated' => [
'App\Listeners\PruneOldTokens',
],
];
测试
Passport 的 actingAs
方法可用于指定当前经过身份验证的用户及其范围。传递给 actingAs
方法的第一个参数是用户实例,第二个参数是应授予用户令牌的范围数组:
public function testServerCreation()
{
Passport::actingAs(
factory(User::class)->create(),
['create-servers']
);
$response = $this->post('/api/create-server');
$response->assertStatus(200);
}