大多数 API 需要某种形式的身份验证才能访问部分或全部数据,在 Laravel 5.3 及更高版本中,我们有一个名为Passport 的新工具(一个单独的包,通过 Composer 引入),在应用程序中提供功能齐全的 OAuth 2.0 服务器,其中包含用于管理客户端和令牌的 API 和 UI 组件。
OAuth 2.0 简介
对于 OAuth 最简单的解释: 因为 APIs 是无状态的,我们不能像在普通浏览器中请求网页一样使用 session ,也就是说,用户登录后保存登录与授权状态到 session 中,并用于随后的请求; 而是 API客户端需要对身份验证端点进行一次调用,并执行某种形式的握手来证明自己。然后,它取回令牌,令牌必须与以后的每个请求(通常通过 Authorization 标头)一起发送以证明其身份。
参考 Oauth简介
Laravel 5.3 之后提供 Passport 包用于 OAuth 2.0 认证。
安装 Passport
composer require laravel/passport
Laravel 5.5 以下版本还需添加Laravel\Passport\PassportServiceProvider::class 到 config/app.php 的 providers 数组中。
Passport 提供了一系列数据迁移, 执行 php artisan migrate 生成 OAuth clients, scopes, tokens 所需的表。
下一步,使用 php artisan passport:install. 它将为 OAuth 服务器创建密钥(storage/oauth-private.key 和 storage/oauth-public.key) 并且将 OAuth 客户端插入到数据库以获取个人式和密码式的授权令牌。
你需要导入 Laravel\Passport\HasApiTokens trait 到 User 模型; 这会将OAuth客户端和令牌相关的关系添加到每个User,如一些与令牌相关的辅助方法。 然后,在 AuthServiceProvider 的 boot() 方法中添加 Laravel\Passport\Passport::routes(),它将增加以下路由:
• oauth/authorize
• oauth/clients
• oauth/clients/client_id
• oauth/personal-access-tokens
• oauth/personal-access-tokens/token_id
• oauth/scopes
• oauth/token
• oauth/token/refresh
• oauth/tokens
• oauth/tokens/token_id
最后,在 config/auth.php 中修改 api guard. 默认是 token 驱动的,改为 passport 驱动。
现在,你已经有了一个功能齐全的 OAuth 2.0 服务器。 你可以使用 php artisan passport:client 创建一个新的客户端,你可以使用 /oauth 前缀的路由来管理你的客户端。
要使用 Passport 身份验证系统保护一个路由,只需将 auth:api 中间件添加到对应的路由即可。
// routes/api.php
Route::get('/user', function (Request $request) {
return $request->user();
})->middleware('auth:api');
为了对那些受保护的路由进行身份认证,你的客户端应用需要传递有一个 token ,形式是在 Authorization header 中,作为 Bearer token。 如下使用 Guzzle HTTP 库做例子。
$http = new GuzzleHttp\Client;
$response = $http->request('GET', 'http://tweeter.test/api/user', [
'headers' => [
'Accept' => 'application/json',
'Authorization' => 'Bearer ' . $accessToken,
],
]);
现在,让我们仔细看看它是如何工作的。
Passport’s API
Passport 会在 /oauth 路由前缀下暴露出你的应用的一个api。 这个 api 提供零个主要作用: 首先,使用 OAuth 2.0 认证体系/认证流(/oauth/authorize and /oauth/token )认证用户; 其次,允许用户管理他们的客户端和 token (其余的路由)。
这有一个很重要的区别,特别是你不太了解 OAuth 。 每台 OAuth 服务器都需要向用户提供进行身份验证的能力;这就是服务的重点。 但是 Passport 还公开了一个管理 OAuth服务的客户端和令牌状态的API。 这意味着您可以轻松构建前端以让您的用户在OAuth应用程序中管理其信息,并且 Passport 实际上带有基于 Vue 的管理器组件,您可以使用它们或从中获取灵感。
Passport 的授权方式
Passport 提供四种不同方式对用户进行身份验证。 两种是传统的 OAuth 2.0 授权(密码许可: password grant 和授权码授予: authorization code grant),还有两种是 Passport 特有的便捷方法(个人令牌: personal token 和同步器令牌: synchronizer token)。
密码许可(password grand)
密码许可比授权码许可用的少,但是更简单。用的少的原因在于安全性不高。允许第三方用户在调用你的 API 时直接使用你平台上的用户名和密码直接进行身份验证,也就以为这你要把用户名密码告诉第三方,显示只适用于自己人或者非常信任的第三方,比如自己的网站系统要做一个手机 app。
授权码许可
授权码许可 OAuth 2.0 授权工作流中最常用也是最复杂的。 比如,我们开发了微信用于日常聊天等,现在还有另一个应用如b站。 我们可以在微信这个应用中安装 Passport,使b站可以允许b站用户,使用微信的授权信息登录等。
在授权码许可中,每一个第三方应用(本例中:b站),都需要在我们的 Passport-enabled 应用(微信)中创建一个 client。一般是 b 站开发人员,去微信开放平台之类的地方申请一个账号并创建一个客户端,这是微信提供的创建客户端的工具。简单起见,这里我们手动创建客户端。
// 在命令行中执行 创建客户端
php artisan passport:client
// 每个客户端需要绑定一个用户, 用户 1 将是整个客户端的拥有者
Which user ID should the client be assigned to?:
> 1
// 起名字
What should we name client?:
> Bilibili
// 授权成功后的回调地址
Where should we redirect the request after authorization [http://tweeter.test/auth/callback]:
> http://bilibili.test/wechat/callback
New client created successfully.
Client ID: 4
Client secret: 5rzqKpeCjIgz3MXpi3tjQ37HBnLLykrgWgmc18uH
现在,我们有了 bilibili 这个客户端的 ID 和 secret , bilibili 可以使用这两者,创建一个工具,这个工具可以允许单个 bilibili 用户(也是 wechat 用户)从 wechat 处获取身份验证令牌,这个令牌可以代表微信用户对微信进行 API 调用(比如获取头像昵称等)。
我们假设 b站 是 laravel 写的。简单模拟一下
// b站 routes/web.php
Route::get('wechat/redirect', function(){
$query = http_build_query([
'client_id'=>4, // 刚才创建的 client 的 id
'redirect_uri'=>url('wechat/callback'),
'response_type'=>'code',
'scope'=>'',
]);
// Builds a string like:
// client_id={$client_id}&redirect_uri={$redirect_uri}&response_type=code
return redirect('http://wechat.test/oauth/authorize?' . $query);
})
当用户在 b站 中点击以上路由时, 他们会重定向到微信 Passport 路由的 /oauth/authorize 。这里他们将看到一个确认页面——你可以使用 Passport 组件预设的确认页面,只需要执行以下命令:
php artisan vendor:publish --tag=passport-views
这个命令会发布一个页面 resources/views/vendor/passport/authorize.blade.php .
一旦用户选择了接受或拒绝, Passport 会重定向,用户就会返回到提供的 redirect_uri 。如上,我们设置的 redirect_uri 是 url(wechat/callback) 那么我们重定向返回的地址是 http://bilibili.test/wechat/callback 。
允许授权的请求(点击允许授权)将包含一个 code , 现在第三方应用(本例 bilibili)可以在毁掉路由中使用这个 code 去换取一个授权令牌 token (微信的)。拒绝授权的请求,将包含错误信息。bilibili 的回调路由可能如下:
// b站 routes/web.php
Route::get('wechat/callback', function(Request $request){
if($request->has('error')){
// 处理错误信息
}
$http = new GuzzleHttp\Client;
$response = $http->post('http://wechat.test/oauth/token',[
'form_params'=>[
'grant_type' => 'authorzation_code',
'client_id' => 4 , // 创建时生成的
'client_secret' => '5rzqKpeCjIgz3MXpi3tjQ37HBnLLykrgWgmc18uH' // 创建时生成的
'redirect_uri' => url('wechat/callback'),
'code' => $request->code
],
]);
$thisUserTokens = json_decode((striing) $response->getBody(), true);
// Do stuff with the tokens
});
b站的开发者,使用 Guzzle HTTP 请求微信的 /oauth/token 路由,这是一个 POST 请求,且包含了刚才允许授权时获得的 授权码 code, 然后微信将返回一个包含一些授权信息 json:
- access_token : 这个令牌b站需要保存,之后用户向微信的请求中将使用该令牌进行身份验证(使用Authorization标头)
- refresh_token: 如果 access_token 过期了,则可以使用该token重新获取一次,而不是重复以上步骤。默认 access_token 有效期是 1 年
- expires_in: 直到 access_token 过期的秒数。
- token_type: 你获取的令牌的类型,即 Bearer;这意味着之后的使用令牌请求时,你需要传递参数的名字,你需要在请求标头中放置名字是
Authorization值是Bearer YOURTOKENHERE的项。
使用 Refresh Token
如果你想强制你的用户经常重新认证,那么你需要设置一个较短的 token 过期时间,然会当 access_token 过期你需要重新获取时(大多是你获取状态 401 的响应时,即:未授权),你需要使用 refresh_token 重新请求获取一个新的 access_token.
// eg: 定义 token 刷新时间
P
// AuthServiceProvider.php 的 boot() 方法
public function boot(){
$this->registerPolicities();
Passport::routes();
// token 有效期持续多久
Passport::tokensExpireIn(
now()->addDays(15)
);
// refresh_token 有效期持续多久
Passport::refreshTokensExpireIn(
now()->addDays(30)
);
}
当需要使用 refresh_token 请求一个 access_token 时,首先你需要在初始身份验证时,保存 refresh_token , 然后,只需将刚才的代码稍加修改即可。
// b站 routes/web.php
Route::get('wechat/request-refresh', function(Request $request){
$http = new GuzzleHttp\Client;
$params = [
'grant_type' => 'refresh_token',
'client_id' => config('wechat.id'), // 可以放到配置里,上边的例子也是
'client_secret' => config('wechat.secret'),
'redirect_uri' => url('wechat/callback'),
'refresh_token' => $theTokenYouSavedEarlier,
'scope' => '',
];
$response = $http->post(
'http://wechat.test/oauth/token',
['form_params' => $params]
);
$thisUserTokens = json_decode( (string) $response->getBody(), true );
// Do stuff with the tokens
})
这个响应中, 第三方应用会收到一组新的 token 。
个人访问令牌 (Personal access tokens)
授权码许适用于第三方应用,密码许可适用于自己的内部应用,但如果你用户想创建一组 token 仅供他自己使用(无论是测试你的 API 或者开发他的私人应用),这时就需要个人访问令牌大显身手了。
创建个人访问令牌 要创建个人访问令牌,你需要在数据库中添加一个个人访问令牌客户端。运行
php artisan passport:install时,数据库已经添加了一个了,但是如果你要重新添加的话, 运行php artisan passport:client --personal。
php artisan passport:client --personal
What should we name the personal access client? [My Application Personal Access Client]:
> My Application Personal Access Client
Personal access client Created successfully.
准确来说,个人访问令牌并不属于任何许可类型,它并不是 OAuth2.0 定义的四种模式之一(也没有 oauth 规定的流程)。相反,它是 Passport 提供的一种便捷方法,为你的用户(同时也是开发者),使他可以轻松的在您的系统中注册的单个客户端,从而方便他创建令牌。
例如,你(Tweeter)有一个用户正在开发一个 SpaceBook(使用Tweeter 授权码授权开发) 的竞品,叫做 RaceBook(为马拉松运动员),他们想先把玩一下 Tweeter API,在开发之前,弄清楚它是怎么工作的。他们需要用授权码的流程来创建 token 吗? 当然不,他们甚至都还没有写任何代码,这就是个人访问令牌的用处。
你可以是使用 JSON API 创建个人访问令牌,我们一会说,当然,你也可是在代码中直接为你的用户创建。
// 不含 scopes
$token = $user->createToken('Token Name')->accessToken;
// 含 scopes
$token = $user->createToken('My Token', ['place-order'])->accessToken;
你的用户可以使用这些 token , 就像使用授权码的流程创建的 token 一样。 scope 稍后讲到。
来自 Laravel 会话认证的令牌(同步令牌)
用户可以通过最后一种方式获取令牌来访问您的 API ,这是 Passport 添加的另一种便捷方法,但普通 OAuth 服务不提供这种方法。 此方法适用于您的用户已经过身份验证的情况,因为他们已正常登录您的 Laravel 应用,并且您希望应用可以通过 JavaScript 脚本访问 API 。如果再通过授权码或者密码授权的方式来重新认证用户,那就太痛苦了,所以 laravel 提供了同步令牌。
如果你将 Laravel\Passport\Http\Middleware\CreateFreshApiToken 中间件添加到你的 web 中间件组(app/Http/Kernel.php)中,则 Laravel 发送给已认证用户的每个响应都会附加一个名为 laravel_token 的 cookie。 这个 cookie 就是 JSON Web Token (JWT),其中包含有关 CSRF 令牌的编码信息。现在,无论是用 JavaScript 附带 X-CSRF-TOKEN 标头的请求发送正常的 CSRF token, 还是发送附带 X-Requested-With 标头的其他 API 请求,API 会将您的 CSRF 令牌与此 cookie 进行比较,就像其他令牌一样,这将使您的用户通过API进行身份验证。
JSON Web Tokens (JWT) JWT 是一种开放的行业标准 RFC 7519 方法,用于在双方之间安全地传输信息。也是目下流行的跨域解决方案。一个 JSON Web Token 就是一个 JSON 对象, 包含着确定用户认证状态和连接权限的所有必要信息。 这个 JSON 使用 HMAC ( keyed-hash message authentication code ) 或者 RSA 数字签名,使其值得信赖。 这个令牌通常是通过
urlPOST请求或者header被编码和交付。一旦,用户通过某种方式通过了系统认证,每一个HTTP请求就会包含 token , 其用于描述用户的身份和授权。 JSON Web 令牌由三个 Base64 编码字符串组成,这些字符串由点 (.) 分隔。类似x.y.z之类。第一部分是 Base64 编码的 JSON 对象,其中包含使用哪个哈希算法; 第二部分是关于用户授权和身份的一系列”声明”;第三部分是签名,或者使用第一节中指定的算法加密和签名的第一部分和第二部分。 更多 JWT.IO 和 jwt-auth Laravel package
Laravel 附带的默认 JavaScript 引导已经设置捆绑了此标头,但如果使用不同的框架,则需要手动设置它。以下演示如何使用 jQuery 进行。
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': "{{ csrf_token() }}",
'X-Requested-With': 'XMLHttpRequest'
}
});
如果你将 CreateFreshApiToken 中间件添加到Web中间件组中,并且在每个JavaScript请求中传递这些标头,那么你的JavaScript请求将能够访问受 Passport 保护的 API 路由,而没有授权码许可或密码许可的复杂。
使用 Passport API 和 Vue 组件管理客户端和令牌
我们已经知道了如何手动创建客户端和令牌,作为使用者如何进行授权,现在让我们来看一下 Passport API 的其他方面,它允许用户建立界面来管理他们的客户端和tokens。
路由
深入了解 API 路由的最简单方法是看一下 Vue 组件提供的示例是怎么工作的以及它们所依赖的路由,因此,我仅作简要概述。
/oauth/clients (GET, POST)
/oauth/clients/{id} (DELETE, PUT)
/oauth/personal-access-tokens (GET, POST)
/oauth/personal-access-tokens/{id} (DELETE)
/oauth/scopes (GET)
/oauth/tokens (GET)
/oauth/tokens/{id} (DELETE)
如您所见,我们在这里有几个实体(客户端,个人访问令牌,作用域和令牌)。 我们列出了所有这些; 我们可以创建(你不能创建作用域,因为它们是在代码中定义的,你不能创建令牌,因为它们是在授权流程中创建的); 我们也可以删除和更新(PUT)。
Vue 组件
Passport 附带了一系列开箱即用的 Vue 组件,使得用户可以方便的管理他们的客户端(他们创建的),授权客户端(允许访问他们的账户)以及个人访问令牌(用于他们自己的测试目的)。
要将这些组件发布到应用程序中,请运行以下命令
php artisan vendor:publish --tag=passport-compontents
在 resources/js/components/passport 中你将拥有 3 个新的 Vue 组件,要将它们添加到您的 Vue 引导,以便可以在您的模板中访问它们,请将它们注册到您的 resources/js/app.js 文件中。如下:
// Importing Passport’s Vue components into app.js
require('./bootstrap');
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')
);
const app = new Vue({
el: '#app'
});
现在你拥有了三个组件,你可以在任何地方使用他们:
<passport-clients></passport-clients>
<passport-authorized-clients></passport-authorized-clients>
<passport-personal-access-tokens></passport-personal-access-tokens>
Tweeter 是你开发的项目, SpaceBook 是基于 Tweeter 授权的。 RaceBook 是使用个人授权的类似 SpaceBook 的网站。
<passport-clients> 显示用户创建的所有客户端。这意味着当他们登录到 tweeter 时,Spacebook 的创建者将看到这里列出的 Spacebook 客户端。
<passport-authorized-clients> 显示所有授权过的客户端。这意味着,SpaceBook 用户 和 同时给 SpaceBook 账户连接权限的 Tweeter 用户, 都会在这里看到 SpaceBook 。
<passport-personal-access-tokens> 显示所有已经个人授权的用户。 例如, RaceBook 的创建者。
如果你新安装 Laravel 想测试这些,这有一些步骤。
- 安装 Passport
命令行执行以下命令
php artisan vendor:publish --tag=passport-components npm install npm run dev php artisan make:auth打开
resources/views/home.blade.php, 添加你需要的 Vue 组件 (eg: ) 放到<div class="card-body">.
如果需要,你可以直接原封不动的使用这些组件. 当然你也可以使用这些作为参考,了解和学习如何使用这些 API ,然后创建你需要的任何样式的前端组件。
Passport Scopes
如果,你熟悉 OAuth,你可能注意到,我们还没有提到 scopes 。截至目前为止,我们所有提到的都可以使用 scope 做限制, 这里,我们先快速看一下什么是 scope。
在 OAuth 中, scopes 被定义成特权的集合,是可以做一些是而不是什么都可以做。如果你之前使用过 GitHub 的 API token,你会发现,一些应用只想获取你的 名字 和 email ,一些想获取你的所有仓库(repos),一些想获取你的 gists。这些都是一个 “scope”,它允许用户和第三方应用程序 定义 应用程序执行其工作所需的访问权限。
你可以在 AuthServiceProvider 的 boot() 方法中定义 scopes 。
// AuthServiceProvider
use Laravel\Passport\Passport;
public function boot()
{
// ...
Passport::tokensCan([
'list-clips' => 'list sound clips',
'add-delete-clips' => 'Add new and delete old sound clips',
'admin-account' => 'Administer account details',
]);
}
一旦你定义了 scopes , 第三方应用就可以定义它请求访问的作用域。只需在初始重定向的参数列表里添加一个 scope 字段,其内容是用空格分隔 scope 名。
// SpaceBook's routes/web.php 授权码模式
Route::get('tweeter/redirect', function(){
$query = http_build_query([
'client_id' => 'tweeter.id'
'redirect_uri' => url('tweeter/callback'),
'response_type' => 'code',
'scope' => 'list-clips add-delete-clip',
]);
return redirect('http://tweeter.test/oauth/authorize?' . $query);
});
当用户使用此应用进行授权时,它将显示请求的作用域列表。这样用户就会知道, “SpaceBook 正在请求查看你的昵称” 还是 “SpaceBook 正在请求获取你的手机号” 等。
你可以使用中间件或者使用 user 实例进行 scope 范围检查。
// 使用 user 实例 检查 进行身份验证的令牌是否可以执行特定操作
Route::get('/events', function () {
if (auth()->user()->tokenCan('add-delete-clips')) {
//
}
});
有两个中间件,可以用于 scope 检查。使用方法,添加他们到 app/Http/Kernel.php 文件的 $routeMiddleware 上:
'scopes' => \Laravel\Passport\Http\Middleware\CheckScopes::class,
'scope' => \Laravel\Passport\Http\Middleware\CheckForAnyScope::class,
现在你可以使用中间件了, scopes 需要同时满足所有指定的 scope , 即 and 关系 (Scope-A && Scope-B); scope 至少满足一个即可,即 or 关系 (Scope-A || Scope-B) 。
// routes/api.php
Route::get('clips', function () {
// token 范围必须同时满足 "list-clips" and "add-delete-clips" scopes
})->middleware('scopes:list-clips,add-delete-clips');
// or
Route::get('clips', function () {
// token 范围至少满足 list-clips 或者 add-delete-clips 的一个
})->middleware('scope:list-clips,add-delete-clips')
如果你没定义任何 scope, scope 就像不存在一样,你的应用可以正常工作。一旦你使用了 scope ,你的第三方应用就需要明确指出他们请求访问的范围。只有一个例外,如果您使用的是密码许可类型,那么您的第三方应用可以请求 * scope,从而使令牌可以访问所有内容。
部署 Passport
在 应用程序生成 keys 之前, passport 是不会生效的,你可以运行 phpa artisan passport:keys 来生成 key 。
API Token Authentication
Laravel 提供了一种简单的API令牌认证机制。它与用户名和密码没什么不同:为每个用户分配了一个令牌,客户端可以将令牌与请求一起传递给该用户,以对该用户进行身份验证。
这种授权机制没有 OAuth 2.0 安全,使用前,务必明白风险。因为只有一个 token 字符串,它就像密码,它有整个系统的连接权限。当然,相比密码,它更安全一点,因为你可以强制设置 token 难以被猜到,或者当你嗅到丝毫违规问题时可以擦除或重置 token , 对于密码这些都是做不到的。
所以,token API authentication 可能不适合你的应用,但是如果它适合的话,实现起来是十分简单的。
首先, users 表添加一个 60 个字符长度的字段 api_token, 并设置唯一索引。
$table->string('api_token', 60)->unique();
其次,更新创建新用户的方法,并确保它为每个新用户为此字段设置一个值,Laravel 有一个生成随机字符串的助手方法, 设置该字段的值为 str_random(60) 即可 。如果你是对运行中的系统添加 API Token 授权,你还需要为已经存在于数据库的用户执行操作(即添加 api_token 的值) 。
要使用这种身份验证方法包装路由,使用 auth:api 中间件即可:
Route::prefix('api')->middleware('auth:api')->group(function () {
//
});
请注意,由于您使用的是标准安全防护之外的身份验证安全防护,因此您在使用任何 auth() 方法时都需要指定该安全防护:
$user = auth()->guard('api')->user();
自定义 404 响应
Laravel 为普通 HTML 视图提供了可自定义的错误消息页面,你也可以为 JSON 格式的响应定义自己的默认的回退响应。 如下:
// routes/api.php
Route::fallback(function () {
return response()->json(['message' => 'Route Not Found'], 404);
})->name('api.fallback.404');
触发回退路由
如果您要自定义 Laravel 捕获“未找到”异常时返回的路由,您可以使用 reactWithRoute() 方法更新异常处理程序:
// App\Exceptions\Handler
public function render($request, Exception $exception)
{
if ($exception instanceof ModelNotFoundException && $request->isJson()) {
return Route::respondWithRoute('api.fallback.404');
}
return parent::render($request, $exception);
}
