vue2.9+laravel5.7+dingo+jwt 高效安全的先后端分离场景实践教程 (系列 1)

前言: 目前市场上,PHP好像主流在做为API开发的形式存在, 如很火的小程序、H5等,天然的,编写可靠、安全的api接口是必不可少的。 因为我目前刚入职没多久,查看了市场上的一些闭源的一些产品。 大多数都具备一个问题,散乱(接口不规范),开放(不安全),耦合度低 固然,这也是为何喜欢喷PHP的缘由, 强调快速开发,且杂乱无章的插入代码,耦合度、复用度低。 由此衍生了众多的垃圾程序员,包括我本身也存在这样的问题php

一、 Api的设计

这里,首选 RESTful , 为何 ?html

  • 强调HTTP应当以资源为中心,而且规范了资源URI的风格;前端

  • 规范了HTTP请求动做(PUT,POST等)的使用,具备对应的语义;ios

  • 遵循REST规范的Web应用将会得到下面好处:laravel

    • URL具备很强可读性的,具备自描述性;
    • 资源描述与视图的松耦合;
    • 可提供OpenAPI,便于第三方系统集成,提升互操做性;
    • 若是提供无状态的服务接口,可提升应用的水平扩展性;

简而言之,经过RESTful,增长接口代码可读性。能够更方便的经过资源(post | GET 等)来控制咱们的接口。 尽管他不是一个标准,但咱们应该向他看齐git

首先建议你们导读一下如下系列文章程序员

api资源控制,强调动词,我在干什么,我须要怎么干json

我认为一套接口应该尽可能知足如下几个原则:

  • 安全可靠,高效,易扩展。
  • 简单明了,可读性强,没有歧义。
  • API 风格统一,调用规则,传入参数和返回数据有统一的标准。

二、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 - 维护中

相关文章
相关标签/搜索