简介
Laravel 通过传统的登录表单已经让用户认证变得很简单,但是 API 认证怎么实现?API 通常使用令牌(token)进行认证并且在请求之间不维护会话(Session)状态。Laravel 官方扩展包 Laravel Passport 让 API 认证变得轻而易举,Passport 基于 Alex Bilbie 维护的 League OAuth2 server,可以在数分钟内为 Laravel 应用提供完整的 OAuth2 服务器实现。
注:该文档建立在你已经很熟悉 OAuth2 的基础之上,如果你还不知道什么是 OAuth2,请先阅读官方文档,或者下面学院君的介绍。
OAuth2 概述
正式开始之前我们先简单了解下 OAuth2。
什么是 OAuth 协议
OAuth 是 Open Authorization 的简写,OAuth 协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是 OAuth 的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此 OAuth 是安全的。
OAuth 本身不存在一个标准的实现,后端开发者自己根据实际的需求和标准的规定实现。其步骤一般如下:
- 客户端要求用户给予授权
- 用户同意给予授权
- 根据上一步获得的授权,向认证服务器请求令牌(token)
- 认证服务器对授权进行认证,确认无误后发放令牌
- 客户端使用令牌向资源服务器请求资源
- 资源服务器使用令牌向认证服务器确认令牌的正确性,确认无误后提供资源
OAuth2 解决什么问题
任何身份认证,本质上都是基于对请求方的不信任所产生的。同时,请求方是信任被请求方的,例如用户请求服务时,会信任服务方。所以,身份认证就是为了解决身份的可信任问题。
在 OAuth 中,简单来说有三方:用户(这里是指属于服务方的用户)、服务方、第三方应用(客户端)。
服务方不信任用户,所以需要用户提供密码或其他可信凭据;
服务方不信任第三方,所以需要第三方提供自已交给它的凭据(通常的一些安全签名之类的就是);
用户部分信任第三方,所以用户愿意把自已在服务方里的某些服务交给第三方使用,但不愿意把自已在服务方的密码交给第三方;
在 OAuth 的流程中,用户登录了第三方的系统后,会先跳去服务方获取一次性用户授权凭据,再跳回来把它交给第三方,第三方的服务器会把授权凭据以及服务方给它的的身份凭据一起交给服务方,这样,服务方一可以确定第三方得到了用户对此次服务的授权(根据用户授权凭据),二可以确定第三方的身份是可以信任的(根据身份凭据),所以,最终的结果就是,第三方顺利地从服务方获取到了此次所请求的服务。
从上面的流程中可以看出,OAuth 完整地解决掉了用户、服务方、第三方 在某次服务时这三者之间的信任问题。
OAuth 基本流程
涉及成员:
- Resource Owner(资源拥有者:用户)
- Client (第三方接入平台:请求者)
- Resource Server (服务器资源:数据中心)
- Authorization Server (认证服务器)
注:Passport 需要你对 OAuth2 非常熟悉才能自如使用,上面关于 OAuth2 的概述转自理解 OAuth2.0 认证一文,更多关于 OAuth2 模式的探讨,还可以参考阮一峰博客:理解OAuth 2.0)。
升级 Passport
升级 Passport 到新的主版本时,请务必仔细阅读升级指南,这很重要,目前 Passport 最新版是 10.x 版本,要求 PHP 最低版本是 7.3,Laravel 最低版本是 8.0。
安装
首先通过 Composer 包管理器安装 Passport:
1composer require laravel/passport
Passport 服务提供者为框架注册了自己的数据库迁移目录,所以在安装完扩展包之后需要迁移数据库,Passport 迁移将会为应用生成用于存放客户端和访问令牌的数据表:
1php artisan migrate
接下来,需要运行 passport:install
命令,该命令将会创建生成安全访问令牌所需的加密键,此外,该命令还会创建「personal access」和「password grant」客户端用于生成访问令牌:
1php artisan passport:install
注:如果你想要使用 UUID 而不是自增整型数值作为 Passport
Client
模型的主键值,安装 Passport 的时候请使用 uuids 选项。
生成记录存放在数据表 oauth_clients
中:
运行完 passport:install
命令后,添加 Laravel\Passport\HasApiTokens
Trait 到 App\Models\User
模型类,该 Trait 将会为模型类提供一些辅助函数用于检查认证用户的令牌和作用域:
1<?php
2
3namespace App\Models;
4
5use Laravel\Passport\HasApiTokens;
6use Illuminate\Notifications\Notifiable;
7use Illuminate\Foundation\Auth\User as Authenticatable;
8
9class User extends Authenticatable
10{
11 use HasApiTokens, Notifiable;
12}
接下来,你需要在 AuthServiceProvider
的 boot
方法中调用 Passport::routes
方法,该方法将会为颁发访问令牌、撤销访问令牌、客户端以及私人访问令牌功能注册所需的路由:
1<?php
2
3namespace App\Providers;
4
5use Laravel\Passport\Passport;
6use Illuminate\Support\Facades\Gate;
7use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
8
9class AuthServiceProvider extends ServiceProvider
10{
11 /**
12 * The policy mappings for the application.
13 *
14 * @var array
15 */
16 protected $policies = [
17 ‘App\Models\Model’ => ‘App\Policies\ModelPolicy’,
18 ];
19
20 /**
21 * Register any authentication / authorization services.
22 *
23 * @return void
24 */
25 public function boot()
26 {
27 $this->registerPolicies();
28
29 Passport::routes();
30 }
31}
最后,在配置文件 config/auth.php
中,需要设置 api
认证守卫的 driver
选项为 passport
。这将告知 Laravel 应用在认证 API 请求时使用 Passport 的 TokenGuard
:
1’guards’ => [
2 ‘web’ => [
3 ‘driver’ => ‘session’,
4 ‘provider’ => ‘users’,
5 ],
6
7 ‘api’ => [
8 ‘driver’ => ‘passport’,
9 ‘provider’ => ‘users’,
10 ‘hash’ => false,
11 ],
12],
客户端 UUID
你可以在运行 passport:install
命令时使用 --uuids
选项告知 Passport 你想要使用 UUID 替代自增整型数值作为 Passport Client
模型的主键值,运行完带有 --uuids
选项的 passport:install
命令后,你将会收到有关取消 Passport 默认迁移的额外告知:
1php artisan passport:install –uuids
前端快速入门
注:如果要使用 Passport Vue 组件,前端 JavaScript 必须使用 Vue 框架,这些组件同时也使用了 Bootstrap CSS 框架。不过,即使你不使用这些工具,这些组件同样可以为你实现自己的前端组件提供有价值的参考。
Passport 附带了 JSON API 以便用户创建客户端和私人访问令牌(access token)。不过,考虑到编写前端代码与这些 API 交互是一件很花费时间的事,Passport 还预置了 Vue 组件作为示例以供使用(或者作为自己实现的参考)。
要发布 Passport Vue 组件,可以使用 vendor:publish
命令:
1php artisan vendor:publish –tag=passport-components
发布后的组件位于 resources/js/components
目录下,组件发布之后,还需要将它们注册到 resources/js/app.js
文件:
1Vue.component(
2 ‘passport-clients’,
3 require(‘./components/passport/Clients.vue’).default
4);
5
6Vue.component(
7 ‘passport-authorized-clients’,
8 require(‘./components/passport/AuthorizedClients.vue’).default
9);
10
11Vue.component(
12 ‘passport-personal-access-tokens’,
13 require(‘./components/passport/PersonalAccessTokens.vue’).default
14);
注:在 Laravel 5.7.19 之前的版本,在注册组件时附加
.default
后缀会导致控制台错误。有关此次更改的说明,请参考 Laravel Mix v4.0.0 版本发行说明。
注册完组件后,确保运行 npm run dev
来重新编译前端资源。重新编译前端资源后,就可以将这些组件放到应用的某个模板中以便创建客户端和私人访问令牌:
1<passport-clients></passport-clients>
2<passport-authorized-clients></passport-authorized-clients>
3<passport-personal-access-tokens></passport-personal-access-tokens>
部署 Passport
第一次部署 Passport 到生产服务器时,可能需要运行 passport:keys
命令。这个命令生成 Passport 需要的密钥以便生成访问令牌,生成的密钥将不会存放在源代码控制中:
1php artisan passport:keys
如果必要的话,可以定义 Passport 密钥的加载路径,这可以通过使用 Passport::loadKeysFrom
方法来实现:
1/**
2 * Register any authentication / authorization services.
3 *
4 * @return void
5 */
6public function boot()
7{
8 $this->registerPolicies();
9
10 Passport::routes();
11
12 Passport::loadKeysFrom(‘/secret-keys/oauth’);
13}
此外,你还可以使用 php artisan vendor:publish --tag=passport-config
发布 Passport 的配置文件,它将会从环境变量中加载加密密钥选项:
1PASSPORT_PRIVATE_KEY=”—–BEGIN RSA PRIVATE KEY—–
2<private key here>
3—–END RSA PRIVATE KEY—–“
4
5PASSPORT_PUBLIC_KEY=”—–BEGIN PUBLIC KEY—–
6<public key here>
7—–END PUBLIC KEY—–“
自定义迁移
如果你不想使用 Passport 的默认迁移,需要在 AppServiceProvider
的 register
方法中调用 Passport::ignoreMigrations
方法。你可以使用 php artisan vendor:publish --tag=passport-migrations
导出默认迁移。
配置
客户端密钥哈希
如果你想要客户端的密钥经过哈希运算后再存储到数据库,需要在 AppServiceProvider
的 boot
方法中调用 Passport::hashClientSecrets
方法:
1Passport::hashClientSecrets();
启用该功能后,所有客户端密钥只会在新的客户端被创建时显示一次,之后就会经过哈希运算保存到数据库,而哈希运算是不可逆的,也就意味着再也无法获取到该密钥的原始值了。
令牌生命周期
默认情况下,Passport 颁发的访问令牌(Access Token)是长期有效的,如果你想要配置生命周期短一点的令牌,可以使用 tokensExpireIn
、refreshTokensExpireIn
和 personalAccessTokensExpireIn
方法,这些方法需要在 AuthServiceProvider
的 boot
方法中调用:
1/**
2 * 注册任意认证/授权服务
3 *
4 * @return void
5 */
6public function boot()
7{
8 $this->registerPolicies();
9
10 Passport::routes();
11
12 Passport::tokensExpireIn(now()->addDays(15));
13
14 Passport::refreshTokensExpireIn(now()->addDays(30));
15
16 Passport::personalAccessTokensExpireIn(now()->addMonths(6));
17}
注:Passport 数据表的
expires_at
字段是只读的,只能用于显示。颁发访问令牌时,Passport 会将经过签名和加密的令牌过期信息保存起来,如果你需要让一个令牌失效,需要执行撤销操作。
覆盖默认模型
你可以按需扩展 Passport 底层使用的方法:
1use Laravel\Passport\Client as PassportClient;
2
3class Client extends PassportClient
4{
5 // …
6}
然后,通过 Passport
类告知 Passport 使用你自定义的方法:
1use App\Models\Passport\Client;
2use App\Models\Passport\Token;
3use App\Models\Passport\AuthCode;
4use App\Models\Passport\PersonalAccessClient;
5
6/**
7 * Register any authentication / authorization services.
8 *
9 * @return void
10 */
11public function boot()
12{
13 $this->registerPolicies();
14
15 Passport::routes();
16
17 Passport::useTokenModel(Token::class);
18 Passport::useClientModel(Client::class);
19 Passport::useAuthCodeModel(AuthCode::class);
20
21Passport::usePersonalAccessClientModel(PersonalAccessClient::class);
22}
颁发访问令牌
通过授权码使用 OAuth2 是大多数开发者熟悉的方式。使用授权码的时候,客户端应用会将用户重定向到你的服务器,服务器可以通过或拒绝颁发访问令牌到客户端的请求。
管理客户端
约定:我们参考 OAuth2 认证流程,这里将 Laravel 应用约定为服务方,开发者开发应用作为第三方客户端。
首先,开发者构建和 Laravel 应用 API 交互的应用时,需要通过创建一个“客户端”将他们的应用注册到 Laravel 应用。通常,这包括提供应用的名称以及用户授权请求通过后重定向到的 URL。(想想你是怎么使用微博、微信、QQ第三方登录API的,就明白这里的流程了)
passport:client
命令
创建客户端最简单的方式就是使用 Artisan 命令 passport:client
,该命令可用于创建你自己的客户端以方便测试 OAuth2 功能。当你运行 client
命令时,Passport 会提示你输入更多关于客户端的信息,并且为你生成 client ID 和 secret:
1php artisan passport:client
新生成记录存放在 oauth_clients
数据表中:
重定向 URL
如果你想要将客户端的多个重定向网址列入白名单,可以在 passport:client
命令交互提示中使用一个通过逗号分隔的列表来指定它们:
1http://example.com/callback,http://examplefoo.com/callback
注:任何包含逗号的 URL 都需要被编码。
JSON API
由于第三方应用开发者不能直接使用 Laravel 服务端提供的 client
命令,为此,Passport 提供了一个 JSON API 用于创建客户端,这省去了你手动编写控制器用于创建、更新以及删除客户端的麻烦。
不过,你需要配对 Passport 的 JSON API 和自己的前端以便为第三方开发者提供一个可以管理他们自己客户端的后台,下面,我们来概览下所有用于管理客户端的 API,为了方便起见,我们将会使用Axios 来演示发送 HTTP 请求到这些 API。
JSON API 应用了 web
和 auth
中间,因此,只能在自己的应用中调用,不能从外部访问。
注:如果你不想要自己实现整个客户端管理前端,可以使用前端快速上手教程在数分钟内搭建拥有完整功能的前端。
GET /oauth/clients
这个路由为认证用户返回所有客户端,这在展示用户客户端列表时很有用,可以让用户很容易编辑或删除客户端:
1axios.get(‘/oauth/clients’)
2 .then(response => {
3 console.log(response.data);
4 });
POST /oauth/clients
这个路由用于创建新的客户端,要求传入两个数据:客户端的 name
和 redirect
URL, redirect
URL 是用户授权请求通过或拒绝后重定向到的位置。
当客户端被创建后,会附带一个 client ID 和 secret,这两个值会在请求访问令牌时用到。客户端创建路由会返回新的客户端实例:
1const data = {
2 name: ‘Client Name’,
3 redirect: ‘http://example.com/callback’
4};
5
6axios.post(‘/oauth/clients’, data)
7 .then(response => {
8 console.log(response.data);
9 })
10 .catch (response => {
11 // List errors on response…
12 });
PUT /oauth/clients/{client-id}
这个路由用于更新客户端,要求传入两个参数:客户端的 name
和 redirect
URL。 redirect
URL 是用户授权请求通过或拒绝后重定向到的位置。该路由将会返回更新后的客户端实例:
1const data = {
2 name: ‘New Client Name’,
3 redirect: ‘http://example.com/callback’
4};
5
6axios.put(‘/oauth/clients/’ + clientId, data)
7 .then(response => {
8 console.log(response.data);
9 })
10 .catch (response => {
11 // List errors on response…
12 });
DELETE /oauth/clients/{client-id}
这个路由用于删除客户端:
1axios.delete(‘/oauth/clients/’ + clientId)
2 .then(response => {
3 //
4 });
请求令牌
授权重定向
客户端被创建后,开发者就可以使用相应的 client ID 和 secret 从应用请求授权码和访问令牌。首先,客户端应用要生成一个重定向请求到服务端应用的 /oauth/authorize
路由:
1use Illuminate\Http\Request;
2use Illuminate\Support\Str;
3
4Route::get(‘/redirect’, function (Request $request) {
5 $request->session()->put(‘state’, $state = Str::random(40));
6
7 $query = http_build_query([
8 ‘client_id’ => ‘client-id’,
9 ‘redirect_uri’ => ‘http://example.com/callback’,
10 ‘response_type’ => ‘code’,
11 ‘scope’ => ”,
12 ‘state’ => $state,
13 ]);
14
15 return redirect(‘http://your-app.com/oauth/authorize?’.$query);
16});
其中 example.com
对应客户端应用域名,your-app.com
对应服务端应用域名。
注:
/oauth/authorize
路由已经通过Passport::routes
方法定义了,不需要手动定义这个路由。
学院君注:目前 Laravel 8.0 新引入的 UI 库 Jetstream 尚未集成对 Passport 的支持,要体验完整的 Passport API 认证流程,需要安装 Laravel UI 库,相应的流程参考 Laravel 7 Passport 文档,这里不再重复演示了。
通过授权请求
接收授权请求的时候,Passport 会自动显示一个视图模板给用户从而允许他们通过或拒绝授权请求(如上图所示),如果用户通过请求,就会被重定向回第三方应用指定的 redirect_uri
,这个 redirect_uri
必须和客户端创建时指定的 redirect
URL 一致。
如果你想要自定义授权通过界面,可以使用 Artisan 命令 vendor:publish
发布 Passport 的视图模板,发布的视图位于 resources/views/vendor/passport
:
1php artisan vendor:publish –tag=passport-views
有时候你可能希望跳过授权提示页面,比如像我们这个示例这样,认证请求发起方和第三方授权通过方位于同一个应用中,这可以通过在客户端模型中定义 skipsAuthorization
方法来完成。如果 skipsAuthorization
方法返回 true
则客户端将会被授权通过,用户将直接被重定向到 redirect_uri
指定的页面:
1<?php
2
3namespace App\Models\Passport;
4
5use Laravel\Passport\Client as BaseClient;
6
7class Client extends BaseClient
8{
9 /**
10 * Determine if the client should skip the authorization prompt.
11 *
12 * @return bool
13 */
14 public function skipsAuthorization()
15 {
16 return $this->firstParty();
17 }
18}
将授权码转化为访问令牌
如果用户通过了授权请求,会被重定向回客户端应用。客户端应用首先会验证之前传递给授权服务方的 state
参数,如果该参数与之前传递的参数值匹配,则客户端会发起一个 POST
请求到服务端来请求一个访问令牌。这个请求应该包含用户通过授权请求时指定的授权码。在这个例子中,我们会使用 Guzzle HTTP 库来生成 POST
请求:
1Route::get(‘/callback’, function (Request $request) {
2 $state = $request->session()->pull(‘state’);
3
4 throw_unless(
5 strlen($state) > 0 && $state === $request->state,
6 InvalidArgumentException::class
7 );
8
9 $http = new GuzzleHttp\Client;
10
11 $response = $http->post(‘http://blog.test/oauth/token’, [
12 ‘form_params’ => [
13 ‘grant_type’ => ‘authorization_code’,
14 ‘client_id’ => ‘client-id’, // your client id
15 ‘client_secret’ => ‘client-secret’, // your client secret
16 ‘redirect_uri’ => ‘http://blog.test/auth/callback’,
17 ‘code’ => $request->code,
18 ],
19 ]);
20
21 return json_decode((string) $response->getBody(), true);
22});
/oauth/token
路由会返回一个包含 token_type
、access_token
、 refresh_token
和 expires_in
属性的 JSON 响应。expires_in
属性包含访问令牌的过期时间(s)。
注:和
/oauth/authorize
路由一样,/oauth/token
路由已经通过Passport::routes
方法定义过了,不需要手动定义这个路由。默认情况下,该路由使用ThrottleRequests
中间件设置对访问频率进行限制。
拿到有效的 access_token
就可以通过它去服务端获取其他需要的资源信息了。至此就完成了通过授权码方式实现 API 认证的流程。实际开发中,99% 的 API 认证都是通过这种方式实现的。
JSON API
Passport 还内置了 JSON API 来管理授权访问令牌,你可以将其与自己的前端代码整合为用户提供访问令牌管理后台。为了方便起见,我们使用 Axios 来演示对 API 发起 HTTP 请求。这个 JSON API 应用了 web
和 auth
中间件,因此,只能从自己的应用中访问。
GET /oauth/tokens
这个路由会返回认证用户创建的所有授权访问令牌,这在列举该用户的所有令牌以便撤销时很有用:
1axios.get(‘/oauth/tokens’)
2 .then(response => {
3 console.log(response.data);
4 });
DELETE /oauth/tokens/{token-id}
这个路由可用于撤销授权访问令牌及其相关的刷新令牌:
1axios.delete(‘/oauth/tokens/’ + tokenId);
刷新令牌
如果应用颁发的是短期有效的访问令牌,那么用户需要通过访问令牌颁发时提供的 refresh_token
刷新访问令牌,在本例中,我们使用 Guzzle HTTP 库来刷新令牌:
1$http = new GuzzleHttp\Client;
2
3$response = $http->post(‘http://blog.test/oauth/token’, [
4 ‘form_params’ => [
5 ‘grant_type’ => ‘refresh_token’,
6 ‘refresh_token’ => ‘the-refresh-token’,
7 ‘client_id’ => ‘client-id’,
8 ‘client_secret’ => ‘client-secret’,
9 ‘scope’ => ”,
10 ],
11]);
12
13return json_decode((string) $response->getBody(), true);
/oauth/token
路由会返回一个包含 access_token
、 refresh_token
和 expires_in
属性的 JSON 响应,同样, expires_in
属性包含访问令牌过期时间(s)。
撤销令牌
你可以使用 TokenRepository
提供的 revokeAccessToken
方法来撤销一个访问令牌(让其失效),然后使用 RefreshTokenRepository
提供的 revokeRefreshTokensByAccessTokenId
方法来撤销某个令牌的刷新令牌:
1$tokenRepository = app(‘Laravel\Passport\TokenRepository’);
2$refreshTokenRepository = app(‘Laravel\Passport\RefreshTokenRepository’);
3
4// 撤销一个访问令牌…
5$tokenRepository->revokeAccessToken($tokenId);
6
7// 撤销该访问令牌的所有刷新令牌…
8$refreshTokenRepository->revokeRefreshTokensByAccessTokenId($tokenId);
清除令牌
当令牌已撤销或者已过期,你可能想要从数据库中清除它们。Passport 通过提供以下命令帮你处理令牌的清除:
1# 清除已撤销或已过期令牌以及认证码…
2php artisan passport:purge
3
4# 只清除已撤销令牌和认证码…
5php artisan passport:purge –revoked
6
7# 只清除已过期令牌和认证码…
8php artisan passport:purge –expired
你还可以在控制台 Kernel
类中配置调度任务来定时自动清理令牌:
1/**
2 * Define the application’s command schedule.
3 *
4 * @param \Illuminate\Console\Scheduling\Schedule $schedule
5 * @return void
6 */
7protected function schedule(Schedule $schedule)
8{
9 $schedule->command(‘passport:purge’)->hourly();
10}
通过 PKCE 颁发授权码
通过「Proof Key for Code Exchange(PKCE,中文可译作保护授权码授权)」颁发授权码是一个对单页面应用或者原生应用进行认证以便访问 API 接口的安全方式。这种颁发方式的适用场景是当你不能保证客户端密钥被安全存储,或者为了降低攻击者拦截授权码的威胁。在这种模式下,当通过授权码获取访问令牌时,「验证码」和「质询码」的组合将替换客户端密钥。
创建客户端
在应用可以通过基于 PKCE 颁发的授权码颁发令牌前,你需要创建一个启用 PCKE 的客户端,这可以通过调用带有 --public
选项的 passport:client
命令来完成:
1php artisan passport:client –public
请求令牌
验证码 & 授权码
由于这个认证授权没有提供客户端密钥,所以开发者需要生成一个代码验证和代码质询的组合以便请求令牌。
验证码是一个长度介于 43 和 128 之间的、包含字母、数字、以及 -
、_
、~
字符的随机字符串(遵循 RFC 7636 标准)。
质询码是一个包含 URL 和文件名安全字符的 Base64 加密字符串。末尾的 =
应该被移除,也不能出现换行符、空白字符、或者其他额外字符。
1$encoded = base64_encode(hash(‘sha256’, $code_verifier, true));
2
3$codeChallenge = strtr(rtrim($encoded, ‘=’), ‘+/’, ‘-_’);
认证重定向
客户端被创建之后,可以在应用中使用客户端 ID 和生成的验证码、质询码来请求授权码和访问令牌。首先,客户端应用应该发起一个指向服务端 oauth/authorize
路由的重定向请求:
1Route::get(‘/redirect’, function (Request $request) {
2 $request->session()->put(‘state’, $state = Str::random(40));
3
4 $request->session()->put(‘code_verifier’, $code_verifier = Str::random(128));
5
6 $codeChallenge = strtr(rtrim(
7 base64_encode(hash(‘sha256’, $code_verifier, true))
8 , ‘=’), ‘+/’, ‘-_’);
9
10 $query = http_build_query([
11 ‘client_id’ => ‘client-id’,
12 ‘redirect_uri’ => ‘http://example.com/callback’,
13 ‘response_type’ => ‘code’,
14 ‘scope’ => ”,
15 ‘state’ => $state,
16 ‘code_challenge’ => $codeChallenge,
17 ‘code_challenge_method’ => ‘S256’,
18 ]);
19
20 return redirect(‘http://your-app.com/oauth/authorize?’.$query);
21});
将授权码转化为访问令牌
用户接收授权请求后,就会被重定向回客户端应用,客户端需要验证 state
参数与重定向到服务端应用前保存的值是否相等,和我们在标准授权码颁发令牌所做的一样。
如果状态参数值匹配,则客户端应用会发起一个 POST
请求到服务端应用来获取访问令牌,该请求需要包含上一步颁发的授权码:
1Route::get(‘/callback’, function (Request $request) {
2 $state = $request->session()->pull(‘state’);
3
4 $codeVerifier = $request->session()->pull(‘code_verifier’);
5
6 throw_unless(
7 strlen($state) > 0 && $state === $request->state,
8 InvalidArgumentException::class
9 );
10
11 $response = (new GuzzleHttp\Client)->post(‘http://your-app.com/oauth/token’, [
12 ‘form_params’ => [
13 ‘grant_type’ => ‘authorization_code’,
14 ‘client_id’ => ‘client-id’,
15 ‘redirect_uri’ => ‘http://example.com/callback’,
16 ‘code_verifier’ => $codeVerifier,
17 ‘code’ => $request->code,
18 ],
19 ]);
20
21 return json_decode((string) $response->getBody(), true);
22});
可以看到,整体流程除了将客户端密钥替换成验证码和质询码之外,其他和通过标准授权码获取访问令牌一样。
密码授权令牌
OAuth2 密码授权允许你的其他第一方客户端,例如移动应用,使用邮箱地址/用户名+密码获取访问令牌。这使得你可以安全地颁发访问令牌给第一方客户端而不必要求你的用户走整个 OAuth2 授权码重定向流程。
创建一个密码授权客户端
在应用可以通过密码授权颁发令牌之前,需要创建一个密码授权客户端,你可以通过使用带 --password
选项的 passport:client
命令来实现。如果你已经运行了 passport:install
命令,则不必再运行这个命令:
1php artisan passport:client –password
这里我们使用一开始通过 passport:install
命令创建的记录作为测试记录。
请求令牌
创建完密码授权客户端后,可以通过发送 POST
请求到 /oauth/token
路由(带上用户邮箱地址和密码)获取访问令牌。这个路由已经通过 Passport::routes
方法注册过了,不需要手动定义。如果请求成功,就可以从服务器返回的 JSON 响应中获取 access_token
和 refresh_token
:
1Route::get(‘/auth/password’, function (\Illuminate\Http\Request $request){
2 $http = new \GuzzleHttp\Client();
3
4 $response = $http->post(‘http://your-app.com/oauth/token’, [
5 ‘form_params’ => [
6 ‘grant_type’ => ‘password’,
7 ‘client_id’ => ‘2’,
8 ‘client_secret’ => ‘8s7NsHGW2H1L03rygmaqqBNaJX2ANKzGW9HJcmAu’,
9 ‘username’ => ‘test@xueyuanjun.com’,
10 ‘password’ => ‘test123456’,
11 ‘scope’ => ”,
12 ],
13 ]);
14
15 return json_decode((string)$response->getBody(), true);
16});
注:记住,访问令牌默认长期有效,不过,如果需要的话你也可以配置访问令牌的最长生命周期。
请求所有域
使用密码授权的时候,你可能想要对应用所支持的所有域进行令牌授权,这可以通过请求 *
域来实现。如果你请求的是 *
域,则令牌实例上的 can
方法总是返回 true
,这个域只会分配给使用 password
或 client_credentials
授权的令牌:
1$response = $http->post(‘http://your-app.com/oauth/token’, [
2 ‘form_params’ => [
3 ‘grant_type’ => ‘password’,
4 ‘client_id’ => ‘client-id’,
5 ‘client_secret’ => ‘client-secret’,
6 ‘username’ => ‘test@xueyuanjun.com’,
7 ‘password’ => ‘my-password’,
8 ‘scope’ => ‘*’,
9 ],
10]);
自定义用户提供者
如果应用使用了多个认证用户提供者,可以在通过 artisan passport:client --password
命令创建客户端时提供 --provider
选项来指定密码授权客户端使用的用户提供者(user provider),给定的提供者名称需要匹配定义在 config/auth.php
配置文件中的有效提供者(providers
数组中的键名),然后就可以使用中间件保护路由来确保只有来自认证守卫指定提供者的用户才可以被授权。
自定义用户名字段
当使用密码授权进行认证的时候,Passport 会使用模型的 email
属性作为「用户名」。不过,你可以通过在模型上定义 findForPassport
方法来自定义这一默认行为:
1<?php
2
3namespace App\Models;
4
5use Laravel\Passport\HasApiTokens;
6use Illuminate\Notifications\Notifiable;
7use Illuminate\Foundation\Auth\User as Authenticatable;
8
9class User extends Authenticatable
10{
11 use HasApiTokens, Notifiable;
12
13 /**
14 * Find the user instance for the given username.
15 *
16 * @param string $username
17 * @return \App\User
18 */
19 public function findForPassport($username)
20 {
21 return $this->where(‘username’, $username)->first();
22 }
23}
自定义密码验证
当使用密码授权进行认证时,Passport 会使用模型的 password
属性来验证给定密码。如果你的模型没有 password
属性或者你希望自定义密码验证逻辑,可以在模型中定义一个 validateForPassportPasswordGrant
方法:
1<?php
2
3namespace App\Models;
4
5use Illuminate\Foundation\Auth\User as Authenticatable;
6use Illuminate\Notifications\Notifiable;
7use Illuminate\Support\Facades\Hash;
8use Laravel\Passport\HasApiTokens;
9
10class User extends Authenticatable
11{
12 use HasApiTokens, Notifiable;
13
14 /**
15 * Validate the password of the user for the Passport password grant.
16 *
17 * @param string $password
18 * @return bool
19 */
20 public function validateForPassportPasswordGrant($password)
21 {
22 return Hash::check($password, $this->password);
23 }
24}
隐式授权令牌
隐式授权和授权码授权有点相似,不过,无需获取授权码,令牌就会返回给客户端。这种授权通常应用于 JavaScript 或移动应用这些客户端凭证不能被安全存储的地方。要启用该授权,在 AuthServiceProvider
中调用 enableImplicitGrant
方法即可:
1/**
2 * Register any authentication / authorization services.
3 *
4 * @return void
5 */
6public function boot()
7{
8 $this->registerPolicies();
9
10 Passport::routes();
11
12 Passport::enableImplicitGrant();
13}
授权启用后,开发者就可以使用他们的 client ID 从应用中请求访问令牌,第三方应用需要像这样发送重定向请求到应用的 /oauth/authorize
路由:
1Route::get(‘/redirect’, function () {
2 $request->session()->put(‘state’, $state = Str::random(40));
3
4 $query = http_build_query([
5 ‘client_id’ => ‘client-id’,
6 ‘redirect_uri’ => ‘http://example.com/callback’,
7 ‘response_type’ => ‘token’,
8 ‘scope’ => ”,
9 ‘state’ => $state,
10 ]);
11
12 return redirect(‘http://your-app.com/oauth/authorize?’.$query);
13});
注:
/oauth/authorize
路由已经在Passport::routes
方法中定义过了,无需再手动定义这个路由。
客户端凭证授权令牌
客户端凭证授权适用于机器对机器的认证,例如,你可以在调度任务中使用这种授权来通过 API 执行维护任务。
在应用可以通过客户端凭证授权颁发令牌之前,需要先创建一个客户端凭证授权客户端,这可以通过在运行 passport:client
命令时添加 --client
选项来实现:
1php artisan passport:client –client
要使用这个方法,需要在 app/Http/Kernel.php
中添加新的中间件 CheckClientCredentials
到 $routeMiddleware
:
1use Laravel\Passport\Http\Middleware\CheckClientCredentials;
2
3protected $routeMiddleware = [
4 ‘client’ => CheckClientCredentials::class,
5];
然后将这个中间件应用到路由:
1Route::get(‘/orders’, function(Request $request) {
2 …
3})->middleware(‘client’);
要限定对特定路由域的访问,可以在添加 client
中间件到路由时提供一个以逗号分隔的域列表:
1Route::get(‘/orders’, function (Request $request) {
2 …
3})->middleware(‘client:check-status,your-scope’);
获取令牌
要获取令牌,发送请求到 oauth/token
:
1$guzzle = new GuzzleHttp\Client;
2
3$response = $guzzle->post(‘http://your-app.com/oauth/token’, [
4 ‘form_params’ => [
5 ‘grant_type’ => ‘client_credentials’,
6 ‘client_id’ => ‘client-id’,
7 ‘client_secret’ => ‘client-secret’,
8 ‘scope’ => ‘your-scope’,
9 ],
10]);
11
12return json_decode((string) $response->getBody(), true)[‘access_token’];
私人访问令牌
有时候,你的用户可能想要颁发访问令牌给自己而不走典型的授权码重定向流程。允许用户通过应用的 UI 颁发令牌给自己在用户体验你的 API 或者作为更简单的颁发访问令牌方式时会很有用。
注:私人访问令牌总是一直有效的,它们的生命周期在使用
tokensExpireIn
或refreshTokensExpireIn
方法时不会修改。
创建一个私人访问客户端
在你的应用可以颁发私人访问令牌之前,需要创建一个私人访问客户端。你可以通过带 --personal
选项的 passport:client
命令来实现,如果你已经运行过了 passport:install
命令,则不必再运行此命令:
1php artisan passport:client –personal
创建完私人访问客户端后,可以将客户端 ID 和纯文本密钥值存放在 .env
文件中:
1PASSPORT_PERSONAL_ACCESS_CLIENT_ID=client-id-value
2PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET=unhashed-client-secret-value
管理私人访问令牌
创建好私人访问客户端之后,就可以使用 User
模型实例上的 createToken
方法为给定用户颁发令牌。 createToken
方法接收令牌名称作为第一个参数,以及一个可选的域数组作为第二个参数:
1$user = App\Models\User::find(1);
2
3// Creating a token without scopes…
4$token = $user->createToken(‘Token Name’)->accessToken;
5
6// Creating a token with scopes…
7$token = $user->createToken(‘My Token’, [‘place-orders’])->accessToken;
JSON API
Passport 还提供了一个 JSON API 用于管理私人访问令牌,你可以将其与自己的前端配对以便为用户提供管理私人访问令牌的后台。下面,我们来概览用于管理私人访问令牌的所有 API。为了方便起见,我们使用 Axios 来演示发送 HTTP 请求到 API。
JSON API 由 web
和 auth
中间件守卫,因此,只能在自己的应用中调用,不能从外部应用调用。
注:如果你不想要实现自己的私人访问令牌前端,可以使用前端快速上手教程在数分钟内打造拥有完整功能的前端。
GET /oauth/scopes
这个路由会返回应用所定义的所有域。你可以使用这个路由来列出用户可以分配给私人访问令牌的所有域:
1axios.get(‘/oauth/scopes’)
2 .then(response => {
3 console.log(response.data);
4 });
GET /oauth/personal-access-tokens
这个路由会返回该认证用户所创建的所有私人访问令牌,这在列出用户的所有令牌以便编辑或删除时很有用:
1axios.get(‘/oauth/personal-access-tokens’)
2 .then(response => {
3 console.log(response.data);
4 });
POST /oauth/personal-access-tokens
这个路由会创建一个新的私人访问令牌,该路由要求传入两个参数:令牌的 name
和需要分配到这个令牌的 scopes
:
1const data = {
2 name: ‘Token Name’,
3 scopes: []
4};
5
6axios.post(‘/oauth/personal-access-tokens’, data)
7 .then(response => {
8 console.log(response.data.accessToken);
9 })
10 .catch (response => {
11 // List errors on response…
12 });
DELETE /oauth/personal-access-tokens/{token-id}
这个路由可以用于删除私人访问令牌:
1axios.delete(‘/oauth/personal-access-tokens/’ + tokenId);
路由保护
通过中间件
Passport 提供了一个认证守卫 用于验证输入请求的访问令牌,当你使用 passport
驱动配置好 api
guard 后,只需要在所有路由上指定需要传入有效访问令牌的 auth:api
中间件即可:
1Route::get(‘/user’, function () {
2 //
3})->middleware(‘auth:api’);
多个认证守卫
如果应用使用完全不同的 Eloquent 模型认证不同类型的用户,你可能需要为每个用户提供者类型定义一个认证守卫配置。这样一来,你就可以保护特定用户提供者的请求。举个例子,在 config/auth.php
配置文件中给定如下守卫配置:
1’guards’ => [
2 … // 其它守卫配置
3
4 ‘api’ => [
5 ‘driver’ => ‘passport’,
6 ‘provider’ => ‘users’,
7 ],
8
9 ‘api-customers’ => [
10 ‘driver’ => ‘passport’,
11 ‘provider’ => ‘customers’,
12 ],
13],
下面这个路由会使用 api-customers
守卫(该守卫使用了 customers
用户提供者)来认证输入请求:
1Route::get(‘/customer’, function () {
2 //
3})->middleware(‘auth:api-customers’);
注:想要了解更多在 Passport 中使用多个用户提供者的信息,请参考密码授权文档。
传递访问令牌
调用被 Passport 保护的路由时,应用 API 的消费者需要在请求的 Authorization
头中指定它们的访问令牌作为 Bearer
令牌。例如:
1$response = $client->request(‘GET’, ‘/api/user’, [
2 ‘headers’ => [
3 ‘Accept’ => ‘application/json’,
4 ‘Authorization’ => ‘Bearer ‘.$accessToken,
5 ],
6]);
令牌作用域
作用域(Scope)允许 API 客户端在请求账户授权的时候请求特定的权限集合。例如,如果你在构建一个电子商务应用,不是所有的 API 消费者都需要下订单的能力,取而代之地,你可以让这些消费者只请求访问订单物流状态的权限,换句话说,作用域允许你的应用用户限制第三方应用自身可以执行的操作。
定义作用域
你可以在 AuthServiceProvider
的 boot
方法中使用 Passport::tokensCan
方法定义 API 的作用域。 tokensCan
方法接收作用域名称数组和作用域描述,作用域描述可以是任何你想要在授权通过页面展示给用户的东西:
1use Laravel\Passport\Passport;
2
3Passport::tokensCan([
4 ‘place-orders’ => ‘Place orders’,
5 ‘check-status’ => ‘Check order status’,
6]);
默认作用域
如果一个客户端没有请求任何指定域,可以通过 setDefaultScope
方法配置 Passport 服务端添加一个默认的域到令牌。通常,我们在 AuthServiceProvider
的 boot
方法中调用这个方法:
1use Laravel\Passport\Passport;
2
3Passport::setDefaultScope([
4 ‘check-status’,
5 ‘place-orders’,
6]);
分配作用域到令牌
请求授权码
当使用授权码请求访问令牌时,消费者应该指定他们期望的作用域作为 scope
查询字符串参数, scope
参数是通过空格分隔的作用域列表:
1Route::get(‘/redirect’, function () {
2 $query = http_build_query([
3 ‘client_id’ => ‘client-id’,
4 ‘redirect_uri’ => ‘http://example.com/callback’,
5 ‘response_type’ => ‘code’,
6 ‘scope’ => ‘place-orders check-status’,
7 ]);
8
9 return redirect(‘http://your-app.com/oauth/authorize?’.$query);
10});
颁发私人访问令牌
如果你使用 User
模型的 createToken
方法颁发私人访问令牌,可以传递期望的作用域数组作为该方法的第二个参数:
1$token = $user->createToken(‘My Token’, [‘place-orders’])->accessToken;
检查作用域
Passport 提供了两个可用于验证输入请求是否经过已发放作用域的令牌认证的中间件。开始使用之前,添加如下中间件到 app/Http/Kernel.php
文件的 $routeMiddleware
属性:
1’scopes’ => \Laravel\Passport\Http\Middleware\CheckScopes::class,
2’scope’ => \Laravel\Passport\Http\Middleware\CheckForAnyScope::class,
检查所有作用域
scopes
中间件会分配给一个用于验证输入请求的访问令牌拥有所有列出作用域的路由:
1Route::get(‘/orders’, function () {
2 // Access token has both “check-status” and “place-orders” scopes…
3})->middleware(‘scopes:check-status,place-orders’);
检查任意作用域
scope
中间件会分配给一个用于验证输入请求的访问令牌拥有至少一个列出作用域的路由:
1Route::get(‘/orders’, function () {
2 // Access token has either “check-status” or “place-orders” scope…
3})->middleware(‘scope:check-status,place-orders’);
检查令牌实例上的作用域
当一个访问令牌认证过的请求进入应用后,你仍然可以使用经过认证的 User
实例上的 tokenCan
方法来检查这个令牌是否拥有给定作用域:
1use Illuminate\Http\Request;
2
3Route::get(‘/orders’, function (Request $request) {
4 if ($request->user()->tokenCan(‘place-orders’)) {
5 //
6 }
7});
额外的域方法
scopeIds
方法将会返回所有已定义的 ID/名字数组:
1Laravel\Passport\Passport::scopeIds();
scopes
方法将会返回所有已定义的 Laravel\Passport\Scope
域实例数组:
1Laravel\Passport\Passport::scopes();
scopesFor
方法将会返回匹配给定 ID/名字的 Laravel\Passport\Scope
实例数组:
1Laravel\Passport\Passport::scopesFor([‘place-orders’, ‘check-status’]);
你可以使用 hasScope
方法判断给定域是否被定义过:
1Laravel\Passport\Passport::hasScope(‘place-orders’);
使用 JavaScript 消费 API
构建 API 时,能够从你的 JavaScript 应用消费你自己的 API 非常有用。这种 API 开发方式允许你自己的应用消费你和其他人分享的同一个 API,这个 API 可以被你的 Web 应用消费,也可以被你的移动应用消费,还可以被第三方应用消费,以及任何你可能发布在多个包管理器上的 SDK 消费。
通常,如果你想要从你的 JavaScript 应用消费自己的 API,需要手动发送访问令牌到应用并在应用的每一个请求中传递它。不过,Passport 提供了一个中间件用于处理这一操作。你所需要做的只是在 app/Http/Kernel.php
文件中添加这个中间件 CreateFreshApiToken
到 web
中间件组:
1’web’ => [
2 // Other middleware…
3 \Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,
4],
注:你需要确保
EncryptCookies
中间件位于CreateFreshApiToken
中间件之前执行。
这个 Passport 中间件将会附加 laravel_token
Cookie 到输出响应,这个 Cookie 包含加密过的JWT,Passport 将使用这个 JWT 来认证来自 JavaScript 应用的 API 请求,现在,你可以发送请求到应用的 API,而不必显示传递访问令牌:
1axios.get(‘/api/user’)
2 .then(response => {
3 console.log(response.data);
4 });
自定义 Cookie 名称
如果需要的话,你可以使用 Passport::cookie
方法自定义 laravel_token
Cookie 的名称。通常,我们在 AuthServiceProvider
的 boot
方法中调用相应的方法:
1/**
2 * Register any authentication / authorization services.
3 *
4 * @return void
5 */
6public function boot()
7{
8 $this->registerPolicies();
9
10 Passport::routes();
11
12 Passport::cookie(‘custom_name’);
13}
CSRF 保护
使用这种认证方法时,需要确保在请求头中包含了有效的 CSRF 令牌。默认的 Laravel JavaScript 脚手架代码包含了 Axios 实例,它会自动使用加密的 XSRF-TOKEN
Cookie 值在同一个请求域下发送 X-XSRF-TOKEN
请求头。
注:如果你选择发送
X-CSRF-TOKEN
请求头而不是X-XSRF-TOKEN
,需要使用csrf_token()
提供的未加密令牌。
事件
Passport 会在颁发访问令牌和刷新令牌时触发事件,你可以使用这些事件来处理或撤销数据库中的其它访问令牌,你可以在应用的 EventServiceProvider
中添加监听器到这些事件:
1/**
2 * The event listener mappings for the application.
3 *
4 * @var array
5 */
6protected $listen = [
7 ‘Laravel\Passport\Events\AccessTokenCreated’ => [
8 ‘App\Listeners\RevokeOldTokens’,
9 ],
10
11 ‘Laravel\Passport\Events\RefreshTokenCreated’ => [
12 ‘App\Listeners\PruneOldTokens’,
13 ],
14];
测试
Passport 的 actingAs
方法可用于指定当前认证用户及其作用域,传递给 actingAs
方法的第一个参数是用户实例,第二个参数是授权给用户令牌的作用域数组:
1use App\User;
2use Laravel\Passport\Passport;
3
4public function testServerCreation()
5{
6 Passport::actingAs(
7 factory(User::class)->create(),
8 [‘create-servers’]
9 );
10
11 $response = $this->post(‘/api/create-server’);
12
13 $response->assertStatus(201);
14}
Passport 的 actingAsClient
方法可用于指定当前的认证客户端及其作用域,该方法的第一个参数是客户端实例,第二个参数是分配给客户端令牌的作用域数组:
1use Laravel\Passport\Client;
2use Laravel\Passport\Passport;
3
4public function testGetOrders()
5{
6 Passport::actingAsClient(
7 factory(Client::class)->create(),
8 [‘check-status’]
9 );
10
11 $response = $this->get(‘/api/orders’);
12
13 $response->assertStatus(200);
14}