## **簡介**
在 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`數據表中初始化兩條記錄,相當于注冊了兩個客戶端應用,一個用于密碼授權令牌認證,一個用于私人訪問令牌認證。

### **修改模型類**
如果要讓用戶支持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 認證相關路由如下:

### **修改配置文件**
最后,修改配置文件`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
~~~

我們將應用的名稱設置為`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 底層的密碼授權類進行校驗,校驗成功后就會返回令牌信息給移動端應用:

返回結果中包含四個字段,`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`新注冊這個第三方應用:

### **配置第三方應用**
接下來,我們回到前端系統,修改`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`上沒有登錄,先跳轉到登錄頁面,登錄之后則跳轉到如下確認授權頁面:

當用戶通過了授權請求,就會被重定向回第三方應用指定的`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`,就可以獲取到訪問令牌了:

需要注意的是,客戶端訪問令牌默認長期有效,所以這里沒有返回用于刷新令牌的`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`:

我們將用戶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`獲取令牌,訪問該鏈接會跳轉到后端應用頁面,如果沒有登錄的話,需要先登錄,登錄之后會跳轉到授權確認頁面:

確認授權之后,就會根據當前 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 賬號進行登錄,可以在右側看到權限選擇面板,默認權限是獲取用戶昵稱、頭像、性別信息,其它信息需要用戶手動勾選才能獲取:

下面我們以通過授權碼獲取令牌為例,演示下令牌作用域功能的實現。
### **配置后端應用**
我們在后端應用`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`字段值了。