Laravel 開發 API

 

1. 原由

       隨着先後端徹底分離,PHP 也基本告別了 view 模板嵌套開發,轉而專門寫資源接口。Laravel 是 PHP 框架中最優雅的框架,國內也愈來愈多人告別 ThinkPHP 選擇了 LaravelLaravel 框架自己對 API 有支持,可是感受再工做中仍是須要再作一些處理。Lumen 用起來不順手,有些包不能很好地支持。因此,將 Laravel 框架進行一些配置處理,讓其在開發 API 時更駕輕就熟。php

       固然,你也能夠點擊這裏 , 直接跳到成果~前端

 

2. 準備工做

 

2.1. 環境

PHP > 7.1 MySQL > 5.5 Redis > 2.8
 

2.2. 工具

postman
composer
 

2.3. 使用 postman

爲了模擬 AJAX 請求,請將 header頭 設置 X-Requested-With 爲 XMLHttpRequestlaravel

 

file
 

 

 

2.4. 安裝 Laravel

Laravel 只要 >=5.5 皆可,這裏採用文章編寫時最新的 5.7 版本git

composer create-project laravel/laravel Laravel --prefer-dist "5.7.*"
 

2.5. 建立數據庫

CREATE TABLE `users` ( `id` INT UNSIGNED NOT NULL PRIMARY KEY auto_increment COMMENT '主鍵ID', `name` VARCHAR ( 12 ) NOT NULL COMMENT '用戶名稱', `password` VARCHAR ( 80 ) NOT NULL COMMENT '密碼', `last_token` text COMMENT '登錄時的token', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '用戶狀態 -1表明已刪除 0表明正常 1表明凍結', `created_at` TIMESTAMP NULL DEFAULT NULL COMMENT '建立時間', `updated_at` TIMESTAMP NULL DEFAULT NULL COMMENT '修改時間' ) ENGINE = INNODB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci;
 

3. 初始化數據

 

3.1. Model 移動

在項目的 app 目錄下能夠看到,有一個 User.php 的模型文件。由於 Laravel 默認把模型文件放在 app 目錄下,若是數據表多的話,這裏模型文件就會不少,不便於管理,因此咱們先要將模型文件移動到其餘文件夾內。github

1) 在 app 目錄下新建 Models 文件夾,而後將 User.php 文件移動進來。
2) 修改 User.php 的內容web

<?php namespace App\Models; //這裏從App改爲了App\Models use Illuminate\Notifications\Notifiable; use Illuminate\Foundation\Auth\User as Authenticatable; class User extends Authenticatable { use Notifiable; protected $table = 'users'; //去掉我建立的數據表沒有的字段 protected $fillable = [ 'name', 'password' ]; //去掉我建立的數據表沒有的字段 protected $hidden = [ 'password' ]; //將密碼進行加密 public function setPasswordAttribute($value) { $this->attributes['password'] = bcrypt($value); } }

3) 由於有關於 User 的命名空間發生了改變,因此咱們全局搜索 App\User, 將其替換爲 App\Models\User. 我一共搜索到 4 個文件ajax

app/Http/Controllers/Auth 目錄下的 RegisterController.php config 目錄下的 services.php config 目錄下的 auth.php database/factories 目錄下的 UserFactory.php
 

3.2. 控制器

由於是專門作 API 的,因此咱們要把是 API 的控制器都放到 app\Http\Controllers\Api 目錄下。redis

使用命令行建立控制器算法

php artisan make:controller Api/UserController

編寫 app/Http/Controllers/Api 目錄下的 UserController.php 文件數據庫

<?php namespace App\Http\Controllers\Api; use Illuminate\Http\Request; use App\Http\Controllers\Controller; class UserController extends Controller { // public function index(){ return 'guaosi'; } }

這裏寫了 index 函數,用來下面創建路由後的測試,查看是否能夠正常訪問。

 

3.3. 路由

在 routes 目錄下的 api.php 是專門用來寫 Api 接口的路由,因此咱們打開它,填寫如下內容,作一個測試.

<?php use Illuminate\Http\Request; Route::namespace('Api')->prefix('v1')->group(function () { Route::get('/users','UserController@index')->name('users.index'); });

由於咱們 Api 控制器的命名空間是 App\Http\Controllers\Api, 而 Laravel 默認只會在命名空間 App\Http\Controllers 下查找控制器,因此須要咱們給出 namespace

同時,添加一個 prefix 是爲了版本號,方便後期接口升級區分。

打開 postman, 用 get 方式請求你的域名/api/v1/users, 最後返回結果是

guaosi

則成功

 

3.4. 建立驗證器

在建立用戶以前,咱們先建立驗證器,來讓咱們服務器接收到的數據更安全。固然,咱們也要把關於 Api 驗證的放在一個專門的文件夾內。
先建立一個 Request 的基類

php artisan make:request Api/FormRequest

由於驗證器默認的權限驗證是 false,致使返回都是 403 的權限不經過錯誤。這裏咱們沒有用到權限認證,爲了方便處理,咱們默認將權限都是經過的狀態。因此,每一個文件都須要咱們將 false 改爲 true

public function authorize() { //false表明權限驗證不經過,返回403錯誤 //true表明權限認證經過 return true; }

因此咱們修改 app/Http/Requests/Api 目錄下的 FormRequest.php 文件

<?php namespace App\Http\Requests\Api; use Illuminate\Foundation\Http\FormRequest as BaseFormRequest; class FormRequest extends BaseFormRequest { public function authorize() { //false表明權限驗證不經過,返回403錯誤 //true表明權限認證經過 return true; } }

這樣這個命名空間下的驗證器都會默認經過權限驗證。固然,若是你須要權限驗證,能夠經過直接覆蓋方法。

接着咱們開始建立關於 UserController 的專屬驗證器

php artisan make:request Api/UserRequest

編輯 app/Http/Requests/Api 目錄下的 UserRequest.php 文件

<?php namespace App\Http\Requests\Api; class UserRequest extends FormRequest { public function rules() { switch ($this->method()) { case 'GET': { return [ 'id' => ['required,exists:shop_user,id'] ]; } case 'POST': { return [ 'name' => ['required', 'max:12', 'unique:users,name'], 'password' => ['required', 'max:16', 'min:6'] ]; } case 'PUT': case 'PATCH': case 'DELETE': default: { return [ ]; } } } public function messages() { return [ 'id.required'=>'用戶ID必須填寫', 'id.exists'=>'用戶不存在', 'name.unique' => '用戶名已經存在', 'name.required' => '用戶名不能爲空', 'name.max' => '用戶名最大長度爲12個字符', 'password.required' => '密碼不能爲空', 'password.max' => '密碼長度不能超過16個字符', 'password.min' => '密碼長度不能小於6個字符' ]; } }
 

3.5. 建立用戶

如今咱們來編寫建立用戶接口,製做一些虛擬數據。(就不使用 seeder 來填充了)
打開 UserController.php

//用戶註冊 public function store(UserRequest $request){ User::create($request->all()); return '用戶註冊成功。。。'; } //用戶登陸 public function login(Request $request){ $res=Auth::guard('web')->attempt(['name'=>$request->name,'password'=>$request->password]); if($res){ return '用戶登陸成功...'; } return '用戶登陸失敗'; }

而後咱們建立路由,編輯 api.php

Route::post('/users','UserController@store')->name('users.store'); Route::post('/login','UserController@login')->name('users.login');

打開 postman, 用 post 方式請求你的域名/api/v1/users, 在 form-data 記得填寫要建立的用戶名和密碼。

最後返回結果是

用戶建立成功。。。

則成功。

 

file
 

 

若是返回

{ "message": "The given data was invalid.", "errors": { "name": [ "用戶名不能爲空" ], "password": [ "密碼不能爲空" ] } }

則證實驗證失敗。

而後驗證是否能夠正常登陸。由於咱們認證的字段是 name 跟 password, 而 Laravel 默認認證的是 email跟 password。因此咱們還要打開 app/Http/Controllers/auth 目錄下的 LoginController.php, 加入以下代碼

public function username() { return 'name'; }

打開 postman, 用 post 方式請求你的域名/api/v1/login
最後返回結果是

用戶登陸成功...

則成功

 

file
 

 

 

3.6. 建立 10 個用戶

爲了測試使用,請自行經過接口建立 10 個用戶。

 

3.7. 編寫相關資源接口

給出總體控制器信息 UserController.php

<?php namespace App\Http\Controllers\Api; use App\Http\Requests\Api\UserRequest; use App\Models\User; use App\Http\Controllers\Controller; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; class UserController extends Controller { //返回用戶列表 public function index(){ //3個用戶爲一頁 $users = User::paginate(3); return $users; } //返回單一用戶信息 public function show(User $user){ return $user; } //用戶註冊 public function store(UserRequest $request){ User::create($request->all()); return '用戶註冊成功。。。'; } //用戶登陸 public function login(Request $request){ $res=Auth::guard('web')->attempt(['name'=>$request->name,'password'=>$request->password]); if($res){ return '用戶登陸成功...'; } return '用戶登陸失敗'; } }
 

3.8. 編寫路由

給出總體路由信息 api.php

<?php use Illuminate\Http\Request; Route::namespace('Api')->prefix('v1')->group(function () { Route::get('/users','UserController@index')->name('users.index'); Route::get('/users/{user}','UserController@show')->name('users.show'); Route::post('/users','UserController@store')->name('users.store'); Route::post('/login','UserController@login')->name('users.login'); });
 

4. 存在問題

以上全部返回的結果,不管正確或者錯誤,都沒有一個統一格式規範,對開發 Api 不太友好的,須要咱們進行一些修改,讓 Laravel 框架能夠更加友好地編寫 Api。

 

5. 構造

 

5.1. 跨域問題

全部問題,跨域先行。跨域問題沒有解決,一切處理都是紙老虎。這裏咱們使用 medz 作的 cors 擴展包

 

5.1.1. 安裝 medz/cors

composer require medz/cors
 

5.1.2. 發佈配置文件

php artisan vendor:publish --provider="Medz\Cors\Laravel\Providers\LaravelServiceProvider" --force
 

5.1.3. 修改配置文件

打開 config/cors.php, 在 expose-headers 添加值 Authorization

return [ ...... 'expose-headers' => ['Authorization'], ...... ];

這樣跨域請求時,才能返回 header 頭爲 Authorization 的內容,不然在刷新用戶 token 時不會返回刷新後的 token

 

5.1.4. 增長中間件別名

打開 app/Http/Kernel.php, 增長一行

protected $routeMiddleware = [ ...... //前面的中間件 'cors'=> \Medz\Cors\Laravel\Middleware\ShouldGroup::class, ];
 

5.1.5. 修改路由

打開 routes/api.php, 在路由組中增長使用中間件

Route::namespace('Api')->prefix('v1')->middleware('cors')->group(function () { Route::get('/users','UserController@index')->name('users.index'); Route::get('/users/{user}','UserController@show')->name('users.show'); Route::post('/users','UserController@store')->name('users.store'); Route::post('/login','UserController@login')->name('users.login'); });
 

5.2. 統一 Response 響應處理

接口主流返回 json 格式,其中包含 http狀態碼status請求狀態data請求資源結果等等。須要咱們有一個 API 接口全局都能有統一的格式和對應的數據處理。參考於這裏

 

5.2.1. 封裝返回的統一消息

在 app/Api/Helpers 目錄 (不存在目錄本身新建) 下新建 ApiResponse.php
填入以下內容

<?php namespace App\Api\Helpers; use Symfony\Component\HttpFoundation\Response as FoundationResponse; use Response; trait ApiResponse { /** * @var int */ protected $statusCode = FoundationResponse::HTTP_OK; /** * @return mixed */ public function getStatusCode() { return $this->statusCode; } /** * @param $statusCode * @return $this */ public function setStatusCode($statusCode,$httpCode=null) { $httpCode = $httpCode ?? $statusCode; $this->statusCode = $statusCode; return $this; } /** * @param $data * @param array $header * @return mixed */ public function respond($data, $header = []) { return Response::json($data,$this->getStatusCode(),$header); } /** * @param $status * @param array $data * @param null $code * @return mixed */ public function status($status, array $data, $code = null){ if ($code){ $this->setStatusCode($code); } $status = [ 'status' => $status, 'code' => $this->statusCode ]; $data = array_merge($status,$data); return $this->respond($data); } /** * @param $message * @param int $code * @param string $status * @return mixed */ /* * 格式 * data: * code:422 * message:xxx * status:'error' */ public function failed($message, $code = FoundationResponse::HTTP_BAD_REQUEST,$status = 'error'){ return $this->setStatusCode($code)->message($message,$status); } /** * @param $message * @param string $status * @return mixed */ public function message($message, $status = "success"){ return $this->status($status,[ 'message' => $message ]); } /** * @param string $message * @return mixed */ public function internalError($message = "Internal Error!"){ return $this->failed($message,FoundationResponse::HTTP_INTERNAL_SERVER_ERROR); } /** * @param string $message * @return mixed */ public function created($message = "created") { return $this->setStatusCode(FoundationResponse::HTTP_CREATED) ->message($message); } /** * @param $data * @param string $status * @return mixed */ public function success($data, $status = "success"){ return $this->status($status,compact('data')); } /** * @param string $message * @return mixed */ public function notFond($message = 'Not Fond!') { return $this->failed($message,Foundationresponse::HTTP_NOT_FOUND); } }
 

5.2.2. 新建 Api 控制器基類

在 app/Http/Controller/Api 目錄下新建一個 Controller.php 做爲 Api 專門的基類.
填入如下內容

<?php namespace App\Http\Controllers\Api; use App\Api\Helpers\ApiResponse; use App\Http\Controllers\Controller as BaseController; class Controller extends BaseController { use ApiResponse; // 其餘通用的Api幫助函數 }
 

5.2.3. 繼承 Api 控制器基類

讓 Api 的控制器繼承這個基類便可。
打開 UserController.php 文件,去掉命名空間 use App\Http\Controllers\Controller

namespace App\Http\Controllers\Api; use App\Http\Requests\Api\UserRequest; use App\Models\User; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; class UserController extends Controller { ...... }
 

5.2.4. 如何使用

得益於前面統一消息的封裝,使用起來很是容易。
1. 返回正確信息

return $this->success('用戶登陸成功...');

2. 返回正確資源信息

return $this->success($user);

3. 返回自定義 http 狀態碼的正確信息

return $this->setStatusCode(201)->success('用戶登陸成功...');

4. 返回錯誤信息

return $this->failed('用戶註冊失敗');

5. 返回自定義 http 狀態碼的錯誤信息

return $this->failed('用戶登陸失敗',401);

6. 返回自定義 http 狀態碼的錯誤信息,同時也想返回本身內部定義的錯誤碼

return $this->failed('用戶登陸失敗',401,10001);

默認 success 返回的狀態碼是 200,failed 返回的狀態碼是 400

 

5.2.5. 修改用戶控制器

咱們將統一消息封裝運用到 UserController 中

<?php namespace App\Http\Controllers\Api; use App\Http\Requests\Api\UserRequest; use App\Models\User; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; class UserController extends Controller { //返回用戶列表 public function index(){ //3個用戶爲一頁 $users = User::paginate(3); return $this->success($users); } //返回單一用戶信息 public function show(User $user){ return $this->success($user); } //用戶註冊 public function store(UserRequest $request){ User::create($request->all()); return $this->setStatusCode(201)->success('用戶註冊成功'); } //用戶登陸 public function login(Request $request){ $res=Auth::guard('web')->attempt(['name'=>$request->name,'password'=>$request->password]); if($res){ return $this->setStatusCode(201)->success('用戶登陸成功...'); } return $this->failed('用戶登陸失敗',401); } }
 

5.2.6. 測試

  1. 返回用戶列表
    請求 http://你的域名/api/v1/users
    file
     
  2. 返回單一用戶
    請求 http://你的域名/api/v1/users/1
    file
     
  3. 登錄正確
    請求 http://你的域名/api/v1/login
    file
     
  4. 登錄錯誤
    請求 http://你的域名/api/v1/login
    file
     
     

    5.3. Api-Resource 資源

在上面請求返回用戶列表和返回單一用戶時,返回的字段都是數據庫裏全部的字段,固然,不包含咱們在 User 模型中去除的 password 字段。

 

5.3.1. 需求

此時,咱們若是想控制返回的字段有哪些,可使用 select 或者使用 User 模型中的 hidden 數組來限制字段。

這 2 種辦法雖然能夠,可是擴展性太差。而且我想對 status 返回的形式進行修改,好比 0 的時候顯示正常,1 顯示凍結,此時就須要遍歷數據進行修改了。此時,Laravel 提供的 API 資源就能夠很好地解決咱們的問題。

當構建 API 時,你每每須要一個轉換層來聯結你的 Eloquent 模型和實際返回給用戶的 JSON 響應。Laravel 的資源類可以讓你以更直觀簡便的方式將模型和模型集合轉化成 JSON。

也就是在 C 層輸出 V 層時,中間再來一層來專門處理字段問題,咱們能夠稱之爲 ViewModel 層。

詳細能夠查看手冊如何使用。

 

5.3.2. 建立單一用戶資源和列表用戶資源

php artisan make:resource Api/UserResource

修改 app/Http/Resources/Api 目錄下的 UserResource.php 文件

<?php namespace App\Http\Resources\Api; use Illuminate\Http\Resources\Json\JsonResource; class UserResource extends JsonResource { /** * Transform the resource into an array. * * @param \Illuminate\Http\Request $request * @return array */ public function toArray($request) { switch ($this->status){ case -1: $this->status = '已刪除'; break; case 0: $this->status = '正常'; break; case 1: $this->status = '凍結'; break; } return [ 'id'=>$this->id, 'name' => $this->name, 'status' => $this->status, 'created_at'=>(string)$this->created_at, 'updated_at'=>(string)$this->updated_at ]; } }
 

5.3.3. 如何使用

返回單一用戶 (單一的資源)

return $this->success(new UserResource($user));

返回用戶列表 (資源列表)

return UserResource::collection($users); //這裏不能用$this->success(UserResource::collection($users)) //不然不能返回分頁標籤信息
 

5.3.4. 修改用戶控制器

//返回用戶列表 public function index(){ //3個用戶爲一頁 $users = User::paginate(3); return UserResource::collection($users); } //返回單一用戶信息 public function show(User $user){ return $this->success(new UserResource($user)); }
 

5.3.5. 測試

返回單一用戶 (單一的資源)

file
 


返回用戶列表 (資源列表)

file
 

 

 

5.4. Enum 枚舉

咱們經常會使用數字來表明狀態,好比用戶表,咱們使用 -1 表明已刪除 0 表明正常 1 表明凍結。

 

5.4.1. 兩個問題

  1. 當咱們判斷一個用戶,若是是刪除或者凍結狀態就不讓其登錄了。判斷代碼這樣寫
    //有可能狀態有不少,因此這邊就直接用 或 來判斷不取反了。 if($user->status==-1||$user->status==1){ // 不容許用戶登陸邏輯 return } //用戶正常登陸邏輯

上面邏輯和編寫沒有什麼問題。由於是如今看,能夠很明白的知道 -1 表明已刪除,1 表明凍結。可是若是一個月後再來看這行代碼,早已經忘記了 -1 跟 1 具體表示的含義。

  1. 參考上面 UserResource.php 編寫時,判斷 status 具體狀態函數,咱們是使用 switch 語句。這樣太不美觀,並且地方用多了還容易冗餘,每次編寫都須要去查看每一個數字表明的具體意思。
 

5.4.2. 解決思路

  1. 第一個問題:爲何一段時間後再看就不知道 -1 跟 1 具體表示的含義?

       這是由於單純的數字沒有解釋說明的做用,變量以及函數這些具備解釋說明的做用,可讓咱們馬上知道具體含義。

  1. 第二個問題:如何給一個數字就能直接知道它表明的含義?

       提供一個函數,返回這個數字表明的具體含義。

而這些,均可以使用 Enum枚舉能夠解決。

 

5.4.3. 注意

PHP 和 Laravel 框架自己是不支持 Enum枚舉的,不過咱們能夠模擬枚舉的功能

 

5.4.4. 建立枚舉

在 app/Models 下新建目錄 Enum , 並在目錄 Enum 下新建 UserEnum.php 文件,填寫如下內容

<?php namespace App\Models\Enum; class UserEnum { // 狀態類別 const INVALID = -1; //已刪除 const NORMAL = 0; //正常 const FREEZE = 1; //凍結 public static function getStatusName($status){ switch ($status){ case self::INVALID: return '已刪除'; case self::NORMAL: return '正常'; case self::FREEZE: return '凍結'; default: return '正常'; } } }
 

5.4.5. 使用

1. 表示具體含義

//有可能狀態有不少,因此這邊就直接用 或 來判斷不取反了。 if($user->status==UserEnum::INVALID||$user->status==UserEnum::FREEZE){ // 不容許用戶登陸邏輯 return } //用戶正常登陸邏輯

2. 修改 UserResource.php

public function toArray($request) { return [ 'id'=>$this->id, 'name' => $this->name, 'status' => UserEnum::getStatusName($this->status), 'created_at'=>(string)$this->created_at, 'updated_at'=>(string)$this->updated_at ]; }

再請求單一用戶和用戶列表接口,返回結果和以前同樣。

 

5.5. 異常自定義處理

 

5.5.1. 再發現一個問題

咱們在 UserController.php 文件中修改

//返回單一用戶信息 public function show(User $user){ 3/0; return $this->success(new UserResource($user)); }

故意報個錯,請求看看結果

file
 


咱們再把設置成 ajax 的 header 頭去掉

file
 

 

報錯很是詳細,而且把咱們隱私設置都暴露出來了,這是因爲咱們.env 的 APP_DEBUG 是 true 狀態。咱們不但願這些信息被其餘訪問者看到。咱們改成 false,再請求看看結果。

 

file
 

 

嗯。很好,不只別人看不到了,連咱們本身都看不到了

 

5.5.2. 需求

  1. 全部的異常信息都以統一 json 格式輸出
  2. 由於咱們是開發者,而且.env 文件默認是不加入 git 上傳線上的,咱們但願能夠當 APP_DEBUG 爲 true(本地) 的時候能夠繼續顯示詳細的錯誤信息,false(線上) 的時候就顯示簡要 json 信息,好比 500。
 

5.5.3. 建立自定義異常處理

在 app/Api/Helpers 目錄下新建 ExceptionReport.php 文件,填入如下內容

<?php namespace App\Api\Helpers; use Exception; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\AuthenticationException; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; use Tymon\JWTAuth\Exceptions\TokenInvalidException; class ExceptionReport { use ApiResponse; /** * @var Exception */ public $exception; /** * @var Request */ public $request; /** * @var */ protected $report; /** * ExceptionReport constructor. * @param Request $request * @param Exception $exception */ function __construct(Request $request, Exception $exception) { $this->request = $request; $this->exception = $exception; } /** * @var array */ //當拋出這些異常時,可使用咱們定義的錯誤信息與HTTP狀態碼 //能夠把常見異常放在這裏 public $doReport = [ AuthenticationException::class => ['未受權',401], ModelNotFoundException::class => ['該模型未找到',404], AuthorizationException::class => ['沒有此權限',403], ValidationException::class => [], UnauthorizedHttpException::class=>['未登陸或登陸狀態失效',422], TokenInvalidException::class=>['token不正確',400], NotFoundHttpException::class=>['沒有找到該頁面',404], MethodNotAllowedHttpException::class=>['訪問方式不正確',405], QueryException::class=>['參數錯誤',401], ]; public function register($className,callable $callback){ $this->doReport[$className] = $callback; } /** * @return bool */ public function shouldReturn(){ //只有請求包含是json或者ajax請求時纔有效 // if (! ($this->request->wantsJson() || $this->request->ajax())){ // // return false; // } foreach (array_keys($this->doReport) as $report){ if ($this->exception instanceof $report){ $this->report = $report; return true; } } return false; } /** * @param Exception $e * @return static */ public static function make(Exception $e){ return new static(\request(),$e); } /** * @return mixed */ public function report(){ if ($this->exception instanceof ValidationException){ $error = array_first($this->exception->errors()); return $this->failed(array_first($error),$this->exception->status); } $message = $this->doReport[$this->report]; return $this->failed($message[0],$message[1]); } public function prodReport(){ return $this->failed('服務器錯誤','500'); } }
 

5.5.4. 捕捉異常

修改 app/Exceptions 目錄下的 Handler.php 文件

<?php namespace App\Exceptions; use App\Api\Helpers\ExceptionReport; use Exception; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; class Handler extends ExceptionHandler { public function render($request, Exception $exception) { //ajax請求咱們才捕捉異常 if ($request->ajax()){ // 將方法攔截到本身的ExceptionReport $reporter = ExceptionReport::make($exception); if ($reporter->shouldReturn()){ return $reporter->report(); } if(env('APP_DEBUG')){ //開發環境,則顯示詳細錯誤信息 return parent::render($request, $exception); }else{ //線上環境,未知錯誤,則顯示500 return $reporter->prodReport(); } } return parent::render($request, $exception); } }
 

5.5.5. 測試

繼續打開設置 AJAX 的 header 頭

1. 關閉 APP_DEBUG,請求剛剛故意錯誤的接口。

file
 


2. 開啓 APP_DEBUG,請求剛剛故意錯誤的接口。

file
 


3. 請求一個不存在的路由,查看返回結果。

file
 

 

其餘的異常顯示,自行測試啦~

 

5.6. jwt-auth

在傳統 web 中,咱們通常是使用 session 來斷定一個用戶的登錄狀態。而在 API 開發中,咱們使用的是 tokenjwt-token 是 Laravel 開發 API 用的比較多的。

JWT 全稱 JSON Web Tokens ,是一種規範化的 token。能夠理解爲對 token 這一技術提出一套規範,是在 RFC 7519 中提出的。

jwt-auth 的詳細介紹分析能夠看 JWT 超詳細分析這篇文章,具體使用能夠看 JWT 完整使用詳解 這篇文章。

 

5.6.1. 安裝

composer require tymon/jwt-auth 1.0.0-rc.3

若是是 Laravel5.5 版本,則安裝 rc.1。若是是 Laravel5.6 版本,則安裝 rc.2

 

5.6.2. 配置

配置參考來自使用 Jwt-Auth 實現 API 用戶認證以及無痛刷新訪問令牌

1. 添加服務提供商
打開 config 目錄下的 app.php 文件,添加下面代碼

'providers' => [ ... Tymon\JWTAuth\Providers\LaravelServiceProvider::class, ]

2. 發佈配置文件

php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"

此命令會在 config 目錄下生成一個 jwt.php 配置文件,你能夠在此進行自定義配置。

3. 生成密鑰

php artisan jwt:secret

此命令會在你的 .env 文件中新增一行 JWT_SECRET=secret。以此來做爲加密時使用的祕鑰。

4. 配置 Auth guard
打開 config 目錄下的 auth.php 文件,修改成下面代碼

'guards' => [ 'web' => [ 'driver' => 'session', 'provider' => 'users', ], 'api' => [ 'driver' => 'jwt', 'provider' => 'users', ], ],

這樣,咱們就能讓 api 的用戶認證變成使用 jwt

5. 更改 Model

若是須要使用 jwt-auth 做爲用戶認證,咱們須要對咱們的 User 模型進行一點小小的改變,實現一個接口,變動後的 User 模型以下

<?php namespace App\Models; use Illuminate\Notifications\Notifiable; use Illuminate\Foundation\Auth\User as Authenticatable; use Tymon\JWTAuth\Contracts\JWTSubject; class User extends Authenticatable implements JWTSubject { use Notifiable; public function getJWTIdentifier() { return $this->getKey(); } public function getJWTCustomClaims() { return []; } ......

6. 配置項詳解
config 目錄下的 jwt.php 文件配置詳解

<?php return [ /* |-------------------------------------------------------------------------- | JWT Authentication Secret |-------------------------------------------------------------------------- | | 用於加密生成 token 的 secret | */ 'secret' => env('JWT_SECRET'), /* |-------------------------------------------------------------------------- | JWT Authentication Keys |-------------------------------------------------------------------------- | | 若是你在 .env 文件中定義了 JWT_SECRET 的隨機字符串 | 那麼 jwt 將會使用 對稱算法 來生成 token | 若是你沒有定有,那麼jwt 將會使用以下配置的公鑰和私鑰來生成 token | */ 'keys' => [ /* |-------------------------------------------------------------------------- | Public Key |-------------------------------------------------------------------------- | | 公鑰 | */ 'public' => env('JWT_PUBLIC_KEY'), /* |-------------------------------------------------------------------------- | Private Key |-------------------------------------------------------------------------- | | 私鑰 | */ 'private' => env('JWT_PRIVATE_KEY'), /* |-------------------------------------------------------------------------- | Passphrase |-------------------------------------------------------------------------- | | 私鑰的密碼。 若是沒有設置,能夠爲 null。 | */ 'passphrase' => env('JWT_PASSPHRASE'), ], /* |-------------------------------------------------------------------------- | JWT time to live |-------------------------------------------------------------------------- | | 指定 access_token 有效的時間長度(以分鐘爲單位),默認爲1小時,您也能夠將其設置爲空,以產生永不過時的標記 | */ 'ttl' => env('JWT_TTL', 60), /* |-------------------------------------------------------------------------- | Refresh time to live |-------------------------------------------------------------------------- | | 指定 access_token 可刷新的時間長度(以分鐘爲單位)。默認的時間爲 2 周。 | 大概意思就是若是用戶有一個 access_token,那麼他能夠帶着他的 access_token | 過來領取新的 access_token,直到 2 周的時間後,他便沒法繼續刷新了,須要從新登陸。 | */ 'refresh_ttl' => env('JWT_REFRESH_TTL', 20160), /* |-------------------------------------------------------------------------- | JWT hashing algorithm |-------------------------------------------------------------------------- | | 指定將用於對令牌進行簽名的散列算法。 | */ 'algo' => env('JWT_ALGO', 'HS256'), /* |-------------------------------------------------------------------------- | Required Claims |-------------------------------------------------------------------------- | | 指定必須存在於任何令牌中的聲明。 | | */ 'required_claims' => [ 'iss', 'iat', 'exp', 'nbf', 'sub', 'jti', ], /* |-------------------------------------------------------------------------- | Persistent Claims |-------------------------------------------------------------------------- | | 指定在刷新令牌時要保留的聲明密鑰。 | */ 'persistent_claims' => [ // 'foo', // 'bar', ], /* |-------------------------------------------------------------------------- | Blacklist Enabled |-------------------------------------------------------------------------- | | 爲了使令牌無效,您必須啓用黑名單。 | 若是您不想或不須要此功能,請將其設置爲 false。 | */ 'blacklist_enabled' => env('JWT_BLACKLIST_ENABLED', true), /* | ------------------------------------------------------------------------- | Blacklist Grace Period | ------------------------------------------------------------------------- | | 當多個併發請求使用相同的JWT進行時, | 因爲 access_token 的刷新 ,其中一些可能會失敗 | 以秒爲單位設置請求時間以防止併發的請求失敗。 | */ 'blacklist_grace_period' => env('JWT_BLACKLIST_GRACE_PERIOD', 0), /* |-------------------------------------------------------------------------- | Providers |-------------------------------------------------------------------------- | | 指定整個包中使用的各類提供程序。 | */ 'providers' => [ /* |-------------------------------------------------------------------------- | JWT Provider |-------------------------------------------------------------------------- | | 指定用於建立和解碼令牌的提供程序。 | */ 'jwt' => Tymon\JWTAuth\Providers\JWT\Namshi::class, /* |-------------------------------------------------------------------------- | Authentication Provider |-------------------------------------------------------------------------- | | 指定用於對用戶進行身份驗證的提供程序。 | */ 'auth' => Tymon\JWTAuth\Providers\Auth\Illuminate::class, /* |-------------------------------------------------------------------------- | Storage Provider |-------------------------------------------------------------------------- | | 指定用於在黑名單中存儲標記的提供程序。 | */ 'storage' => Tymon\JWTAuth\Providers\Storage\Illuminate::class, ], ];
 

5.6.3. 測試

1. 咱們在 UserController 控制器中將 login 方法進行修改以及新增一個 logout 方法用來退出登陸還有 info 方法用來獲取當前用戶的信息。

//用戶登陸 public function login(Request $request){ $token=Auth::guard('api')->attempt(['name'=>$request->name,'password'=>$request->password]); if($token) { return $this->setStatusCode(201)->success(['token' => 'bearer ' . $token]); } return $this->failed('帳號或密碼錯誤',400); } //用戶退出 public function logout(){ Auth::guard('api')->logout(); return $this->success('退出成功...'); } //返回當前登陸用戶信息 public function info(){ $user = Auth::guard('api')->user(); return $this->success(new UserResource($user)); }

2. 添加一下路由
routes/api.php

//當前用戶信息 Route::get('/users/info','UserController@info')->name('users.info');

3. 接着咱們打開 postman, 請求 http://你的域名/api/v1/login. 能夠看到接口返回的 token.

{ "status": "success", "code": 201, "data": { "token": "bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC90ZXN0LmNvbVwvYXBpXC92MVwvbG9naW4iLCJpYXQiOjE1NTEzMzUyNzgsImV4cCI6MTU1MTMzODg3OCwibmJmIjoxNTUxMzM1Mjc4LCJqdGkiOiJrUzZSWHRoQVBkczR6ck4wIiwic3ViIjoxLCJwcnYiOiIyM2JkNWM4OTQ5ZjYwMGFkYjM5ZTcwMWM0MDA4NzJkYjdhNTk3NmY3In0.FLk-JPFBDTWcItPRN8SVGaLI0j2zgiWLLs_MNKxCafQ" } } 

4. 此時,咱們打開 Postman 直接訪問 http://你的域名/api/v1/users/info, 你會看到報了以下錯誤.

Trying to get property 'id' of non-object

這是咱們沒有攜帶 token 致使的。報錯不友好咱們將在下面自動刷新用戶認證解決。

5. 咱們在 Postman 的 Header 頭部分再加一個 key 爲 Authorizationvalue 爲登錄成功後返回的 token 值,而後再次進行請求,能夠看到成功返回當前登錄用戶的信息。

file
 

 

 

5.7. 自動刷新用戶認證

 

5.7.1. 需求

如今我想用戶登陸後,爲了保證安全性,每一個小時該用戶的 token 都會自動刷新爲全新的,用舊的 token 請求不會經過。咱們知道,用戶若是 token 不對,就會退到當前界面從新登陸來得到新的 token,我同時但願雖然刷新了 token,可是可否不要從新登陸,就算從新登陸也是一週甚至一個月以後呢?給用戶一種無感知的體驗。

看着感受很神奇,咱們一塊兒手摸手來實現。

 

5.7.2. 自定義認證中間件

php artisan make:middleware Api/RefreshTokenMiddleware

打開 app/Http/Middleware/Api 目錄下的 RefreshTokenMiddleware.php 文件,填寫如下內容

<?php namespace App\Http\Middleware\Api; use Auth; use Closure; use Tymon\JWTAuth\Exceptions\JWTException; use Tymon\JWTAuth\Facades\JWTAuth; use Tymon\JWTAuth\Http\Middleware\BaseMiddleware; use Tymon\JWTAuth\Exceptions\TokenExpiredException; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; // 注意,咱們要繼承的是 jwt 的 BaseMiddleware class RefreshTokenMiddleware extends BaseMiddleware { /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * * @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException * * @return mixed */ public function handle($request, Closure $next) { // 檢查這次請求中是否帶有 token,若是沒有則拋出異常。 $this->checkForToken($request); // 使用 try 包裹,以捕捉 token 過時所拋出的 TokenExpiredException 異常 try { // 檢測用戶的登陸狀態,若是正常則經過 if ($this->auth->parseToken()->authenticate()) { return $next($request); } throw new UnauthorizedHttpException('jwt-auth', '未登陸'); } catch (TokenExpiredException $exception) { // 此處捕獲到了 token 過時所拋出的 TokenExpiredException 異常,咱們在這裏須要作的是刷新該用戶的 token 並將它添加到響應頭中 try { // 刷新用戶的 token $token = $this->auth->refresh(); // 使用一次性登陸以保證這次請求的成功 Auth::guard('api')->onceUsingId($this->auth->manager()->getPayloadFactory()->buildClaimsCollection()->toPlainArray()['sub']); } catch (JWTException $exception) { // 若是捕獲到此異常,即表明 refresh 也過時了,用戶沒法刷新令牌,須要從新登陸。 throw new UnauthorizedHttpException('jwt-auth', $exception->getMessage()); } } // 在響應頭中返回新的 token return $this->setAuthenticationHeader($next($request), $token); } }
 

5.7.3. 增長中間件別名

打開 app/Http 目錄下的 Kernel.php 文件,添加以下一行

protected $routeMiddleware = [ ...... 'api.refresh'=>\App\Http\Middleware\Api\RefreshTokenMiddleware::class, ];
 

5.7.4. 路由器修改

接着咱們將路由進行修改,添加上咱們寫好的中間件。
routes/api.php

<?php use Illuminate\Http\Request; Route::namespace('Api')->prefix('v1')->middleware('cors')->group(function () { //用戶註冊 Route::post('/users','UserController@store')->name('users.store'); //用戶登陸 Route::post('/login','UserController@login')->name('users.login'); Route::middleware('api.refresh')->group(function () { //當前用戶信息 Route::get('/users/info','UserController@info')->name('users.info'); //用戶列表 Route::get('/users','UserController@index')->name('users.index'); //用戶信息 Route::get('/users/{user}','UserController@show')->name('users.show'); //用戶退出 Route::get('/logout','UserController@logout')->name('users.logout'); }); });
 

5.7.5. 測試

1. 此時咱們再次不攜帶 token,使用 Postman 直接訪問 http://你的域名/api/v1/users/info, 返回以下錯誤

{ "status": "error", "code": 422, "message": "未登陸或登陸狀態失效" }

2. 那隨便輸入 token 又會是怎麼樣呢?咱們也來嘗試一下

{ "status": "error", "code": 400, "message": "token不正確" }

3. 如今,咱們再作一個若是 token 過時了,可是刷新限制沒有過時的狀況,看看會有什麼結果。咱們先將 config/jwt.php 裏的 ttl 從 60 改爲 1。意味着從新生成的 token 將會 1 分鐘後過時。

而後咱們從新登陸獲取到 token,替換 /api/v1/users/info 原有的 token,進行訪問,能夠正常返回用戶的信息。

等過了一分鐘,咱們再進行訪問,發現依舊能夠返回用戶信息,可是咱們在返回的 Headers 的 Authorization 能夠看到新的 token

file
 


此時若是咱們再次訪問,則報出異常

 

{ "status": "error", "code": 422, "message": "未登陸或登陸狀態失效" }

咱們替換上新的 token,再次訪問,訪問正常經過。

4. 如今,咱們接着繼續作 token 和刷新時間都過時的狀況,會發生什麼。咱們再將 config/jwt.php 裏的 refresh_ttl 從 20160 改爲 2

從新按照 3 步驟執行一次,當剛過一分鐘時,返回結果與 3 相同,都是正常返回信息而且在 Headers 攜帶了新的 token。

當 2 分鐘事後,報以下錯誤信息。

{ "status": "error", "code": 422, "message": "未登陸或登陸狀態失效" }

5. 爲了後面的方便,咱們將修改的 ttl 和 refresh_ttl 的時間復原。

 

5.7.6. 前端邏輯

上面能夠看出,當 token 過時或者無效以及亂寫,返回的 HTTP狀態碼都是 422。這是由於這個異常被咱們上面自定義異常捕捉了

UnauthorizedHttpException::class=>['未登陸或登陸狀態失效',422],

因此,能夠跟前端小夥伴商量一個狀態碼,專門表示接收到這個狀態碼就要退回從新登陸了。當 Header 頭攜帶 Authorization 時,就要及時自動替換新的 token,不須要回到從新登陸界面。這樣用戶就能徹底無感知啦~

 

5.8. 多角色認證

若是咱們的系統不只僅只有一種角色身份,還有其餘的角色身份須要認證呢?目前咱們的角色認證是認證 Users 表的,若是咱們再加入一個 Admins 表,也要角色認證要如何操做?

 

5.8.1. Admin 用戶表

咱們將數據庫的 Users 表複製一份,將其命名爲 Admins 表,而且將其中的一個用戶名進行修改,以示區別。

 

5.8.2. 框架文件

咱們分別將 User.php 模型文件,UserEnum.php 枚舉文件,UserResource.php 資源文件,UserRequest.php 驗證器文件 UserController.php 控制器文件各複製一份,更改成 Admin 的,並將其中內容也改成 Admin 相關。由於就是複製粘貼,把 user 改爲 admin, 因爲篇幅問題具體修改過程我就不放代碼了。具體的能夠看下面的成品

 

5.8.3. 用戶認證文件

打開 config/auth.php 文件,修改以下內容

'guards' => [ 'web' => [ 'driver' => 'session', 'provider' => 'users', ], 'api' => [ 'driver' => 'jwt', 'provider' => 'users', ], 'admin' => [ 'driver' => 'jwt', 'provider' => 'admins', ], ], 'providers' => [ 'users' => [ 'driver' => 'eloquent', 'model' => App\Models\User::class, ], 'admins' => [ 'driver' => 'eloquent', 'model' => App\Models\Admin::class, ], // 'users' => [ // 'driver' => 'database', // 'table' => 'users', // ], ],

此時,guard 守護就多了一個 admin,當 Auth::guard('admin') 時,就會自動查找 Admin 模型文件,這樣就能跟上面的 User 模型認證分開了。

 

5.8.4. 刷新用戶認證中間件

咱們須要再複製一個刷新用戶認證的中間件,專門爲 admin 認證以及刷新 token.
app/Http/Middleware/Api/RefreshAdminTokenMiddleware.php

<?php namespace App\Http\Middleware\Api; use Auth; use Closure; use Tymon\JWTAuth\Exceptions\JWTException; use Tymon\JWTAuth\Facades\JWTAuth; use Tymon\JWTAuth\Http\Middleware\BaseMiddleware; use Tymon\JWTAuth\Exceptions\TokenExpiredException; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; // 注意,咱們要繼承的是 jwt 的 BaseMiddleware class RefreshAdminTokenMiddleware extends BaseMiddleware { /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * * @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException * * @return mixed */ public function handle($request, Closure $next) { // 檢查這次請求中是否帶有 token,若是沒有則拋出異常。 $this->checkForToken($request); // 使用 try 包裹,以捕捉 token 過時所拋出的 TokenExpiredException 異常 try { // 檢測用戶的登陸狀態,若是正常則經過 if ($this->auth->parseToken()->authenticate()) { return $next($request); } throw new UnauthorizedHttpException('jwt-auth', '未登陸'); } catch (TokenExpiredException $exception) { // 此處捕獲到了 token 過時所拋出的 TokenExpiredException 異常,咱們在這裏須要作的是刷新該用戶的 token 並將它添加到響應頭中 try { // 刷新用戶的 token $token = $this->auth->refresh(); // 使用一次性登陸以保證這次請求的成功 Auth::guard('admin')->onceUsingId($this->auth->manager()->getPayloadFactory()->buildClaimsCollection()->toPlainArray()['sub']); } catch (JWTException $exception) { // 若是捕獲到此異常,即表明 refresh 也過時了,用戶沒法刷新令牌,須要從新登陸。 throw new UnauthorizedHttpException('jwt-auth', $exception->getMessage()); } } // 在響應頭中返回新的 token return $this->setAuthenticationHeader($next($request), $token); } }
 

5.8.5. 增長中間件別名

打開 app/Http 目錄下的 Kernel.php 文件,添加以下一行

protected $routeMiddleware = [ ...... 'admin.refresh'=>\App\Http\Middleware\Api\RefreshAdminTokenMiddleware::class, ];
 

5.8.6. 路由文件

routes/api.php

<?php use Illuminate\Http\Request; Route::namespace('Api')->prefix('v1')->middleware('cors')->group(function () { //用戶註冊 Route::post('/users', 'UserController@store')->name('users.store'); //用戶登陸 Route::post('/login', 'UserController@login')->name('users.login'); Route::middleware('api.refresh')->group(function () { //當前用戶信息 Route::get('/users/info', 'UserController@info')->name('users.info'); //用戶列表 Route::get('/users', 'UserController@index')->name('users.index'); //用戶信息 Route::get('/users/{user}', 'UserController@show')->name('users.show'); //用戶退出 Route::get('/logout', 'UserController@logout')->name('users.logout'); }); //管理員註冊 Route::post('/admins', 'AdminController@store')->name('admins.store'); //管理員登陸 Route::post('/admin/login', 'AdminController@login')->name('admins.login'); Route::middleware('admin.refresh')->group(function () { //當前管理員信息 Route::get('/admins/info', 'AdminController@info')->name('admins.info'); //管理員列表 Route::get('/admins', 'AdminController@index')->name('admins.index'); //管理員信息 Route::get('/admins/{user}', 'AdminController@show')->name('admins.show'); //管理員退出 Route::get('/admins/logout', 'AdminController@logout')->name('admins.logout'); }); });
 

5.8.7. 控制器文件

app/Http/Controllers/Api/AdminController.php

<?php namespace App\Http\Controllers\Api; use App\Http\Requests\Api\UserRequest; use App\Http\Resources\Api\AdminResource; use App\Models\Admin; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; class AdminController extends Controller { //返回用戶列表 public function index(){ //3個用戶爲一頁 $admins = Admin::paginate(3); return AdminResource::collection($admins); } //返回單一用戶信息 public function show(Admin $admin){ return $this->success(new AdminResource($admin)); } //返回當前登陸用戶信息 public function info(){ Auth::guard('admin')->user(); return $this->success(new AdminResource($admins)); } //用戶註冊 public function store(UserRequest $request){ Admin::create($request->all()); return $this->setStatusCode(201)->success('用戶註冊成功'); } //用戶登陸 public function login(Request $request){ $token=Auth::guard('admin')->attempt(['name'=>$request->name,'password'=>$request->password]); if($token) { return $this->setStatusCode(201)->success(['token' => 'bearer ' . $token]); } return $this->failed('帳號或密碼錯誤',400); } //用戶退出 public function logout(){ Auth::guard('admin')->logout(); return $this->success('退出成功...'); } }
 

5.8.8. 測試

咱們將 admin 這邊登錄返回的 token 放在 admin 的請求用戶信息接口,看看會不會串號。結果返回

{ "status": "success", "code": 200, "data": { "id": 1, "name": "guaosi123", "status": "正常", "created_at": "2019-02-26 08:12:31", "updated_at": "2019-02-26 08:12:31" } }

咱們再將 token 放在 user 的請求用戶信息接口,看看會不會串號。結果返回

{ { "status": "success", "code": 200, "data": { "id": 1, "name": "guaosi123", "status": "正常", "created_at": "2019-02-26 08:12:31", "updated_at": "2019-03-01 01:48:12" } } }

看來 jwt-auth 真的串號了,這個問題咱們下面再開一個標題進行解決。

 

5.8.9. 自動區分 guard

1. 當咱們編寫登錄,退出,獲取當前用戶信息的時候,都須要

Auth::guard('admin')

經過制定 guard 的具體守護是哪個。由於框架默認的 guard 默認守護的是 web

因此,我但願可讓 guard 自動化,若是我請求的是 users 的,我就守護 api。若是我請求的是 admins的,我就守護 admin

接下來,就以 admins 的爲例,users 的保持不動

2. 新建中間件

php artisan make:middleware Api/AdminGuardMiddleware

打開 app/Http/Middleware/Api/AdminGuardMiddleware.php 文件,填入如下內容

<?php namespace App\Http\Middleware\Api; use Closure; class AdminGuardMiddleware { /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * * @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException * * @return mixed */ public function handle($request, Closure $next) { config(['auth.defaults.guard'=>'admin']); return $next($request); } }

3. 添加中間件別名
打開 app/Http 目錄下的 Kernel.php 文件,添加以下一行

protected $routeMiddleware = [ ...... 'admin.guard'=>\App\Http\Middleware\Api\AdminGuardMiddleware::class, ];

4. 修改路由
接着咱們將路由進行修改,添加上咱們寫好的中間件。
routes/api.php

Route::middleware('admin.guard')->group(function () { //管理員註冊 Route::post('/admins', 'AdminController@store')->name('admins.store'); //管理員登陸 Route::post('/admin/login', 'AdminController@login')->name('admins.login'); Route::middleware('admin.refresh')->group(function () { //當前管理員信息 Route::get('/admins/info', 'AdminController@info')->name('admins.info'); //管理員列表 Route::get('/admins', 'AdminController@index')->name('admins.index'); //管理員信息 Route::get('/admins/{user}', 'AdminController@show')->name('admins.show'); //管理員退出 Route::get('/admins/logout', 'AdminController@logout')->name('admins.logout'); }); });

5. 修改控制器
app/Http/Controllers/Api/AdminController.php

//返回當前登陸用戶信息 public function info(){ $admins = Auth::user(); return $this->success(newAdminResource($admins)); } //用戶登陸 public function login(Request $request){ $token=Auth::attempt(['name'=>$request->name,'password'=>$request->password]); if($token) { return $this->setStatusCode(201)->success(['token' => 'bearer ' . $token]); } return $this->failed('帳號或密碼錯誤',400); } //用戶退出 public function logout(){ Auth::logout(); return $this->success('退出成功...'); }

6. 測試結果
將 admin 登錄後的 token 再次攜帶訪問 /api/v1/admins/info, 依舊能夠正常輸出當前用戶信息。

user 的自動區分請本身填寫,這裏就再也不囉嗦一遍了。

 

5.9. 修復角色認證串號問題

首先,咱們須要知道一個問題,jwt-auth 頒發的 token 裏面是不包含模型驅動的。也就是說,經過這個令牌,咱們不知道它究竟是屬於 api 仍是屬於 admin 的。

折騰了一夜,百度了不少資料,想找找有沒有解決辦法。結果找到的都是沒什麼做用的,或者是讓自動刷新失效了。最後本身追源碼,找到了這種比較完美的方式。

 

5.9.1. 函數

咱們先來看幾個咱們在中間件中用的函數

$this->checkForToken($request) //這個函數只會檢測是否攜帶token以及token是否能被當前密鑰所解析 $this->auth->parseToken()->authenticate() //將使用token進行登陸,若是token過時,則拋出 TokenExpiredException 異常 $this->auth->refresh(); //刷新當前token

而後咱們再來看一個有趣的函數

Auth::check(); //能夠根據當前的`guard`來判斷這個token是否屬於這個 guard ,不是則拋出 TokenInvalidException 異常 //可是,當token過時時,不管是否是屬於這個 guard ,它也是都拋出 TokenInvalidException 異常。這致使咱們沒法正常判斷出究竟是屬於哪一種問題 //因此,想要用check()來判斷,是不可能的。

接着,咱們繼續看一個有意思的函數

Auth::payload(); //能夠輸出當前token的載荷信息(也就是token解析後的內容) //可是,若是你這個token已通過期了,那這個函數將會報錯
 

5.9.2. 原理

咱們經過 Auth::payload() 能夠看到未過時 token 的載荷信息

{ "sub": "1", "iss": "http://test.com/api/v1/admin/login", "iat": 1551407332, "exp": 1551407392, "nbf": 1551407332, "jti": "f9zwcMHaXBr5kQYp", "prv": "df883db97bd05ef8ff85082d686c45e832e593a9" }

咱們實際上是能夠拿到這些荷載信息的。同時,咱們也能夠加入本身的信息,這樣在中間件時候進行解析,拿到咱們的負載,就能夠進行判斷是不是屬於當前 guard 的 token 了。

 

5.9.3. 實現

修改 app\Http\Controllers\Api\AdminController.php 中的 login 方法,在 token 中加入咱們定義的字段。

//用戶登陸 public function login(Request $request) { //獲取當前守護的名稱 $present_guard =Auth::getDefaultDriver(); $token = Auth::claims(['guard'=>$present_guard])->attempt(['name' => $request->name, 'password' => $request->password]); if ($token) { return $this->setStatusCode(201)->success(['token' => 'bearer ' . $token]); } return $this->failed('帳號或密碼錯誤', 400); }

再修改中間件 app/Http/Middleware/Api/RefreshAdminTokenMiddleware.php ,讓其就算過時 token 也能讀取出裏面的信息

<?php namespace App\Http\Middleware\Api; use Auth; use Closure; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; use Tymon\JWTAuth\Exceptions\JWTException; use Tymon\JWTAuth\Exceptions\TokenExpiredException; use Tymon\JWTAuth\Exceptions\TokenInvalidException; use Tymon\JWTAuth\Http\Middleware\BaseMiddleware; // 注意,咱們要繼承的是 jwt 的 BaseMiddleware class RefreshAdminTokenMiddleware extends BaseMiddleware { /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * * @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException * * @return mixed * @throws TokenInvalidException */ public function handle($request, Closure $next) { // 檢查這次請求中是否帶有 token,若是沒有則拋出異常。 $this->checkForToken($request); //1. 格式經過,驗證是不是專屬於這個的token //獲取當前守護的名稱 $present_guard = Auth::getDefaultDriver(); //獲取當前token $token=Auth::getToken(); //即便過時了,也能獲取到token裏的 載荷 信息。 $payload = Auth::manager()->getJWTProvider()->decode($token->get()); //若是不包含guard字段或者guard所對應的值與當前的guard守護值不相同 //證實是不屬於當前guard守護的token if(empty($payload['guard'])||$payload['guard']!=$present_guard){ throw new TokenInvalidException(); } //使用 try 包裹,以捕捉 token 過時所拋出的 TokenExpiredException 異常 //2. 此時進入的都是屬於當前guard守護的token try { // 檢測用戶的登陸狀態,若是正常則經過 if ($this->auth->parseToken()->authenticate()) { return $next($request); } throw new UnauthorizedHttpException('jwt-auth', '未登陸'); } catch (TokenExpiredException $exception) { // 3. 此處捕獲到了 token 過時所拋出的 TokenExpiredException 異常,咱們在這裏須要作的是刷新該用戶的 token 並將它添加到響應頭中 try { // 刷新用戶的 token $token = $this->auth->refresh(); // 使用一次性登陸以保證這次請求的成功 Auth::onceUsingId($this->auth->manager()->getPayloadFactory()->buildClaimsCollection()->toPlainArray()['sub']); } catch (JWTException $exception) { // 若是捕獲到此異常,即表明 refresh 也過時了,用戶沒法刷新令牌,須要從新登陸。 throw new UnauthorizedHttpException('jwt-auth', $exception->getMessage()); } } // 在響應頭中返回新的 token return $this->setAuthenticationHeader($next($request), $token); } }

這個中間件是通用的,能夠直接替換 User 的刷新用戶認證中間件噢

 

5.9.4. 測試

此時再次進行測試是否串號,最後結果能夠成功阻止以前的串號問題,暫未發現其餘 BUG。

user 的修復串號問題請本身修改,這裏就再也不囉嗦一遍了。

 

5.10. 單一設備登錄

 

5.10.1. 提出需求

同一時間只容許登陸惟一一臺設備。例如設備 A 中用戶若是已經登陸,那麼使用設備 B 登陸同一帳戶,設備 A 就沒法繼續使用了。

 

5.10.2. 原理

咱們在登錄,token 過時自動更換的時候,都會產生一個新的 token

咱們將 token 都存到表中的 last_token 字段。在登錄接口,獲取到 last_token 裏的值,將其加入黑名單。

這樣,只要咱們不管在哪裏登錄,以前的 token 必定會被拉黑失效,必須從新登錄,咱們的目的也就達到了。

 

5.10.3. 實現

修改 app\Http\Controllers\Api\AdminController.php 中的 login 方法,在登錄的時候,拉黑上一個 token

//用戶登陸 public function login(Request $request) { //獲取當前守護的名稱 $present_guard =Auth::getDefaultDriver(); $token = Auth::claims(['guard'=>$present_guard])->attempt(['name' => $request->name, 'password' => $request->password]); if ($token) { //若是登錄,先檢查原先是否有存token,有的話先失效,而後再存入最新的token $user = Auth::user(); if ($user->last_token) { try{ Auth::setToken($user->last_token)->invalidate(); }catch (TokenExpiredException $e){ //由於讓一個過時的token再失效,會拋出異常,因此咱們捕捉異常,不須要作任何處理 } } $user->last_token = $token; $user->save(); return $this->setStatusCode(201)->success(['token' => 'bearer ' . $token]); } return $this->failed('帳號或密碼錯誤', 400); }

再修改中間件 app/Http/Middleware/Api/RefreshAdminTokenMiddleware.php ,更新的 token 加到 last_token

<?php namespace App\Http\Middleware\Api; use Auth; use Closure; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; use Tymon\JWTAuth\Exceptions\JWTException; use Tymon\JWTAuth\Exceptions\TokenExpiredException; use Tymon\JWTAuth\Exceptions\TokenInvalidException; use Tymon\JWTAuth\Http\Middleware\BaseMiddleware; // 注意,咱們要繼承的是 jwt 的 BaseMiddleware class RefreshAdminTokenMiddleware extends BaseMiddleware { /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * * @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException * * @return mixed * @throws TokenInvalidException */ public function handle($request, Closure $next) { // 檢查這次請求中是否帶有 token,若是沒有則拋出異常。 $this->checkForToken($request); //1. 格式經過,驗證是不是專屬於這個的token //獲取當前守護的名稱 $present_guard = Auth::getDefaultDriver(); //獲取當前token $token=Auth::getToken(); //即便過時了,也能獲取到token裏的 載荷 信息。 $payload = Auth::manager()->getJWTProvider()->decode($token->get()); //若是不包含guard字段或者guard所對應的值與當前的guard守護值不相同 //證實是不屬於當前guard守護的token if(empty($payload['guard'])||$payload['guard']!=$present_guard){ throw new TokenInvalidException(); } //使用 try 包裹,以捕捉 token 過時所拋出的 TokenExpiredException 異常 //2. 此時進入的都是屬於當前guard守護的token try { // 檢測用戶的登陸狀態,若是正常則經過 if ($this->auth->parseToken()->authenticate()) { return $next($request); } throw new UnauthorizedHttpException('jwt-auth', '未登陸'); } catch (TokenExpiredException $exception) { // 3. 此處捕獲到了 token 過時所拋出的 TokenExpiredException 異常,咱們在這裏須要作的是刷新該用戶的 token 並將它添加到響應頭中 try { // 刷新用戶的 token $token = $this->auth->refresh(); // 使用一次性登陸以保證這次請求的成功 Auth::onceUsingId($this->auth->manager()->getPayloadFactory()->buildClaimsCollection()->toPlainArray()['sub']); //刷新了token,將token存入數據庫 $user = Auth::user(); $user->last_token = $token; $user->save(); } catch (JWTException $exception) { // 若是捕獲到此異常,即表明 refresh 也過時了,用戶沒法刷新令牌,須要從新登陸。 throw new UnauthorizedHttpException('jwt-auth', $exception->getMessage()); } } // 在響應頭中返回新的 token return $this->setAuthenticationHeader($next($request), $token); } }
 

5.10.4. 測試

咱們先登錄一次 /api/v1/admin/login,將獲取到 token 攜帶訪問 /api/v1/admins/info。正常訪問。

file
 


當咱們再次請求登錄 /api/v1/admin/login,而後繼續用原 token 訪問 /api/v1/admins/info,提示錯誤。

file
 

 

user 的請自行添加,自行測試結果

 

5.11. horizon 管理異步隊列

開發中,咱們也常常須要使用異步隊列,來加快咱們的響應速度。好比發送短信,發送驗證碼等。可是隊列執行結果的成功或者失敗只能經過日誌來查看。這裏,咱們使用 horizonl 來管理異步隊列,完成登錄和刷新 token 時,將 token 存入 last_token 的由於放在異步完成。

Horizon 提供了一個漂亮的儀表盤,而且能夠經過代碼配置你的 Laravel Redis 隊列,同時它容許你輕易的監控你的隊列系統中諸如任務吞吐量,運行時間和失敗任務等關鍵指標。

 

5.11.1. 安裝

horizon 的詳細介紹能夠查看手冊

composer require laravel/horizon
 

5.11.2. 發佈配置文件

php artisan vendor:publish --provider="Laravel\Horizon\HorizonServiceProvider"
 

5.11.3. 修改隊列驅動

打開 .env 文件,將 QUEUE_CONNECTION 從 sync 改爲 redis

QUEUE_CONNECTION=redis
 

5.11.4. 儀表盤權限驗證

儀表盤不能經過接口訪問。因此咱們作驗證的時候,能夠經過指定的 IP 才能正常經過進入儀表盤。IP 能夠寫在.env 文件裏,當 IP 發生變化時進行修改。

在 .env 最後加上一行

HORIZON_IP=想經過訪問的IP地址 好比 HORIZON_IP=127.0.0.1

修改改 app/Providers/AuthServiceProvider.php 文件 裏的 boot 方法

public function boot() { $this->registerPolicies(); Horizon::auth(function($request){ if(env('APP_ENV','local') =='local'{ return true; }else{ $get_ip=$request->getClientIp(); $can_ip=en('HORIZON_IP''127.0.0.1'); return $get_ip == $can_ip; } }); }
 

5.11.5. 編寫任務類

建立一個專門負責保存 last_token 的任務類

php artisan make:job Api/SaveLastTokenJob

打開 app/Jobs/Api/SaveLastTokenJob.php 文件 ,填寫如下內容

<?php namespace App\Jobs\Api; use Illuminate\Bus\Queueable; use Illuminate\Queue\SerializesModels; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; class SaveLastTokenJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; protected $model; protected $token; /** * Create a new job instance. * * @return void */ public function __construct($model,$token) { // $this->model=$model; $this->token=$token; } /** * Execute the job. * * @return void */ public function handle() { // $this->model->last_token = $this->token; $this->model->save(); } }
 

5.11.6. 使用任務類

將控制器與中間件裏的

$user->last_token = $token; $user->save();

統一替換爲

SaveLastTokenJob::dispatch($user,$token);
 

5.11.7. 運行 Horizon

php artisan horizon

此時,進程處於阻塞狀態。
打開瀏覽器輸入 http://你的域名/horizon, 能夠看到 Horizon 儀表盤。

 

file
 

 

 

5.11.8. Supervisor 守護進程

咱們可使用 Supervisor 來守護咱們的 horizon 阻塞進程。具體方法能夠看我以前寫的文章: 安裝和使用守護進程 --Supervisor

 

5.11.9. 測試

確認 horizon 已經正常啓動。而後咱們訪問 /api/v1/admin/login 這個登錄接口。打開數據庫能夠發現,last_token 與返回結果的 token 相同。咱們也能夠再打開儀表盤,看任務完成狀況

 

file
 

 

 

5.11.10. 注意

若是修改了 job 類的源碼,須要將 horizon 從新啓動,不然代碼仍是未改動前的。(應該是 horzion 是將全部任務類常駐內存的緣由)

 

6. 成品

到此,全部修改已經所有完成,若是還有新的更改也會實時更新。同時,本文中的全部修改都已經在正式項目中運行過了。

若是你已經看完了整篇文章,知道了修改的緣由,可是不想受累本身修改一遍。我已經將修改後的上傳到全球最大的同性交友網站了,能夠直接點擊這裏直接搬走。或者複製下方的連接打開。

項目地址:

https://github.com/guaosi/Laravel_api_init

 

原文出處:https://www.guaosi.com/2019/02/26/laravel-api-initialization-preparation/    

                  https://learnku.com/articles/25947#f80eda

相關文章
相關標籤/搜索