搭建 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);
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容