搭建 Laravel API 脚手架

  • 创建 Laravel 项目
composer create-project laravel/laravel api-scaffold --prefer-dist "5.8.*"
  • 安装代码提示工具 Laravel IDE Helper
composer require barryvdh/laravel-ide-helper
php artisan ide-helper:generate  // 为 Facades 生成注释
  • 安装 laravel-s
composer require "hhxsv5/laravel-s:~3.5.0" -vvv
php artisan laravels publish
  • 自定义全局辅助函数

(1) 创建文件 app/helpers.php

<?php
.
.
.

(2) 修改项目 composer.json,在项目 composer.json 中 autoload 部分里的 files 字段加入该文件即可:

{
    ...

    "autoload": {
        "files": [
            "app/helpers.php"
        ]
    }

    ...
}

(3) 运行一下命令:

composer dump-autoload
  • 解决跨域(添加中间件)

(1) 安装 medz/cors

composer require medz/cors

(2) 发布配置文件

php artisan vendor:publish --provider="Medz\Cors\Laravel\Providers\LaravelServiceProvider" --force

(3) 修改配置文件,打开 config/cors.php,在 expose-headers 添加值 Authorization

return [
    ......
    'expose-headers' => ['Authorization'],
    ......
];

这样跨域请求时,才能返回 header 头为 Authorization 的内容,否则在刷新用户 token 时不会返回刷新后的 token。
(4) 增加中间件别名,打开 app/Http/Kernel.php,增加一行

protected $routeMiddleware = [
        ......
        'cors'=> \Medz\Cors\Laravel\Middleware\ShouldGroup::class,
];
  • 移动Model

(1) 在 app 目录下新建 Models 文件夹,然后将 User.php 文件移进去。
(2) 修改 User.php 文件,更改 namespace 为新创建的文件夹路径。

<?php
namespace App/Models/User.php;
.
.
.
  1. 编辑器全局搜索 App\User 替换为 App\Models\User。
  • 中间件实现返回 JSON 响应
    (1) 创建中间件ForceJson
php artisan make:middleware ForceJson

app/Http/Middleware/ForceJson.php

<?php

namespace App\Http\Middleware;

use Closure;

class ForceJson
{
    public function handle($request, Closure $next)
    {
        $request->headers->set('Accept', 'application/json');
        return $next($request);
    }
}

(2) 添加全局中间件
app/Http/Kernel.php

<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    /**
     * The application's global HTTP middleware stack.
     *
     * @var array
     */
    protected $middleware = [
        \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
        \App\Http\Middleware\ForceJson::class,
    ];

    .
    .
    .
  • 统一 Response 响应处理
    在app目录下新建目录MyTrait,并且新建ApiResponse.php
<?php

namespace App\MyTrait;

use Response;

trait ApiResponse {
    /**
     * 元数据
     *
     * @var array
     */
    protected $meta = [];

    /**
     * 错误提示信息
     *
     * @var string
     */
    protected $error = '';

    /**
     * 客户端提示信息
     *
     * @var string
     */
    protected $msg = '';

    /**
     * 状态码
     *
     * @var int
     */
    protected $statusCode = 200;

    /**
     * 状态码对应解析
     *
     * @var array
     */
    protected $codeInfo = [
        200    =>    '成功',
        400    =>    '客户端请求存在语法错误,服务器无法理解',
        401    =>    '身份认证出错',
        403    =>    '没有权限',
        404    =>    '找不到资源',
        422    =>    '验证失败',
        500    =>    '服务器出错',
    ];

    /**
     * 获取状态码解析
     *
     * @param $statusCode
     * @return mixed
     */
    public function getStatusCodeInfo($statusCode) {
        if (!array_key_exists($statusCode, $this->codeInfo)) {
            return '没有此状态码的错误提示信息';
        }
        return $this->codeInfo[$statusCode];
    }

    /**
     * 获取状态码
     *
     * @return int
     */
    public function getStatusCode() {
        return $this->statusCode;
    }

    /**
     * 设置状态码
     *
     * @param $statusCode
     * @return $this
     */
    public function setStatusCode($statusCode) {
        $this->statusCode = $statusCode;
        return $this;
    }

    /**
     * 获取客户端提示信息
     *
     * @return string
     */
    public function getMsg() {
        return $this->msg;
    }

    /**
     * 设置客户端提示信息
     *
     * @param $msg
     * @return $this
     */
    public function setMsg($msg) {
        $this->msg = $msg;
        return $this;
    }

    /**
     * 获取错误提示信息
     *
     * @return mixed
     */
    public function getErrors() {
        return $this->getStatusCodeInfo($this->getStatusCode());
    }

    /**
     * 设置元信息
     *
     * @param array $collection
     * @return $this
     */
    public function setMeta(array $collection) {
        // 设置总页码
        $collection['meta']['total_pages'] = $collection['meta']['last_page'];

        // 删除不必要的字段
        unset($collection['meta']['to']);
        unset($collection['meta']['path']);
        unset($collection['meta']['from']);
        unset($collection['meta']['last_page']);

        $this->meta = $collection['meta'];

        return $this;
    }

    /**
     * 获取元信息
     *
     * @return array
     */
    public function getMeta() {
        return $this->meta;
    }

    /**
     * 返回数据
     *
     * @param array $data
     * @param array $header
     * @return \Illuminate\Http\JsonResponse
     */
    public function response($data = [], $header = []) {

        $responseData = [
            'code'     =>  $this->getStatusCode(),
            'error'    =>  $this->getErrors(),
            'message'  =>  $this->getMsg(),
            'data'     =>  $data,
            'meta'     =>  $this->getMeta(),
        ];

        if (empty($this->getMeta())) {
            unset($responseData['meta']);
        }

        return Response::json($responseData, $this->getStatusCode(), $header);
    }
}
  • 格式化参数验证异常响应

(1)自定义验证异常 app/Exceptions/CustomValidationException.php

<?php

namespace App\Exceptions;

use App\MyTrait\ApiResponse;
use Illuminate\Validation\ValidationException;

class CustomValidationException extends ValidationException
{
    use ApiResponse;

    public function render() {

        $errors = [];

        foreach ($this->errors() as $key => $error) {
            $errors[$key] = current($error);
        }

        return $this->setStatusCode($this->status)->response($errors);
    }
}

(2)创建基础验证 FormRequest

php artisan make:request Api/FormRequest

app/Http/Request/Api/FormRequest.php

<?php

namespace App\Http\Requests\Api;

use App\Exceptions\CustomValidationException;
use \Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest as BaseFormRequest;

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

    /**
     * @param Validator $validator
     * @throws CustomValidationException
     */
    protected function failedValidation(Validator $validator)
    {
        throw new CustomValidationException($validator);
    }
}
  • 创建基Controller并且使用响应trait
php artisan make:controller Api/Controller

app/Http/Controllers/Api/Controller.php

<?php

namespace App\Http\Controllers\Api;

use App\MyTrait\ApiResponse;
use App\Http\Controllers\Controller as BaseController;

class Controller extends BaseController
{
    use ApiResponse;
}
  • 认证 jwt-auth

(1)安装jwt-auth,Laravel 5.8 版本对应的包为 tymon/jwt-auth:1.0.0-rc.4.1

composer require tymon/jwt-auth:1.0.0-rc.4.1

(2)发布配置文件

# 这条命令会在 config 下增加一个 jwt.php 的配置文件
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"

(3)生成加密密钥

# 这条命令会在 .env 文件下生成一个加密密钥,如:JWT_SECRET=foobar
php artisan jwt:secret

(4)更新模型,app/Models/User.php

<?php

namespace App;

use Tymon\JWTAuth\Contracts\JWTSubject;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable implements JWTSubject    # 这里别忘了加
{
    use Notifiable;

    // Rest omitted for brevity

    /**
     * Get the identifier that will be stored in the subject claim of the JWT.
     *
     * @return mixed
     */
    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    /**
     * Return a key value array, containing any custom claims to be added to the JWT.
     *
     * @return array
     */
    public function getJWTCustomClaims()
    {
        return [];
    }
}

(5)修改 auth.php (config)

'defaults' => [
    'guard' => 'api',
    'passwords' => 'users',
],

'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],

    'api' => [
        'driver' => 'jwt',      // 原来是 token 改成jwt
        'provider' => 'users',
    ],
],
  • 无感刷新token

(1)创建中间件

php artisan make:middleware RereshToken

(2)app/Http/Middleware/RefreshToken.php

<?php

namespace App\Http\Middleware;

use \Auth;
use Closure;
use Tymon\JWTAuth\Exceptions\JWTException;
use Tymon\JWTAuth\Http\Middleware\BaseMiddleware;
use Tymon\JWTAuth\Exceptions\TokenExpiredException;
use Tymon\JWTAuth\Exceptions\TokenInvalidException;
use Tymon\JWTAuth\Exceptions\TokenBlacklistedException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;

class RefreshToken extends BaseMiddleware
{
    public function handle($request, Closure $next)
    {
        try {
            // 检查此次请求中是否带 token, 如果没有则抛出异常 (BaseMiddleware的方法)
            $this->checkForToken($request);
        } catch (UnauthorizedHttpException $exception) {
            throw new UnauthorizedHttpException('jwt-auth', '请提供Token');
        }

        // 使用 try 包裹,以捕捉 token 过期所抛出的 TokenExpiredException 异常
        try {
            // 获取 token 中的 user 信息
            $user = $this->auth->parseToken()->authenticate();

            // 检测登录状态
            if (!$user) {
                throw new UnauthorizedHttpException('jwt-auth', '你还没有登录');
            } else {
                return $next($request);
            }

        } catch (TokenExpiredException $exception) {
            // 此处捕获到了 token 过期所抛出的 TokenExpiredException 异常,我们在这里需要做的是刷新该用户的 token 并将它添加到响应头中
            try {
                // 刷新用户的 token
                $token = $this->auth->refresh();
                // 使用一次性登录以保证此次请求的成功
                Auth::guard('api')->onceUsingId($this->auth->manager()->getPayloadFactory()->buildClaimsCollection()->toPlainArray()['sub']);

                //刷新了token,将token存入数据库(防止刚刷新用户在别的地方登录没有拉黑此token)
                $user = Auth::user();
                $user->last_token = $token;
                $user->save();

            } catch (JWTException $exception) {
                // 异常为token被拉入黑名单
                if ($exception instanceof TokenBlacklistedException || $exception instanceof TokenInvalidException) {
                    throw new UnauthorizedHttpException('jwt-auth', '登录凭证被拉入了黑名单');
                } else {
                    // 如果捕获到此异常,即代表 refresh 也过期了,用户无法刷新令牌,需要重新登录。
                    throw new UnauthorizedHttpException('jwt-auth', '登录凭证过期,请重新登录');
                }
            }
        }

        // 在响应头中返回新的 token
        return $this->setAuthenticationHeader($next($request), $token);
    }
}

(3)app/Http/kernel.php

protected $routeMiddleware = [
        .
        .
        .
        'refresh.token' => \App\Http\Middleware\RefreshToken::class,
   ];
  • 自定义处理异常

(1)创建 app/Exceptions/ExceptionReport.php

<?php

namespace App\Exceptions;

use Exception;
use App\MyTrait\ApiResponse;
use Illuminate\Http\Request;
use Tymon\JWTAuth\Exceptions\TokenInvalidException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;

class ExceptionReport
{
    use ApiResponse;

    /**
     * http 请求
     *
     * @var Request
     */
    public $requset;

    /**
     * 拦截的异常
     *
     * @var Exception
     */
    public $exception;

    /**
     * 要处理的异常
     */
    protected $report;

    public $doReport = [
        TokenInvalidException::class => 401,        // jwt
        UnauthorizedHttpException::class => 401,
    ];

    /**
     * ExceptionReport constructor.
     * @param Request $request
     * @param Exception $exception
     */
    function __construct(Request $request, Exception $exception) {
        $this->requset   = $request;
        $this->exception = $exception;
    }

    /**
     * @param Exception $exception
     * @return ExceptionReport
     */
    public static function make(Exception $exception) {
        return new static(\request(), $exception);
    }

    /**
     * 判断拦截的异常是否在异常列表中
     *
     * @return bool
     */
    public function shouldReturn() {
        if (! ($this->requset->wantsJson() || $this->requset->ajax())) {
            return false;
        }

        foreach (array_keys($this->doReport) as $report) {
            if ($this->exception instanceof  $report) {
                $this->report = $report;
                return true;
            }
        }

        return false;
    }

    /**
     * 处理异常
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function report() {
        return $this->setStatusCode($this->doReport[$this->report])
                    ->setMsg($this->exception->getMessage())
                    ->response();
    }
}

(2)app/Exceptions/handler.php

    .
    .
    .

    /**
     * @param \Illuminate\Http\Request $request
     * @param Exception $exception
     * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\Response|\Symfony\Component\HttpFoundation\Response
     */
    public function render($request, Exception $exception)
    {
        // 将异常拦截到自己的 ExceptionReport 方法
        $reporter = ExceptionReport::make($exception);

        if ($reporter->shouldReturn()) {
            return $reporter->report();
        }

        return parent::render($request, $exception);
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容