手摸手教你让 Laravel 开发 API 更得心应手

本文搬运自我自己的博客

1. 起因

   随着前后端完全分离,`PHP`也基本告别了`view`模板嵌套开发,转而专门写资源接口。`Laravel`PHP框架中最优雅的框架,国内也越来越多人告别`ThinkPHP`选择了`Laravel``Laravel`框架本身对`API`有支持,但是感觉再工作中还是需要再做一些处理。`Lumen`用起来不顺手,有些包不能很好地支持。所以,将`Laravel`框架进行一些配置处理,让其在开发`API`时更得心应手。

   当然,你也可以点击[这里](https://learnku.com/articles/25947#9ea6b3),直接跳到成果~

2. 准备工作

2.1. 环境

PHP > 7.1
MySQL > 5.5
Redis > 2.8

2.2. 工具

postman
composer

2.3. 使用postman

为了模拟AJAX请求,请将 header头 设置X-Requested-WithXMLHttpRequest

file

2.4. 安装Laravel

Laravel只要>=5.5皆可,这里采用文章编写时最新的5.7版本

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目录下,如果数据表多的话,这里模型文件就会很多,不便于管理,所以我们先要将模型文件移动到其他文件夹内。

1) 在app目录下新建Models文件夹,然后将User.php文件移动进来。
2) 修改User.php的内容

<?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个文件

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

3.2. 控制器

因为是专门做API的,所以我们要把是API的控制器都放到app\Http\Controllers\Api目录下。

使用命令行创建控制器

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": [
            "密码不能为空"
        ]
    }
}

则证明验证失败。

然后验证是否可以正常登录。因为我们认证的字段是namepassword,而Laravel默认认证的是emailpassword。所以我们还要打开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 代表冻结。但是如果一个月后再来看这行代码,早已经忘记了 -11 具体表示的含义。

  1. 参考上面UserResource.php编写时,判断status具体状态函数,我们是使用switch语句。这样太不美观,而且地方用多了还容易冗余,每次编写都需要去查看每个数字代表的具体意思。

5.4.2. 解决思路

  1. 第一个问题:为什么一段时间后再看就不知道-11 具体表示的含义?

    这是因为单纯的数字没有解释说明的作用,变量以及函数这些具有解释说明的作用,可以让我们立刻知道具体含义。
  2. 第二个问题:如何给一个数字就能直接知道它代表的含义?

    提供一个函数,返回这个数字代表的具体含义。

而这些,都可以使用Enum枚举可以解决。

5.4.3. 注意

PHPLaravel框架本身是不支持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
我们再把设置成ajaxheader头去掉
file

报错非常详细,并且把我们隐私设置都暴露出来了,这是由于我们.envAPP_DEBUGtrue状态。我们不希望这些信息被其他访问者看到。我们改为false,再请求看看结果。

file

嗯。很好,不仅别人看不到了,连我们自己都看不到了

5.5.2. 需求

  1. 所有的异常信息都以统一json格式输出
  2. 因为我们是开发者,并且.env文件默认是不加入git上传线上的,我们希望可以当APP_DEBUGtrue(本地)的时候可以继续显示详细的错误信息,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. 测试

继续打开设置AJAXheader

1.关闭APP_DEBUG,请求刚刚故意错误的接口。
file
2.开启APP_DEBUG,请求刚刚故意错误的接口。
file
3.请求一个不存在的路由,查看返回结果。
file

其他的异常显示,自行测试啦~

5.6. jwt-auth

在传统web中,我们一般是使用session来判定一个用户的登陆状态。而在API开发中,我们使用的是tokenjwt-tokenLaravel开发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.我们在PostmanHeader头部分再加一个keyAuthorizationvalue为登陆成功后返回的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": 422,
    "message": "未登录或登录状态失效"
}

3.现在,我们再做一个如果token过期了,但是刷新限制没有过期的情况,看看会有什么结果。我们先将config/jwt.php里的ttl60改成1。意味着重新生成的token将会1分钟后过期。

然后我们重新登录获取到token,替换/api/v1/users/info原有的token,进行访问,可以正常返回用户的信息。

等过了一分钟,我们再进行访问,发现依旧可以返回用户信息,但是我们在返回的HeadersAuthorization可以看到新的token
file
此时如果我们再次访问,则报出异常

{
    "status": "error",
    "code": 422,
    "message": "未登录或登录状态失效"
}

我们替换上新的token,再次访问,访问正常通过。

4.现在,我们接着继续做token和刷新时间都过期的情况,会发生什么。我们再将config/jwt.php里的refresh_ttl20160改成2

重新按照3步骤执行一次,当刚过一分钟时,返回结果与3相同,都是正常返回信息并且在Headers携带了新的token。

当2分钟过后,报如下错误信息。

{
    "status": "error",
    "code": 422,
    "message": "未登录或登录状态失效"
}

5.为了后面的方便,我们将修改的ttlrefresh_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管理异步队列

开发中,我们也经常需要使用异步队列,来加快我们的响应速度。比如发送短信,发送验证码等。但是队列执行结果的成功或者失败只能通过日志来查看。这里,我们使用horizon来管理异步队列,完成登陆和刷新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_CONNECTIONsync改成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=env('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. 成品

到此,所有修改已经全部完成,如果还有新的更改也会实时更新。同时,本文中的所有修改都已经在正式项目中运行过了。

如果你已经看完了整篇文章,知道了修改的原因,但是不想受累自己修改一遍。我已经将修改后的上传到全球最大的同性交友网站了,可以直接点击这里直接搬走。或者复制下方的链接打开。

项目地址:

github.com/guaosi/Laravel_api_init

本作品采用《CC 协议》,转载必须注明作者和本文链接
附言 1  ·  4年前

2019/03/23
根据overtrue的提议,删除了部分无用的判断代码。感谢大家的建议~

本帖由系统于 4年前 自动加精
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 161

@guaosi Laravel 的大部分操作,基本都是以异常形式处理,所以不需要 if else 的。

4年前 评论
if($user){
   return '用户注册成功。。。';
}
return '用户注册失败。。。';

我猜你的代码永远都进不到 "用户注册失败" 里。

4年前 评论
medz

:+1:

4年前 评论

标题亮了 :joy: 结语还有彩蛋

4年前 评论

@rachel :stuck_out_tongue_winking_eye:幽默一点才有人看

4年前 评论
if($user){
   return '用户注册成功。。。';
}
return '用户注册失败。。。';

我猜你的代码永远都进不到 "用户注册失败" 里。

4年前 评论

@overtrue :joy:确实是走不到,写的时候没多想,感觉有成功就要写个失败

4年前 评论

@memory2018 :unamused: :unamused: :unamused:

4年前 评论

老哥写的东西,基本都在项目中处理了,总结的可以。收藏下,省的自己总结了。

4年前 评论

@guaosi 我来看串号的问题了。

4年前 评论

@guaosi Laravel 的大部分操作,基本都是以异常形式处理,所以不需要 if else 的。

4年前 评论

@overtrue 受教了,稍后就改优雅~

4年前 评论

插个眼 :+1:

4年前 评论

@overtrue 能否举个栗子,菜机的我很多地方还是用 if else 的!

4年前 评论

@JINJIALEI 因为Laravel大部分如果出错都是抛出异常,比如我上面的创建新用户,要么成功要么直接抛出异常,走不到注册失败

4年前 评论

@JINJIALEI 周末抽个空改一下

4年前 评论

验证器那里的方法 authorize 不是用来做权限验证的吗。怎么变成开启和不开启了?

4年前 评论

@fengzi91 这个注释确实是错误的,稍后修正,感谢 :sweat_smile: :sweat_smile:

4年前 评论

@guaosi 那就等你改优雅了,我再来看

4年前 评论

小白问一下,这样用HTTP状态码做为程序运行结果状态合适吗?

4年前 评论

@隔壁老王 认为合适就合适。如果感觉不合适,想内部定义自己的码。那应该是想加自己的错误码比如10001,此时'$this->failed()'可以传入第三个参数,也就是自己想定义的错误码。

4年前 评论
__yu

手摸手系列~~

4年前 评论

关于多角色认证这里 我也踩了一些坑,我现在加上laravel自带中间件 auth:admin auth:api 就可以了,可以守护不同的 guard

4年前 评论

@guaosi 问一下 比如我写了一个发短信的方法,这个方法如果短信发送失败需要告诉用户,正常我会在这个方法里返回一个 status 和一个 message,调用的地方根据返回的状态去判断有没有发送成功,这只是我举的一个例子,我想说的是很多方法都是这么去处理当前方法的执行成功的状态吗?有没有更好的写法?

4年前 评论

@JINJIALEI 一般在我们在成功是不会做过多的自定义码来区分,200,201,202已经可以很好表示成功了。只有失败才会自定义内部的码,用来给前端反馈具体错误。

4年前 评论
llys
public function setStatusCode($statusCode,$httpCode=null)
{
        $httpCode = $httpCode ?? $statusCode;
        $this->statusCode = $statusCode;
        return $this;
}

这个$httpCode是不是没用到啊

4年前 评论

@llys 嗯,对的,确实没有用到。原来我是想做一些其他的事情,后面忘记了,不过不影响使用哈。

4年前 评论
llys

use Response;
这个是引用的哪个 我找了半天没找到这个命名空间额 我比较小白 :see_no_evil: :see_no_evil:

4年前 评论

@llys 来自于Facedes,完整为\Illuminate\Support\Facades\Response.文档参考这里

4年前 评论
llys

@guaosi 好的 感谢 :grinning:

4年前 评论

有不少启发,很不错,指出一点小问题:

  • 5.5.1 中,Laravel 本身异常处理,会对 Ajax 需要返回 JSON 格式的请求返回 JSON,可以看下源码
  • 5.5.4 代码中有用到 env 函数,是错误的。env 函数应只出现在 config 文件中。项目上线执行 php artisan config:cache 命令对配置进行缓存以后,env 函数取不到值
4年前 评论
王举

resource 也是可以加到返回一起的。然后还加了debugbar的api debug返回。
https://github.com/wowiwj/story-server/blo...

后面实际开发过程发现到处use这个Trait还是挺冗余的,搞了一个全局方法
https://github.com/wowiwj/story-server/blo...

return api()->resource($resource);

...

return api()->success("done");

当然实际看个人ai'hao

我的小本儿打开这个网页居然卡死了,呃eee。。。

4年前 评论

感谢楼主的认真分享,对项目有很大的帮助

4年前 评论

@王举 大佬,进不去啊,是不是做得私仓.进去就是404.
貌似用mac,评论都会很卡

4年前 评论
王举

@guaosi 谢谢提醒,公开啦哈哈

4年前 评论

注册错误信息不能返回,直接跳转到首页去了,验证器没有起到作用,这是什么情况?@guaosi

4年前 评论

像这种 Auth::guard('web')->attempt([]) 方法都怎么追踪?attempt

4年前 评论

首先感谢作者的成果,通过你的文章,使我初步了解了Laravel的开发方式。
只是教程中踩到了几个坑,比如展示的代码里没有写明use哪些文件,导致报错。如果能写的更加详细,这将是Laravel小白的终极教程。在此衷心感谢。
一个从TP奔向Laravel的小白!!!!

4年前 评论

@ww520zln 有些use本来就是laravel故意可以不写命名空间。它的包路径查找跟tp不一样,所以没办法很详细写出来对应位置在哪里。你可以直接下载做好的进行对比研究

4年前 评论

@xingxiaoli 看下文档的用户认证

4年前 评论

@guaosi 我就是通过对比你的包进行修改的。问题基本都解决了,就是有个问题想请教下,API认证为什么不选择Laravel Passport呢?请指教下!谢谢!

4年前 评论

没有jwt-auth好用,就这个原因

4年前 评论

写的很详细, :+1: :+1: :+1:

但是我这边有一个疑问:无感刷新token问题

如果我一个页面有多个请求(异步)的时候,此时所有请求都是带已过期的token访问。

第一个请求是会通过并返回新的token,但是其他请求是用过期token访问,此时返回的是401,token过期。

这样还是做不到无感刷新啊?@guaosi 你有什么好的解决办法吗

4年前 评论

@cyd622 暂时没有更好的办法,只能重登了

4年前 评论

@cyd622 @guaosi 配置blacklist_grace_period参数,允许过期token在一定时间内访问,这个就能解决并发请求token失效的问题

4年前 评论

@tegic @guaosi ok_hand: :grin: 没仔细看jwt配置文件,这个参数是可以解决并发 :+1:

4年前 评论

@tegic 感谢,学习了 :+1:

4年前 评论

@guaosi 请教下,我把数据表name字段改成了account之后,注册时候报错:" General error: 1364 Field 'account' doesn't have a default value (SQL: insert into users (password, updated_at, created_at) values (123456, 2019-04-03 15:13:47, 2019-04-03 15:13:47))", 这个是什么原因?

4年前 评论

@ww520zln 因为我用的是orm中的create方法,这个会对应到相应的模型文件,去寻找$fillable 所允许的字段

4年前 评论

@cyd622 我看了也想说这个。但是看都后面有人说了。我看到过一个解决的就是,以过期token为key缓存起来,设置过期时间,在过期时间内,旧的token,正常通过。 :grin:

4年前 评论

@guaosi 弱弱问下大哥

Route::get('/users','UserController@index')->name('users.index');里面最后的

name('users.index');是什么意思?

4年前 评论

@汪阿浠 给路由起别名,用在render比较多,具体可以看文档

4年前 评论

@guaosi 我也碰到一样的问题,新注册用户失败直接跳到laravel首页上去了,这个要如何修改呢?我尝试在User::create($request->all()) 这个地方try也没用,请指教,你回复的2.3是啥?你这教程2.3是postman.....

4年前 评论

看得出来楼主是实战派的,辛苦了,建议用官方的 Passport 再出一个版本,支持应该会更好

Ps. Enum 建议用这个包 https://github.com/BenSampo/laravel-enum

4年前 评论

@zhuxiacool 首先,你应该先阅读一下文档这里,为什么会出现这个情况.然后才会知道为什么我要这样进行解决。

file

4年前 评论

@陈伯乐 哈,我不知道还真的有enum这个包,有机会尝试一下,感谢

4年前 评论

@zhuxiacool 我也是直接进到了首页的情况,你现在解决了吗?用的什么方法?

4年前 评论

if ($this->auth->parseToken()->authenticate()) {
return $next($request);
}
throw new UnauthorizedHttpException('jwt-auth', '未登录');
这一步的时候,用了一个不合法的token,为什么我这里返回的是500错误;查了下日志 如下:
local.ERROR: Could not decode token: Error while decoding to JSON: Syntax error {"exception":"[object] (Tymon\JWTAuth\Exceptions\TokenInvalidException(code: 0): Could not decode token: Error while decoding to JSON: Syntax error at /www/laravel/vendor/tymon/jwt-auth/src/Providers/JWT/Lcobucci.php:133, RuntimeException(code: 0): Error while decoding to JSON: Syntax error at /www/laravel/vendor/lcobucci/jwt/src/Parsing/Decoder.php:36)
[stacktrace]
大佬看看是什么原因,谢谢。

4年前 评论

@Kalamet 确认真的有看我开头写的要求做了吗?

file

4年前 评论

有绑定模型的路由,同路径的路由需要放在没绑定路由的后面!!!
感觉肯定有小伙伴碰到:该模型未找到的报错,将 Handel中render 方法的 $exception 打印出来 也就是

No query results for model [App\Post]. 的报错!

建议博主在添加此条api路由时多啰嗦一句,这样可能对小白用户更友好一点

Route::get('/users/info','UserController@info')->name('users.info');
4年前 评论

@suixinerle 确实是如果将

Route::get('/users/{user}', 'UserController@show')->name('users.show');

放在了

Route::get('/users/info', 'UserController@info')->name('users.info');

前面的话,请求/users/info会提示

No query results for model [App\Post].

原来测试的时候没有想过路由冲突的问题。通过更换路由先后顺序确实是可以解决这个情况。
但是需要这样解决,太无法体现laravel的优雅了。我们可以用上路由提供的正则表达式来很好解决这个问题。

//用户信息
Route::get('/users/{user}', 'UserController@show')->where('user', '[0-9]+')->name('users.show');
4年前 评论
maliao

兄dei,一直卡在FormRequest这里,验证不通过会重定向到首页去了,要怎样才能返回出下面的错误信息

    public function messages()
    {
        return [
            'username.required' => '用户名不能为空'
        ];
    }
4年前 评论

@maliao 首先,你应该先阅读一下文档这里,为什么会出现这个情况。然后才会知道为什么我要这样进行解决。

file

4年前 评论
maliao

@guaosi 我就是按照你那样子做的啊,就一直重定向到首页去,不会返回出错误信息啊

4年前 评论
maliao

终于解决了,需要在FormRequest.php文件添加下面方法

    use Illuminate\Http\Exceptions\HttpResponseException;
    ......

    protected function failedValidation(Validator $validator) {
        $error= $validator->errors()->all();
        throw new HttpResponseException(
            response()->json(['status'=>'error','code'=>10000,'message'=>$error[0]])
        );
    }
4年前 评论

@maliao
1、看下post的hader头部分是否设置了ajax访问字段
图片

2、可以克隆一下成品, 看看是否还会出现一样的问题。跳回首页的问题我一开始也遇到过,是因为如果不是ajax访问,laravel是默认退回上一页,如果上一页不存在则跳回首页

4年前 评论
怎样的心 3年前

@tegic @guaosi 如果通过配置blacklist_grace_period来解决并发请求token的问题,那么会出现单点登录失效的问题。比如我设置60s,那么在设定时间内,使用新旧两个token都能通过验证

4年前 评论

@guaosi 刷新token不返回

file

4年前 评论
zpers 4年前
g-sabo 3年前

@不负韶华。 http code 422表示已经返回了。检查你的cors的字段是否允许'expose-headers' => ['Authorization'],

4年前 评论

@guaosi

file 我捕获异常一直抛出的是

file
而不是 token过期的异常 我觉得很纳闷

4年前 评论

@guaosi 不错的文章受益匪浅 发自内心的感谢 :+1:

4年前 评论

文章不错,手摸手就过分了

4年前 评论

@guaosi 大佬 学了你文章 我加薪了3k

4年前 评论

JWT 使用attempt()方法来验证获取token,token返回的一直是FALSE,自己在数据库查询,然后用login()方法正常返回token这是啥原因?大佬。

4年前 评论
kcersing 4年前

file创建用户的时候出的异常,萌新探路,请高手指教

4年前 评论

定义多用户的时候admin 无效,怎么解决

4年前 评论

@2827717649 方法不被允许,我刚才也碰到了,你检查下你的路由配置那里,应该把这两个路由放进 group里面。

Route::namespace('Api')->prefix('v1')->group(function () {
    Route::get('/users','UserController@index')->name('users.index');
    Route::post('/users','UserController@store')->name('users.store');
    Route::post('/login','UserController@login')->name('users.login');
});
4年前 评论
Toiu

@overtrue :grin:早期偷学了超哥的一招abort(400, '我抛出了一个异常')+ 控制器直接 return UserResource::collection($users); 炒鸡顺手

4年前 评论

授权动作的时候, 如果token 过期了, 会进行一次性登陆 token会刷新返回在header里, 但是这样的话policy就不能执行成功了, 是因为一次性登陆的原因吗.

4年前 评论

file
这是 token失效后捕获到的错误

4年前 评论

@guaosi 在 5.5.3. 创建自定义异常处理中
file
有两个array_first() 重复了

4年前 评论

好文章,自己一定从头来一遍

4年前 评论

用户注册方法那里需要进行修改,要不然密码在数据库是明文保存的,而且会导致后面的 login 方法报用户名或密码错误的错误。
修改代码如下:

//用户注册
    public function store(UserRequest $request)
    {
        $cred = [
            'name' => $request->name,
            'password' => bcrypt($request->password),
        ];
        User::create($cred);
        return $this->setStatusCode(201)->success('用户注册成功');
    }
4年前 评论

@Stone007 我用了修改器喔。可以看步骤3.1位置。

    //将密码进行加密
    public function setPasswordAttribute($value)
    {
        $this->attributes['password'] = bcrypt($value);
    }

修改器说明:修改器《Laravel 5.8 中文文档》

4年前 评论

返回正确信息 是用message 方法吧, 用success返回信息 格式就变成 {status:success, code:200, data:"用户登录成功..."}

5.2.4. 如何使用

     得益于前面统一消息的封装,使用起来非常容易

  1. 返回正确信息
    return $this->message('用户登录成功...');
4年前 评论

@wowow api里成功不仅仅是要文字显示成功,更重要的是http状态码

4年前 评论
wowow 4年前

异步请求的时候,token过期了,会有问题呀@guaosi

4年前 评论

请问下,使用 Api-Resource 资源 返回的 单一用户 有code、status,但是返回列表就没有了。这个返回的格式统一的话,需要怎么处理?

file

单一用户
file

列表用户

file

4年前 评论
bing 3年前

@gyp719 抱歉哈,工作忙,有些东西一直没处理。这个问题是已知的,不过已经有大佬给出了解决方案

4年前 评论

非常感谢!

4年前 评论

现在的postman 更新了,在新版的postman 中无法 显示 这个header 头信息了

file

有其他什么办法可以获取吗?

4年前 评论

亲 我也是tp转laravel的小白 我想问一下我按照教程来的 为什么我注册的时候会报这个错误呢?

file

4年前 评论
Janpun 4年前

有一个问题,加入黑名单的token是存储到哪里的?如果服务端存储了token(哪怕是标示了不可使用的token),那jwt就变成有状态的了,那和session不就一样了嘛,所以想问在这里还有没有其他解决方案?

4年前 评论

感谢分享。一个小建议:代码做一下格式化以及空格处理。

4年前 评论

我想问一下,封装统一返回消息格式后。如果一直返回200是没问题的。但我看你有时候返回201那这个状态码有可能改变。那让前端如何判断此次请求是成功的呢。

4年前 评论

我在测试的时候发现在token的bearer前面加若干字符串,token也能通过验证。查laravel源码,在验证时有用Str::startWith()方法判断是否是已bearer开头的,这和测试情况还是不符。请问是哪里出的问题。

file

4年前 评论

跟着这篇文章做了两遍,收获很大, 十分感谢。真的是从tp ci转的laravel,现在还是laravel小白。 请教一下,怎么样学习laravel可以少走弯路。 :joy:只是跟着做了,但是不知道那些composer引入的类的实现原理,心里就有点不是很得劲的感觉。

4年前 评论
gyp719 4年前
不负韶华。 4年前
softer 3年前

file

您好,我是小白照著您的步驟在第2次輸入username以及password總是顯現出這樣的畫面,沒辦法順利跑出"用戶登入成功",不太確定是哪邊出了問題

4年前 评论

laravel 5.8版本
JWT 1.0.0-rc.4.1
为什么我登录之后返回token,使用info接口不加header有信息返回但是获取不到数据,jwt验证没有生效?

4年前 评论

5.6.3. 4 输入info接口之前应该先加上middle,不然还是会直接进入路由。

4年前 评论

感谢大佬辛勤付出,别的地方看到推荐进来的,上完班晚上回去研究,奥利给

4年前 评论

@gyp719 用户列表 返回统一格式问题解决了吗?

4年前 评论

这是个什么错?求大佬解答

file

3年前 评论
ZLSN (作者) 3年前

@ZLSN

file

不是自带的么,还需要自己写?

3年前 评论
a512395193 3年前
ZLSN 3年前
a512395193 3年前
ZLSN 3年前
ZLSN 3年前

膜拜大佬,祝大佬晚上约炮成功

3年前 评论

file

用auth:admin是正确的,说明jwt是对的。但是用admin.refresh就一直提示“未登录或者登录状态失效”,请问下可能是什么原因啊?我猜测是RefreshAdminTokenMiddleware的问题,但不知道具体原因。 file 补充下:我的表结构和表名称与文章中不同,主键pid字符串类型。

3年前 评论

@g-sabo 我升级了JWT 也是遇到这个问题, user 是正常的, admin.refresh 有问题,总是找到的 users 表的数据,你的jwt是什么版本?

3年前 评论

你这个应该是串号的问题,作者给出了答案。我的是路由的问题,你把源码下载下来,再对比下文章你就发现问题了。

3年前 评论

提醒大家一下,跨域中间件如果没有用博主的版本而是安装的最新版本需要重新发布下配置文件,因为旧版本中有单词错误,如果不重新发布会导致配置文件失效从而导致跨域失败。我在这个问题上卡了一天了。

3年前 评论

horizon这个,部署新代码前,是直接运行 supervisorctl stop horizon 吗?还是先运行php artisan horizon:terminate,再运行supervisorctl stop horizon

3年前 评论

写的真的很好 有个问题请教下 如果user 里面 我想要验证的 是mobile 或者是其他字段 这个改如何转化模型 手摸手系列

3年前 评论
mayong (作者) 3年前

你好博主,我刚从tp转laravel 不久,我是个laravel小白、我按照你这个文档写了两遍到最后都出现同样一个问题。第一个设备登录并获取数据都没问题,但当第一个设备登录状态时我在用第二个设备登录第一个设备登录的账号登录也没问题,但当我刷新第一个设备时返回500

3年前 评论
ZLSN 3年前
liunian-zy 3年前
const STATUS_INVALID = -1; 
const STATUS_NORMAL = 0;
const STATUS_FREEZE = 1; 

public static $statusMap=[
    self::STATUS_INVALID=>'已删除',
    self::STATUS_NORMAL=>'正常',
    self::STATUS_FREEZE=>'冻结',
];

public static function getStatusName($status){
    return self::$statusMap[$status] ?? '正常';
}
$rules=[
    'GET'=>[
        'id' => ['required,exists:shop_user,id']
    ],
    'POST'=>[
        'name' => ['required', 'max:12', 'unique:users,name'],
        'password' => ['required', 'max:16', 'min:6']
    ],
];

return $rules[$this->method()] ?? [];

:smile: 一点小建议

3年前 评论

file 为什么我的报错界面跟你的不一样啊。postman 是不是还需要设置其他的东西 。。 laravel版本是6.18的

3年前 评论

laravel 6.18 jwt-auth 1.0
自动刷新用户认证 这段 token 错误时的报错信息跟文档不一样。是我哪里写错了吗

file

file RefreshTokenMiddleware.php file

3年前 评论

我现在觉得枚举这个文件其实没必要新建,我觉得可以写到model里面。还请赐教

3年前 评论
爆炸青山绿水 3年前

@guaosi 完善当验证不通过时 不是统一的api 返回 代码如下:

编辑 app/Http/Requests/Api 目录下的 FormRequest.php 文件

<?php

namespace App\Http\Requests\Api;

use Illuminate\Foundation\Http\FormRequest as BaseFormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Contracts\Validation\Validator;
use APP\Api\Helpers\ApiResponse;

class FormRequest extends BaseFormRequest
{
    use ApiResponse;

    protected function failedValidation(Validator $validator)
    {
        throw (new HttpResponseException(
            $this->failed($validator->errors(),422)
        ));
    }

    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

}

返回结果如下:

file

3年前 评论

想咨询下, laravel8 怎么进行捕获异常,期待回复。

2年前 评论
gyp719 (作者) 2年前

@guaosi 单设备登录逻辑会存在这个问题:
当系统是在使用一段时间后才开启的单设备登录,那对于已经登录且正在使用中的token因为没有记录到库里,所以没办法让它失效,只能等到自动刷新了之后才再在其它设备登录才会有效果。不过如果token的ttl设置的短一些1、2个小时影响也没那么大。

2年前 评论

:grin:根据大佬的文档改造了一下laravel8,欢迎star:github.com/gedongdong/laravel-api-...

2年前 评论

正在学习API评论关注中……

11个月前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!