vue2.9+laravel5.7+dingo+jwt 高效安全的前後端分離場景實踐教程 (系列 1)

塵鋒發表於2019-01-24

vue2.9+laravel5.7+dingo+jwt 高效安全的前後端分離場景實踐教程 (系列 1)

前言: 目前市場上,PHP好像主流在作為API開發的形式存在, 如很火的小程式、H5等,自然的,編寫可靠、安全的api介面是必不可少的。 由於我目前剛入職沒多久,檢視了市場上的一些閉源的一些產品。 大多數都具有一個問題,散亂(介面不規範),開放(不安全),耦合度低 當然,這也是為什麼喜歡噴PHP的原因, 強調快速開發,且雜亂無章的插入程式碼,耦合度、複用度低。 由此衍生了眾多的垃圾程式設計師,包括我自己也存在這樣的問題

1、 Api的設計

這裡,首選 RESTful , 為什麼 ?

  • 強調HTTP應當以資源為中心,並且規範了資源URI的風格;

  • 規範了HTTP請求動作(PUT,POST等)的使用,具有對應的語義;

  • 遵循REST規範的Web應用將會獲得下面好處:

    • URL具有很強可讀性的,具有自描述性;
    • 資源描述與檢視的鬆耦合;
    • 可提供OpenAPI,便於第三方系統整合,提高互操作性;
    • 如果提供無狀態的服務介面,可提高應用的水平擴充套件性;

簡而言之,通過RESTful,增加介面程式碼可讀性。可以更方便的通過資源(post | GET 等)來控制我們的介面。 儘管他不是一個標準,但我們應該向他看齊

首先建議大家導讀一下以下系列文章

api資源控制,強調動詞,我在幹什麼,我需要怎麼幹

我認為一套介面應該儘量滿足以下幾個原則:

  • 安全可靠,高效,易擴充套件。
  • 簡單明瞭,可讀性強,沒有歧義。
  • API 風格統一,呼叫規則,傳入引數和返回資料有統一的標準。

2、dingo Api

Laravel的場景中,其實自5.5版本迭代之後, 自帶的 response 足以滿足我們的需要

dingo Api 目前還沒有穩定版本, 建議大家自我斟酌

使用 dingo Api 大概有以下幾個好處

  • 版本控制更方便(封裝了一系列的方法供你使用) - 不要重複的造輪子

  • 響應設定更加隨心所欲

  • 神奇的 Transformers (提高耦合度, 複用程式碼率簡直直線上升)

  • 節流設定

  • 異常處理管理

實踐的話,當然開始我們的實踐之旅啦

導讀: laravel-china.org/docs/larave… |

安裝

我們採用的是 vagrant + Homestead + composer 的本地環境 | 專案包這裡忽略,自行安裝

  1. 採用 laravel_china 中國映象

     composer config repo.packagist composer https://packagist.laravel-china.org
    複製程式碼
  2. 安裝包

     composer require dingo/api:^2.0.0-alpha2
    
     composer require liyu/dingo-serializer-switch
    複製程式碼
  3. 釋出

     php artisan vendor:publish --provider="Dingo\Api\Provider\LaravelServiceProvider"
    
     # 詳細的相關配置,這裡不做介紹
    
     API_STANDARDS_TREE=vnd # 專案環境
     API_VERSION=v1 # 版本號
     API_NAME="My API" # 專案名稱
     API_STRICT=false # 是否開啟嚴格模式【嚴格模式要求客戶端傳送 Accept 頭】
     API_DEFAULT_FORMAT=json # 響應格式
    複製程式碼
  4. 開始寫程式碼

  • api.php 【僅供參考】

      $api = app('Dingo\Api\Routing\Router');
    
      $api->version('v1',[
          'namespace' => 'App\Api\Controllers',
          'middleware' => ['serializer:array','bindings'],
          'name' => 'api.'
      ], function ($api) {
          $api->post('/login','AuthController@login')->name('login'); # 登陸api
          $api->post('/user' ,'AuthController@index')->name('user'); # 獲取使用者的資訊
          $api->get('/articles' ,'ArticleController@list')->name('articles'); # 獲取文章列表
          $api->get('/article/{id}' ,'ArticleController@detail')->name('article.detail'); # 獲取文章詳細資訊
          $api->get('/keywords' ,'KeywordController@list')->name('keywords'); # 獲取標籤列表
          $api->get('/keyword/{keyword}' ,'KeywordController@detail')->name('keyword.detail'); # 獲取標籤下的文章列表
    
          $api->group(['middleware' => ['jwt.auth','bindings']], function ($api) {
              $api->patch('/reply/{reply}', 'ReplyController@edit')->name('reply.edit'); # 修改評論的狀態
              $api->delete('/reply/{reply}', 'ReplyController@delete')->name('reply.delete'); # 刪除評論的狀態
              $api->put('/reply/batch', 'ReplyController@batch')->name('reply.batch'); # 批量修改評論的狀態
              $api->delete('/reply/batch', 'ReplyController@deleteBybatch')->name('reply.deleteBybatch'); # 批量刪除評論的狀態
              $api->post('/refresh', 'AuthController@refresh')->name('refresh.token'); # 重新整理token
              $api->post('/logout', 'AuthController@logout')->name('logout'); # 登出
              $api->get('/todolists', 'UserController@todolists')->name('todolists'); # 檢視我的todolists
              $api->get('/replies', 'ReplyController@list')->name('replies'); # 獲取評論列表
              $api->post('/index', 'IndexController@index')->name('index'); # 獲取儀表盤相關資料
          });
      });
    複製程式碼

serializer:array 需安裝 composer require liyu/dingo-serializer-switch ,當渲染資料到前端的時候,會預設的加data = {} , 安裝此項東西可以減輕一些前端的麻煩

bindings 中介軟體由於被dingo接管了, 所以如果使用模型繫結的話, 需加入 bindings 這個中介軟體

  • 響應方法

      在控制器中需繼承 `Helpers`
    
      namespace App\Api\Controllers;
    
      use Dingo\Api\Routing\Helpers;
      use App\Http\Controllers\Controller;
    
      class BaseController extends Controller
      {
          use Helpers;
      }
    
      # 方法
      // 一個自定義訊息和狀態碼的普通錯誤。
      return $this->response->error('This is an error.', 404);
    
      // 一個沒有找到資源的錯誤,第一個引數可以傳遞自定義訊息。
      return $this->response->errorNotFound();
    
      // 一個 bad request 錯誤,第一個引數可以傳遞自定義訊息。
      return $this->response->errorBadRequest();
    
      // 一個伺服器拒絕錯誤,第一個引數可以傳遞自定義訊息。
      return $this->response->errorForbidden();
    
      // 一個內部錯誤,第一個引數可以傳遞自定義訊息。
      return $this->response->errorInternal();
    
      // 一個未認證錯誤,第一個引數可以傳遞自定義訊息。
      return $this->response->errorUnauthorized();
    
      // 新增額外的頭資訊
      return $this->response->item($user, new UserTransformer)->withHeader('X-Foo', 'Bar')
    
      // 新增 Meta 資訊
      return $this->response->item($user, new UserTransformer)->addMeta('foo', 'bar');
    
      // 設定響應狀態碼
      return $this->response->item($user, new UserTransformer)->setStatusCode(200);
    複製程式碼

我們這裡追求實踐,暫時忽略相關的原理性問題

  • 響應生成器

這個覺得是好東西,首先我的目錄結構如下

file

檢視 文章的控制器

# ArticleController.php
....
namespace App\Api\Controllers;

use App\Api\Transformers\ArticleTransformer;
use App\Model\Article;

class ArticleController extends BaseController
{
    /**
     * 獲取文章列表
     */
    public function list()
    {
        $articles = Article::query()->orderByDesc('created_at')->get();

        return $this->response->collection($articles, new ArticleTransformer());
    }

    public function detail($id)
    {
        if( !$article = Article::find($id) ) {
            return $this->response->errorNotFound('文章未找到或者已刪除');
        }

        return $this->response->item($article, new ArticleTransformer());
    }
}

# ArticleTransformer.php

...
namespace App\Api\Transformers;

use App\Model\Article;
use Carbon\Carbon;
use League\Fractal\TransformerAbstract;

class ArticleTransformer extends TransformerAbstract
{
    protected $availableIncludes = ['keywords', 'category'];

    public function transform(Article $article)
    {
        return [
            'id' => $article->id,
            'title' => $article->title,
            'body' => $article->body,
            'category_id' => $article->category_id,
            'readCount' => $article->readCount,
            'create_time' => Carbon::make($article->created_at)->toDateTimeString()
        ];
    }

    /**
     * 包含文章標籤欄位
     */
    public function includeKeywords(Article $article)
    {
        return $this->collection($article->keywords, new KeywordsTransformer());
    }

    public function includeCategory(Article $article)
    {
        return $this->item($article->category, new CategoryTransformer());
    }
}

# Article.php

amespace App\Model;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Article extends Model
{
    use SoftDeletes;

    /**
     * 檢視文章對應的標籤 遠端多對多
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function keywords()
    {
        return $this->hasManyThrough(Keyword::class, ArticleKeyword::class, 'article_id','id');
    }
.....
複製程式碼

有沒有發現,程式碼輕量很多? transform 傳遞對應的模型,資料 。他會自動根據你對應的 collection 【集合】 或者 item 【單個模型】來返回你所需要的資料

其中 collectionitem 需要注意的地方。 【我在這裡吃了點虧】

collection 是一個集合,當操作返回的是多個資料的時候使用它, 例如

$articles = Article::all()
$this->collection($articles, new ArticleTransformer());
複製程式碼

如果這裡使用item則會報一個error級別的錯誤

關於 include ,必須繼承 TransformerAbstract 且 使用 $availableIncludes

如上面中對於的介面為

http://surest.test/api/articles

那麼我們產生的資料如下

file

http://surest.test/api/articles?include=category,keywords

file

對吧,一目瞭然, 通過 include 想要什麼資料,就拿什麼資料, 而且 通過 ArticleTransformer , 你同時可以在如何地方,重複使用這一套程式碼。

而無需在進行編輯,或者為了對應某個介面或者要求而去寫程式碼 , 直接 include 了事

  • 返回資料也是如此

當我們規定了相關的響應引數的時候, 直接使用 $this->response 即可

# example
# 會丟擲一個 404 錯誤,可以參看原始碼, 很簡單
return $this->response->errorNotFound();
複製程式碼
  • 節流設定

    當我們需要防止某個介面重複的被使用,例如常見的攻擊之類的, 可以有效的預防

    Kernel.php 中 設定

      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,
          ],
    
          # 新增如下
          'api' => [
              'throttle:60,1',
              'bindings',
          ],
      ];
    複製程式碼

由此, 簡單的dingoapi操作完美成功了, 基本上能應付日常的需求, 如更深層次的,可以參考dingapi文件laravel-china.org/docs/dingo-…

文章部分更新【加強版】

關於這一段程式碼的修改版

    public function detail($id)
    {
        if( !$article = Article::find($id) ) {
            return $this->response->errorNotFound('文章未找到或者已刪除');
        }

        return $this->response->item($article, new ArticleTransformer());
    }
複製程式碼

如上, 在 laravel 中似乎可以找到更好的辦法來進行替代他, findOrFail - findOrFail , 檢視官方的api得知,它會丟擲一個 ModelNotFoundException 錯誤。

回到上面, 說到, 由於dingo接管了laravel自帶的錯誤訊息, 我們可以這樣使用

# AppServiceProvider.php
....
class AppServiceProvider extends ServiceProvider
{
    ...
    public function register()
    {
        \API::error(function (\Illuminate\Database\Eloquent\ModelNotFoundException $exception) {
            abort('404','模型未找到');
        });
    }   
}
複製程式碼

如上, 才可以使用我們的 findOrFail 。 但是,在對文章的增刪改查中我們發現,大量運用到了檢查文章是否存在的程式碼塊, 我們來優化一下程式碼, 可以這樣使用

建立一箇中介軟體

php artisan make:middleware FilterArticle

# FilterArticle.php
...
use Dingo\Api\Routing\Helpers;
..
class FilterArticle
{
    use Helpers;
    /**
    * Handle an incoming request.
    *
    * @param  \Illuminate\Http\Request  $request
    * @param  \Closure  $next
    * @return mixed
    */
    public function handle($request, Closure $next)
    {
        if( $aid = $request->id ) {
            if( Article::find($aid) ) {
                return $next($request);
            }else{
                return $this->response->errorNotFound('文章未找到');
            }
        }else{
            return $this->response->errorNotFound('引數錯誤');
        }
    }

    ...
}

# 註冊中介軟體 | Kernel.php

protected $routeMiddleware = [
    'auth' => \App\Http\Middleware\Authenticate::class,
    .....
    'article.check' => FilterArticle::class
];

# ArticleController.php

class ArticleController extends BaseController
{
    public function __construct()
    {
        $this->middleware('article.check',
            ['only' => ['edit','create','delete','detail']]);
    }
    .....
}
複製程式碼

由此,我們的程式碼將變成這樣

/**
 * 檢視文章詳情
 * @param $id
 * @return \Dingo\Api\Http\Response|void
 */
public function detail(Article $article)
{
    return $this->response->item($article, new ArticleTransformer());
}
複製程式碼

怎麼樣, 爽不爽呢~~, 使用 Article::find($aid) 的原因, 是因為能夠更加定製化的看到自己的錯誤原因。 更加通俗易懂,當然,findOrFail 也是很好滴

下期預告: 將介紹

  • JWT 的安裝使用、原理、最佳操作方法等

  • VUE 下如何實現token的讀取變化重新整理, 無狀態變化token 、OAuth模式等

  • VUE 下 axios 攔截器 、 Promise餓了麼元件的 的部分應用場景

: 面朝大海,春暖花開 | 一切以新手角度出發,講一些文件你不知道的應用場景

我的部落格: surest.cn - 維護中

相關文章