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 等)来控制我们的接口。 尽管他不是一个标准,但我们应该向他看齐
首先建议大家导读一下以下系列文章
(RESTful API 最佳实践 阮一峰)[www.ruanyifeng.com/blog/2018/1…]
(Github 的 Restful HTTP API 设计分解)[laravel-china.org/courses/lar…]
api资源控制,强调动词,我在干什么,我需要怎么干
我认为一套接口应该尽量满足以下几个原则:
- 安全可靠,高效,易扩展。
- 简单明了,可读性强,没有歧义。
- API 风格统一,调用规则,传入参数和返回数据有统一的标准。
2、dingo Api
在 Laravel
的场景中,其实自5.5版本迭代之后, 自带的 response
足以满足我们的需要
dingo Api 目前还没有稳定版本, 建议大家自我斟酌
使用 dingo Api
大概有以下几个好处
版本控制更方便(封装了一系列的方法供你使用) - 不要重复的造轮子
响应设置更加随心所欲
神奇的
Transformers
(提高耦合度, 复用代码率简直直线上升)节流设置
异常处理管理
实践的话,当然开始我们的实践之旅啦
导读: laravel-china.org/docs/larave… |
安装
我们采用的是 vagrant + Homestead + composer
的本地环境 | 项目包这里忽略,自行安装
采用
laravel_china
中国镜像composer config repo.packagist composer https://packagist.laravel-china.org 复制代码
安装包
composer require dingo/api:^2.0.0-alpha2 composer require liyu/dingo-serializer-switch 复制代码
发布
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 # 响应格式 复制代码
开始写代码
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); 复制代码
我们这里追求实践,暂时忽略相关的原理性问题
- 响应生成器
这个觉得是好东西,首先我的目录结构如下
">
查看 文章的控制器
# 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 【单个模型】来返回你所需要的数据
其中 collection
和 item
需要注意的地方。 【我在这里吃了点亏】
collection
是一个集合,当操作返回的是多个数据的时候使用它, 例如
$articles = Article::all()
$this->collection($articles, new ArticleTransformer());
复制代码
如果这里使用item
则会报一个error
级别的错误
关于 include
,必须继承 TransformerAbstract
且 使用 $availableIncludes
如上面中对于的接口为
http://surest.test/api/articles
那么我们产生的数据如下
http://surest.test/api/articles?include=category,keywords
对吧,一目了然, 通过 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
本文由 邓尘锋 创作,采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: May 4, 2019 at 11:37 am