## **简介**
在 Laravel 中实现 API 认证(通常使用令牌(token)进行认证并且在请求之间不维护会话(Session)状态)无需手动搭建 OAuth 服务,因为官方为我们提供了 Passport 扩展包,Passport 基于 Alex Bilbie 维护的[League OAuth2 server](https://github.com/thephpleague/oauth2-server),可以在数分钟内为 Laravel 应用提供完整的 OAuth2 服务器实现。
OAuth 本身不存在一个标准的实现,后端开发者自己根据实际的需求和标准的规定实现。其步骤一般如下:
* 客户端要求用户给予授权
* 用户同意给予授权
* 根据上一步获得的授权,向认证服务器请求令牌(token)
* 认证服务器对授权进行认证,确认无误后发放令牌
* 客户端使用令牌向资源服务器请求资源
* 资源服务器使用令牌向认证服务器确认令牌的正确性,确认无误后提供资源
## **初始化安装和配置**
### **安装**
首先通过 Composer 包管理器安装 Passport:
~~~
composer require laravel/passport
~~~
> 注:如果安装过程中提示需要更高版本的 Laravel:`laravel/passport v5.0.0 requires illuminate/http ~5.6`,可以通过指定版本来安装`composer require laravel/passport ~4.0`。
Passport 服务提供者为框架注册了自己的数据库迁移目录,所以在注册服务提供者之后(Laravel 5.5之后会自动注册服务提供者)需要迁移数据库,Passport 迁移将会为应用生成用于存放客户端和访问令牌的数据表:
~~~
php artisan migrate
~~~
> 注:如果你不想使用 Passport 的默认迁移,需要在`AppServiceProvider`的`register`方法中调用`Passport::ignoreMigrations`方法。你可以使用`php artisan vendor:publish --tag=passport-migrations`导出默认迁移。
接下来,需要运行`passport:install`命令,该命令会在`storage`目录下生成`oauth-private.key`和`oauth-public.key`,分别包含 OAuth 服务的私钥和公钥,用于安全令牌的加密解密,然后在`oauth_clients`数据表中初始化两条记录,相当于注册了两个客户端应用,一个用于密码授权令牌认证,一个用于私人访问令牌认证。
![](https://img.kancloud.cn/05/43/0543860c5651f08c6737c6ae6360b96b_2872x472.jpg)
### **修改模型类**
如果要让用户支持API认证,需要在对应模型类中使用`Laravel\Passport\HasApiTokens`Trait,比如`User`模型类:
~~~
<?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;
.......
}
~~~
该 Trait 中包含了授权令牌与客户端相关方法,后面我们在认证时会用到。
### **API认证路由**
Passport为API认证提供了相应的路由,而与之前注册路由不同的是,这一次我们在`AuthServiceProvider`的`boot`方法中注册API 认证相关路由:
~~~
<?php
namespace App\Providers;
use Laravel\Passport\Passport;
...
public function boot()
{
$this->registerPolicies();
Passport::routes();
//该方法将会为颁发访问令牌、撤销访问令牌、客户端以及私人访问令牌注册必要的路由
}
~~~
默认提供的路由控制器位于`\Laravel\Passport\Http\Controllers`命名空间下,并且路由前缀为`/oauth`,如果你想要自定义这些配置,可以在上述`routes`方法中通过第二个参数传入。具体注册的 API 认证相关路由如下:
![](https://img.kancloud.cn/fb/d5/fbd50101d924154c6e8ec31c6b3e90f9_2682x944.jpg)
### **修改配置文件**
最后,修改配置文件`config/auth.php`,将 API 认证驱动由`token`修改为`passport`:
~~~
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'passport',
'provider' => 'users',
],
],
~~~
## **单页面应用API认证**
如果你的 API 认证只用于客户端 JavaScript 与后端接口的交互,比如单页面应用,没必要走复杂的跳转授权流程,可以通过在`App\Http\Kernel`的`$middlewareGroups`中新增一个`CreateFreshApiToken`中间件来实现:
~~~
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,
],
'api' => [
'throttle:60,1',
'bindings',
],
];
~~~
该中间件注册在`web`路由中,会在用户首次通过 Web 页面登录表单登录后在 Cookie 中设置一个 Token,这样后续客户端发送请求时就会在 Cookie 中带上这个 Token。在访问需要认证的 API 接口时,会走`auth:api`认证中间件(配置文件`config/auth.php`中配置的 API 认证驱动是`passport`), Laravel 框架底层就会根据中间件传入的`api`参数,针对 API 接口认证会通过`Laravel\Passport\Guards\TokenGuard`获取认证信息:
~~~
public function user(Request $request)
{
if ($request->bearerToken()) {
return $this->authenticateViaBearerToken($request);
} elseif ($request->cookie(Passport::cookie())) {
return $this->authenticateViaCookie($request);
}
}
~~~
如果请求头中包含`Bearer Authentication`请求头,则获取对应的请求头 Token 信息,否则从 Cookie 中获取名为`laravel_token`的 Token 信息,由于我们在保存这个 Token 的时候包含了用户ID,所以最终会提取其中的用户 ID 从数据库获取用户数据并返回,从而完整用户认证判断和信息获取。感兴趣的同学可以跟着这个思路去看一下底层的实现代码。
## **移动端应用篇【密码授权令牌】**
这些移动端应用包括客户端 App、H5应用(基于 HTML5 开发,通常简称 H5 应用,域名与 Web 网站不同),也完全适用于同一个公司不同系统间的认证,包括不同 Web 网站间认证。对于这些自有网站,我们通常不希望进行常见的 OAuth 跳转授权,会影响用户体验,因此我们基于 Passport 提供的密码授权令牌来实现相应的 API 请求认证。
### **创建一个测试移动端应用**
既然是分离的独立应用之间的认证,我们先来创建一个新的应用用来测试,名字叫做`testapp`:
~~~
composer create-project --prefer-dist laravel/laravel testapp
~~~
安装完成后,进入应用根目录运行`npm install`初始化前端资源,然后配置这个应用的域名为`app.test`。
下面我们将通过`testapp`应用来模拟移动端应用,通过主项目`blog`作为后端应用,移动端应用访问后端应用的认证 API 接口时,需要后端应用授权才能访问,在 OAuth 服务中,这个授权通过颁发一个访问令牌实现。我们使用密码授权令牌的原因是和授权码令牌相比,这个过程中没有授权确认和跳转,整个过程就像是用户提交登录表单进行认证一样。
### **在后端应用中注册移动端应用**
我们在`blog`项目根目录下运行这个命令来注册个密码授权客户端`testapp`:
~~~
php artisan passport:client --password
~~~
![](https://img.kancloud.cn/fc/2e/fc2ecf5ce75b5468556eef48f6078a91_1436x274.jpg)
我们将应用的名称设置为`testapp`就好了,其它都会自动生成,执行完毕后在`oauth_clients`数据表中修改对应数据库记录的`redirect`字段值为`http://app.test/auth/callback`。
> 注:对于大型项目来说,可以通过后台注册中心申请审核的方式完成新应用的注册,方便统一管理。这里我们只是通过 Artisan 命令快速演示。
### **配置移动端应用**
回到`testapp`应用,在项目根目录下的`.env`文件中新增两个配置项,将刚刚在后端应用中注册的`testapp`应用配置信息填写到这里:
~~~
CLIENT_ID=7
CLIENT_SECRET=2JPrCvRyoJ14f0OqCe6nnQZNDfPLNNPY7TcfDnco
~~~
然后在`config/services.php`中新增如下配置项:
~~~
'blog' => [
'appid' => env('CLIENT_ID'),
'secret' => env('CLIENT_SECRET'),
'callback' => 'http://app.test/auth/callback'
]
~~~
### **在移动端应用填写表单登录进行登录**
接下来,在`testapp`应用中,我们将借助 Laravel 自带的认证脚手架快速实现认证路由和视图,以完成登录表单和请求提交:
~~~
php artisan make:auth
~~~
然后在`Auth/LoginController`控制器中重写`login`方法,将默认到数据库检查用户登录凭证的逻辑改为将请求发送到后端应用获取授权令牌:
~~~
// 在控制器顶部引入如下命名空间
use GuzzleHttp\Client;
use Illuminate\Http\Request;
// 重写 AuthenticatesUsers 中的 login 方法
public function login(Request $request)
{
$request->validate([
'email' => 'required|string',
'password' => 'required|string',
]);
$http = new Client();
// 发送相关字段到后端应用获取授权令牌
$response = $http->post('http://blog.test/oauth/token', [
'form_params' => [
'grant_type' => 'password',
'client_id' => config('services.blog.appid'),
'client_secret' => config('services.blog.secret'),
'username' => $request->input('email'), // 这里传递的是邮箱
'password' => $request->input('password'), // 传递密码信息
'scope' => '*'
],
]);
return response($response->getBody());
}
~~~
通过后端应用中 Passport 底层的密码授权类进行校验,校验成功后就会返回令牌信息给移动端应用:
![](https://img.kancloud.cn/51/6e/516ec873e3f324da9757a074f63e24a3_1924x866.jpg)
返回结果中包含四个字段,`access_token`是授权令牌,`token_type`表示认证类型是`Bearer`,我们可以将这个`access_token`值设置到`Bearer Authentication`请求头去请求需要认证的后端 API 接口。`refresh_token`在令牌过期后刷新令牌时使用,最后`expires_in`表示令牌有效期(单位是秒,即有效期一年)。
### **令牌的有效期**
如上例示,Passport 生成的授权令牌默认有效期是一年,但是为了提升系统安全性,也可以自定义配置其有效期。我们可以在`AuthServiceProvider`的`boot`方法中通过 Passport 门面上的`tokensExpireIn`或`refreshTokensExpireIn`方法来设置令牌有效期:
~~~
/**
* 注册任意认证/授权服务
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
Passport::routes();
Passport::tokensExpireIn(now()->addDays(15));
Passport::refreshTokensExpireIn(now()->addDays(15));
//以上两个方法等效,使用任意一个都可以设置授权令牌有效期为15天。过期之后,需要刷新令牌
}
~~~
### **刷新令牌**
当授权访问令牌过期后,我们可以通过在`oauth/token`路由请求中指定操作类型为`refresh_token`来刷新令牌:
~~~
$http = new GuzzleHttp\Client;
$response = $http->post('http://blog.test/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);
~~~
刷新令牌后,会生成新的令牌并返回,同时将老的令牌撤销。
## **第三方应用篇(授权码获取令牌)**
在公司自有系统之间我们通过用户密码授权令牌访问认证 API 接口,如果我们自己也是一个开放平台,需要支持第三方应用户接入获取认证信息呢?比如我们常见的第三方应用接入微信登录、微博登录、QQ 登录就是这样的例子。
我们把自己的系统比作微信、微博这样的平台,支持第三方 App、网站应用的接入,这个时候如果还通过用户密码授权令牌就有安全隐患了,如果第三方应用不可信,把用户名密码信息记录下来,这就泄露了用户的密码数据,所以面对这种场景,我们引入另一种 OAuth 认证方式 —— 通过授权码的方式获取令牌访问认证 API。
### **在后端系统注册第三方应用**
我们还是在上一篇教程创建的`testapp`应用基础上进行测试,并且在后端系统通过 Artisan 命令`passport:client`新注册这个第三方应用:
![](https://img.kancloud.cn/cb/50/cb50a6e72ad3059e17a846913fc4a6ea_1464x480.jpg)
### **配置第三方应用**
接下来,我们回到前端系统,修改`testapp`根目录下`.env`中的`CLIENT_ID`和`CLIENT_SECRET`配置项:
~~~
CLIENT_ID=9
CLIENT_SECRET=Xde5hsAbpEU8MMjwELFh6RNOzxX2LsrxgFTZvXkP
~~~
`config/services.php`中的配置保持和上例一样不变。
### **编写第三方应用路由和控制器**
通过授权码获取访问令牌需要两步操作:1. 到后端系统请求授权,如果用户在后端系统没有登录需要先登录,登录之后让用户确认授权,授权之后通过`callback`配置的跳转地址回跳到前端应用,并且在 URL 中带上授权码;2. 用户通过这个授权码获取访问令牌,拿到访问令牌之后就可以请求后端系统认证 API 接口了。
所以,我们需要在前端应用的`routes/web.php`中新增两个路由,一个用于请求授权获取授权码,一个用于从后端应用跳转回来,通过授权码在回跳路由中发起后端请求获取令牌:
~~~
Route::get('/auth', 'Auth\LoginController@oauth');
Route::get('/auth/callback', 'Auth\LoginController@callback');
~~~
然后定义相应的控制器方法:
~~~
public function oauth()
{
$query = http_build_query([
'client_id' => config('services.blog.appid'),
'redirect_uri' => config('services.blog.callback'),
'response_type' => 'code',
'scope' => '',
]);
return redirect('http://blog.test/oauth/authorize?'.$query);
}
public function callback(Request $request)
{
$code = $request->get('code');
if (!$code) {
dd('授权失败');
}
$http = new Client();
$response = $http->post('http://blog.test/oauth/token', [
'form_params' => [
'grant_type' => 'authorization_code',
'client_id' => config('services.blog.appid'), // your client id
'client_secret' => config('services.blog.secret'), // your client secret
'redirect_uri' => config('services.blog.callback'),
'code' => $code,
],
]);
return response($response->getBody());
}
~~~
### **测试通过授权码获取令牌**
在浏览器中访问`http://app.test/auth`,在前端应用中通过后端系统进行授权认证,如果你在后端应用`blog.test`上没有登录,先跳转到登录页面,登录之后则跳转到如下确认授权页面:
![](https://img.kancloud.cn/13/a4/13a476af87589e62dc3b420fffc755b9_1190x386.jpg)
当用户通过了授权请求,就会被重定向回第三方应用指定的`redirect_uri`,然后发起获取令牌请求获取访问令牌了。
这样,下次从前端应用`app.test`中访问后端系统 API 接口时,就可以通过在请求头中带上`access_token`来获取`blog.test`上的认证资源了,对应的逻辑和上一篇通过密码获取令牌访问是一样的。
## **开放平台篇【客户端凭证令牌】**
客户端凭证令牌的授权方式,不需要走典型的登录或授权重定向流程,适用于机器与机器之间的接口认证,类似我们做微信、微博、支付宝开放平台开发,需要先申请自己的应用,申请通过后,这些开放平台会给我们分配对应的 APP ID 和 APP SECRET。然后我们通过这个 APP ID 和 APP SECRET 去开放平台获取 Token(令牌),最后拿着这个令牌去访问认证资源即可。
我们还是以之前创建的测试项目`testapp`作为客户端应用,把后端项目`blog`作为类似微信的开放平台,为了简化流程,我们还是通过 Artisan 命令在后端注册客户端应用,免去申请流程,然后在客户端应用中通过分配的 APP ID 和 APP SECRET 获取授权令牌,最后拿着这个令牌访问后端认证接口。
### **在开放平台注册客户端应用**
我们通过如下 Artisan 命令在后端应用`blog`中注册客户端应用:
~~~
php artisan passport::client
~~~
该命令执行成功后,会在`oauth_clients`表中新增一条记录,包含给客户端应用分配的 APP ID 和 APP SECRET 信息,分别是`id`字段和`secret`字段。
### **更新第三方应用的配置信息**
回到客户端应用`testapp`,修改`.env`中的`CLIENT_ID`和`CLIENT_SECRET`配置:
~~~
CLIENT_ID=11
CLIENT_SECRET=XKmtGXC1CdG2LvhUpdp3y81IjuyrP0rLUPPq8reg
~~~
`config/services.php`中的`blog`配置项保持不变。
### **在客户端应用中定义路由和控制器**
接下来,我们在客户端应用中定义获取令牌的路由,在`routes/web.php`新增下面行代码:
~~~
Route::get('/auth/client', 'Auth\LoginController@client');
~~~
然后在`LoginController`控制器中编写对应的`client`方法:
~~~
public function client()
{
$http = new Client();
$response = $http->post('http://blog.test/oauth/token', [
'form_params' => [
'grant_type' => 'client_credentials',
'client_id' => config('services.blog.appid'), // your client id
'client_secret' => config('services.blog.secret'), // your client secret
'scope' => '*'
],
]);
return response($response->getBody());
}
~~~
注意到我们在获取令牌的请求数据中将`grant_type`类型设置为了`client_credentials`,意为通过客户端凭证颁发访问令牌。
至此,我们就完成了完整的获取客户端凭证令牌的代码编写和配置工作,接下来简单测试下这个流程。
### **测试客户端应用访问开放平台认证接口**
首先在浏览器中访问`http://app.test/auth/client`,就可以获取到访问令牌了:
![](https://img.kancloud.cn/75/fe/75fec7c534f7ebeaaf88532522536030_2824x550.jpg)
需要注意的是,客户端访问令牌默认长期有效,所以这里没有返回用于刷新令牌的`refresh_token`字段。
在测试认证 API 接口之前,我们还需要在后端应用的`routes/api.php`中新增一个测试路由:
~~~
Route::middleware('client')->get('/test', function (Request $request) {
return '欢迎访问 Laravel 学院!';
});
~~~
不同于之前需要检测用户认证的`auth:api`中间件,我们在这个路由中应用了`client`中间件,表示该路由需要通过客户端凭证访问令牌进行认证才能访问,接下来,我们在`app/Http/Kernel.php`的`$routeMiddleware`属性中定义这个中间件:
~~~
'client' => \Laravel\Passport\Http\Middleware\CheckClientCredentials::class,
~~~
这样,我们就可以在 Postman 中测试这个 API 接口的访问了。
## **沙箱测试篇【私人访问令牌】**
私人访问令牌的授权方式比较特殊,它不需要授权码,也不需要用户输入登录凭证,而是用户给自己颁发访问令牌。这种授权方式在用户测试、体验平台提供的认证 API 接口时非常方便,比如微信开放平台和支付宝开发平台都有沙箱测试模式,在这种测试模式下获取授权令牌的方式其实就是通过私人访问令牌来实现的。
我们还是将后端应用`blog`类比做开放平台,然后我们在这个开发平台上通过的测试应用体验系统提供的认证 API。这个时候,测试应用不需要分离出来,也没法分离出来。
### **在后台系统注册测试应用**
我们在`blog`项目根目录下通过如下 Artisan 命令注册一个测试应用,还是将其命名为`testapp`:
~~~
php artisan passport:client --personal
~~~
这样,我们就模拟创建了一个测试应用`testapp`,该应用记录存放在`oauth_clients`数据表中。
### **获取访问令牌**
既然是用户自己给自己颁发访问令牌,那就需要用到模型类了,我们以`User`模型类存储的用户为例进行演示。首先在该模型类中使用`HasApiTokens`Trait(已经使用的话跳过此步骤):
~~~
...
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Passport\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, Notifiable;
...
~~~
然后我们在`routes/web.php`中定义一个新的路由,用于测试获取访问令牌:
~~~
Route::get('auth/personal', 'Auth\LoginController@personal');
~~~
接下来,在控制器`LoginController`编写对应的方法`personal`:
~~~
public function personal()
{
$user = User::where('name', '学院君')->first();
$token = $user->createToken('Users')->accessToken;
dd($token);
}
~~~
这样,我们访问`http://blog.test/auth/personal`就可以获取到该用户的访问令牌了。生成的令牌记录可以在`oauth_access_tokens`数据表中找到,私人访问令牌默认是长期有效的。
拿到这个令牌之后我们就可以通过它访问认证接口了。
## **隐式授权令牌**
隐式授权令牌和通过授权码获取令牌有点相似,不过,它不需要获取授权码,就可以将令牌返回给客户端,通常适用于同一个公司自有系统之间的认证,尤其是客户端应用不能安全存储令牌信息的时候。
要启用该授权,需要在后端系统`AuthServiceProvider`的`boot`方法中调用`enableImplicitGrant`方法:
~~~
public function boot()
{
...
//启用隐式授权令牌
Passport::enableImplicitGrant();
}
~~~
启用隐式授权之后开发者就可以通过对应应用的 ClientID 从应用中请求访问令牌,还是老规矩,我们先在后端系统`blog`中注册前端应用`testapp`:
![](https://img.kancloud.cn/2e/d4/2ed416861cca1c27886e1a6e4193979d_1434x478.jpg)
我们将用户ID字段留空,设置应用名称及授权成功后的回调地址。
### **前端应用设置**
在前端应用`testapp`中,首先需要在`.env`环境配置中修改`CLIENT_ID`和`CLIENT_SECRET`配置值:
~~~
CLIENT_ID=13
CLIENT_SECRET=GDTgIeNVsQ5tPFbok55deciO5My2TSRtv2FYFFHM
~~~
在`config/services.php`的`blog`配置项中修改`callback`配置值:
~~~
'callback' => 'http://app.test/auth/implicit/callback'
~~~
然后需要在`routes/web.php`里面注册对应的隐式认证路由:
~~~
Route::get('/auth/implicit', 'Auth\LoginController@implicit');
Route::get('/auth/implicit/callback', 'Auth\LoginController@implicitCallback');
~~~
最后在控制器`LoginController`中编写`implicit`和`implicitCallback`方法:
~~~
public function implicit()
{
$query = http_build_query([
'client_id' => config('services.blog.appid'),
'redirect_uri' => config('services.blog.callback'),
'response_type' => 'token',
'scope' => '',
]);
return redirect('http://blog.test/oauth/authorize?'.$query);
}
public function implicitCallback(Request $request)
{
dd($request->get('access_token'));
}
~~~
我们在`auth/implicit`路由中发送认证请求到后端系统的`oauth/authorize`路由,如果认证成功会将令牌信息通过传入的`redirect_uri`链接回跳的时候返回。
### **测试隐式授权认证**
首先,我们通过`http://app.test/auth/implicit`获取令牌,访问该链接会跳转到后端应用页面,如果没有登录的话,需要先登录,登录之后会跳转到授权确认页面:
![](https://img.kancloud.cn/13/a4/13a476af87589e62dc3b420fffc755b9_1190x386.jpg)
确认授权之后,就会根据当前 URL 中的`redirect_uri`参数值跳转回前端应用,并且在 URL 中附加`access_token`信息:
~~~
http://app.test/auth/implicit/callback#access_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6ImUzNWZmOWNjNmU4NzQ0NzQ4OTM4MjFlOGM0NzQ3M2M0YWE3NmQ2MDgyZDJmNTY3NjU3YWU4MmNmNDBmZWJlNzYxYmZjMTg5NDBjMjU2ODJlIn0.eyJhdWQiOiIxMyIsImp0aSI6ImUzNWZmOWNjNmU4NzQ0NzQ4OTM4MjFlOGM0NzQ3M2M0YWE3NmQ2MDgyZDJmNTY3NjU3YWU4MmNmNDBmZWJlNzYxYmZjMTg5NDBjMjU2ODJlIiwiaWF0IjoxNTQ1OTgzMDAxLCJuYmYiOjE1NDU5ODMwMDEsImV4cCI6MTU3NzUxOTAwMSwic3ViIjoiMjAiLCJzY29wZXMiOltdfQ.C6mpRQdUKMHCyULYXMnu6f_WysaV4UbCoiSnPU0Zi8CBj8q5a5pa_CDU_pfnhoDjby7XCwBz6SSUPy6FRf2H0QzBIqMMjc7G39RXcIsLrvVThY5Zagm5EH3iNQ2Odf_qKZtzw9pjv_Y8g07vd8qEMo7wDG5H5yaBVtUvKrE4hb2mb_yZI9v76ievAWZM2ryw8dMbUMrCKZe3Q1FDYf7SiJ7iTxJRBINQYFW5QMBcZy0m8lSnMxS7Xq8WsZ_ZiCdOdCXms17Anfiuba438oxEPtNFZf23Ma4Htp0_oLhejHO9Sz5RXQ8KB7d5SuIwRyCk380TBfv77_OsY3vYBhFtwprZ0tVpZOOAM_qdUKrIbJEVCSGIQaqz8wHjS2WqHkW8I-nVsU8zhewWkFhbeIYAMrqXgPRrljom2u5WAvEwcsHBRe4QxRSN6QwBJiyVLOFWCi6aq_3JmOpDXG8PDlOy2M7DYj4XrkghAvO-FTnpkr_zgoy9ssdRFMzlea5NngdbeDg-QFzma0Jmem253qNXOaN3yS-w15VT4j555UrQXkp2OXS2iuMybYDYlNJExD2QqjHmTHbf2y5YchFTqrBoq00OZ3ASyUxL3w-UzHDjEm8t_oABF6ezsNAIqJVpndy1vHFjb9bfwgYpo7r6i7iizpo_8z0vN64BQowPG-GW9Kk&token_type=Bearer&expires_in=31536000
~~~
通过锚点返回`access_token`的原因是不会把它们发送到服务器(你可以无论通过 Laravel 还是PHP 都解析不到`access_token`),只有客户端才能解析上述锚点里的参数。这也正好符合隐式授权令牌的使用场景:客户端凭证不能被安全存储的移动应用或 JavaScript 应用。
## **令牌作用域**
在认证过程中,有时候我们还需要对令牌的授权作用域进行限制,不是认证接口的所有返回数据都可以通过该令牌进行访问,或者不是所有接口都需要通过该令牌进行访问,是否能够获取对应数据或访问对应接口取决于用户的主动勾选。我们以腾讯视频登录功能为例,如果选择通过 QQ 账号进行登录,可以在右侧看到权限选择面板,默认权限是获取用户昵称、头像、性别信息,其它信息需要用户手动勾选才能获取:
![](https://img.kancloud.cn/47/db/47db1d4d144c88901db00c2a30337e93_1520x1014.jpg)
下面我们以通过授权码获取令牌为例,演示下令牌作用域功能的实现。
### **配置后端应用**
我们在后端应用`blog`中通过`Passport::tokensCan`定义 API 认证的令牌作用域。打开`AuthServiceProvider`服务提供者类,在`boot`方法中调用该方法,设置三个令牌作用域:
~~~
// 令牌作用域
Passport::tokensCan([
'basic-user-info' => '获取用户名、邮箱信息',
'all-user-info' => '获取用户所有信息',
'get-post-info' => '获取文章详细信息',
]);
~~~
`basic-user-info`用于限定获取用户接口指定字段信息,`all-user-info`用于获取用户接口所有字段信息,`get-post-info`用于限定访问文章接口。
然后打开`app/Http/Kernel.php`,在`$routeMiddleware`属性中引入两个中间件:
~~~
'scopes' => \Laravel\Passport\Http\Middleware\CheckScopes::class,
'scope' => \Laravel\Passport\Http\Middleware\CheckForAnyScope::class,
~~~
`scopes`用于检查传入令牌作用域是否包含所有指定中间件参数,`scope`用于检查传入令牌作用域是否包含任意指定中间件参数。听起来有点懵,下面我们举个例子。
在`routes/api.php`中新增一个获取文章详情信息的路由,并修改路由定义如下:
~~~
Route::middleware('auth:api')->group(function () {
Route::get('/user', function (Request $request) {
$user = $request->user();
if ($user->tokenCan('all-user-info')) {
// 如果用户令牌有获取所有信息权限,返回所有用户字段
return $user;
}
// 否则返回用户名和邮箱等基本信息
return ['name' => $user->name, 'email' => $user->email];
})->middleware('scope:basic-user-info,all-user-info');
Route::get('/post/{id}', function (Request $request, $id) {
return \App\Post::find($id);
})->middleware('scopes:get-post-info');
});
~~~
上述第一个路由应用了`scope`中间件,要求传入令牌作用域包含`basic-user-info`或`all-user-info`任意一个即可,并且在路由闭包中根据用户具体字段获取权限进一步进行了细分;第二个路由应用了`scopes`中间件,要求传入令牌作用域必须包含`get-post-info`,如果有多个的话,可以通过逗号分隔。
### **在第三方应用中测试**
回到第三方应用`testapp`,将配置信息修改回授权码令牌对应配置,然后在`/auth`路由对应控制器方法`LoginController@oauth`中,设置`scope`请求字段值:
~~~
public function oauth()
{
$query = http_build_query([
'client_id' => config('services.blog.appid'),
'redirect_uri' => config('services.blog.callback'),
'response_type' => 'code',
'scope' => 'all-user-info get-post-info',
]);
return redirect('http://blog.test/oauth/authorize?'.$query);
}
~~~
然后,我们在浏览器中访问`http://test.app/auth`通过授权码获取令牌,跳转到后端系统授权界面时会多出一段提示信息告知该授权令牌的作用域。
当我们点击绿色的确认授权按钮后,除了可以在第三方应用中获取到授权令牌,也会在后端应用数据表`oauth_access_tokens`和`oauth_auth_codes`生成的记录看到对应的`scopes`字段值了。