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

相关文章
相关标签/搜索