搭建一個自己的 Laravel API 腳手架 - Delighture

Darius_zh發表於2019-11-25

搭建一個自己的 Laravel API 腳手架 - Delighture

  在社群學習 Laravel 也有幾個月了,於是想通過搭建一個自己的API腳手架來作為實踐,同時以後的新專案也會採用該腳手架來實現。不足之處懇請提出,日後也會不斷完善該腳手架。

Delighture Github地址:https://github.com/Seven-Night-7/delightur...

  1. 安裝 Laravel 5.8,配置基礎 .env 資訊

1.1 基礎使用者表

  1. 使用命令 php artisan make:migration create_users_table 建立基礎使用者表,遷移程式碼如下

database\migrations\2019_11_22_014057_create_users_table.php

public function up()
{
    Schema::create('users', function (Blueprint $table) {
        $table->bigIncrements('id');
        $table->string('account')->comment('賬號');
        $table->string('password')->comment('密碼');
        $table->unsignedTinyInteger('status')->default(0)->comment('賬號狀態 0:正常 1:凍結');
        $table->timestamps();
        $table->softDeletes();
    });
}

2.1 自定義全域性輔助函式

  1. 定製自定義全域性輔助函式模組,使用命令 php artisan make:provider HelperServiceProvider 建立 HelperServiceProvider ,其程式碼如下

app\Providers\HelperServiceProvider.php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class HelperServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        foreach (glob(app_path('Helpers') . '/*.php') as $file) {
            require_once $file;
        }
    }
}
  1. config/app.php 中加入 App\Providers\HelperServiceProvider::class

config\app.php

'providers' => [
    .
    .
    .
    App\Providers\RouteServiceProvider::class,

    //  輔助函式
    App\Providers\HelperServiceProvider::class,

],

3 .1 自定義全域性響應

  1. 考慮到 dingo/api 包不方便統一響應格式,決定還是自定義一個簡單的響應邏輯。建立 Helpers/response.php 輔助函式檔案,寫入方法 json_response() 。其中 App\Enums\StatusCode 為自定義狀態碼類,後面會給出該類程式碼。程式碼如下

app\Helpers\response.php

<?php

//  統一響應
function json_response($statusCode, $data = [], $message = '')
{
    $message = $message ?
        $message : (isset(\App\Enums\StatusCode::$statusMessage[$statusCode]) ?
            \App\Enums\StatusCode::$statusMessage[$statusCode] : '未知狀態碼');

    return response()->json([
        'status_code' => $statusCode,
        'message' => $message,
        'data' => $data,
    ]);
}
  1. 建立基礎控制器 BaseController ,寫入控制器統一響應方法 response(),後續所有的控制器都應該繼承 BaseController ,程式碼如下

app\Http\Controllers\BaseController.php

<?php

namespace App\Http\Controllers;

use App\Enums\StatusCode;

class BaseController extends Controller
{
    /**
     * 控制器統一響應
     * @param int $statusCode
     * @param array $data
     * @param string $message
     * @return \Illuminate\Http\JsonResponse
     */
    public function response($statusCode = StatusCode::SUCCESS, $data = [], $message = '')
    {
        return json_response($statusCode, $data, $message);
    }
}

4.1 自定義全域性狀態碼

  1. 緊接著我們利用列舉包來實現 App\Enums\StatusCode 自定義狀態碼類。包命令 composer require bensampo/laravel-enum ,安裝完成後使用命令 php artisan make:enum StatusCode 便捷生成 App\Enums\StatusCode ,並定義一些已經用到的自定義狀態碼。(自己對列舉包的瞭解還比較淺顯,後續會深入)程式碼如下

app\Enums\StatusCode.php

<?php

namespace App\Enums;

use BenSampo\Enum\Enum;

final class StatusCode extends Enum
{
    const SUCCESS = 0;
    const FAIL  = -1;

    const PARAM_ERROR = -10000;

    const LOGIN_ERROR = -20001;
    const USER_IS_FROZEN = -20002;

    const TOKEN_ERROR = -30000;
    const MISSING_TOKEN = -30001;

    public static $statusMessage = [
        0      => '請求成功',
        -1     => '請求失敗',

        -10000 => '引數驗證錯誤',

        -20001 => '賬號或密碼錯誤',
        -20002 => '賬號已被凍結',

        -30000 => 'token異常',
        -30001 => 'token不存在',
    ];
}

5.1 JWT授權登入與驗證

  1. 下一步我們將實現基於JWT的token驗證,實現步驟如下
  • 包命令 composer require tymon/jwt-auth:1.0.0-rc.4.1 (注意是Laravel 5.8版本對應的包,不同的Laravel版本對應的包的版本會不一樣,需要自己去尋找)

  • 生成 JWT 的 secret ,命令 php artisan jwt:secret

  • 釋出 JWT 配置檔案,命令 php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider" ,得到 config/jwt.php 配置檔案

  • 修改 config/auth.phpapi 的指定驅動 driverjwt ,以及後面的配置 providersusers 的指定模型 modelApp\Models\User::class ,如下

config\auth.php

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

    'api' => [
        'driver' => 'jwt',
        'provider' => 'users',
    ],
],
.
.
.
'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model' => App\Models\User::class,
    ],
],
.
.
.
  • 建立 App\Models\User::class 模型,程式碼如下

app\Models\Model.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Tymon\JWTAuth\Contracts\JWTSubject;

class User extends Authenticatable implements JWTSubject
{
    use SoftDeletes;

    protected $hidden = ['password'];

    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    public function getJWTCustomClaims()
    {
        return [];
    }
}
  • 建立中介軟體 TokenCheckMiddleware 校驗 token 有效性以及實現無痛重新整理 token 。注意這裡丟擲的是自定義的異常響應,後續的異常也會用 throw new HttpResponseException() 來處理。程式碼如下

app\Http\Middleware\TokenCheckMiddleware.php

<?php

namespace App\Http\Middleware;

use App\Enums\StatusCode;
use Closure;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Support\Facades\Auth;
use Tymon\JWTAuth\Exceptions\JWTException;
use Tymon\JWTAuth\Facades\JWTAuth;
use Tymon\JWTAuth\Http\Middleware\BaseMiddleware AS JWTBaseMiddleware;

class TokenCheckMiddleware extends JWTBaseMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        try {
            $auth = JWTAuth::parseToken();
        } catch (JWTException $exception) {
            //  token不存在
            throw new HttpResponseException(json_response(StatusCode::MISSING_TOKEN));
        }

        if ($auth->check()) {
            //  token通過
            return $next($request);
        }

        try {
            //  重新整理使用者的 token
            $token = $this->auth->refresh();

            //  使用一次性登入以保證此次請求的成功
            Auth::guard('api')->onceUsingId($this->auth->manager()->getPayloadFactory()->buildClaimsCollection()->toPlainArray()['sub']);

        } catch (JWTException $exception) {
            // 如果捕獲到此異常,即代表 refresh 也過期了,使用者無法重新整理令牌,需要重新登入。
            throw new HttpResponseException(json_response(StatusCode::TOKEN_ERROR, [], $exception->getMessage()));
        }

        //  在響應頭中返回新的 token
        return $this->setAuthenticationHeader($next($request), $token);
    }
}
  • App\Http\Kernel 中為中介軟體起別名,程式碼如下

app\Http\Kernel.php

.
.
.
protected $routeMiddleware = [
    .
    .
    .
    //  token校驗
    'token.check' => \App\Http\Middleware\TokenCheckMiddleware::class
];
.
.
.
  1. 建立控制器 AuthenticationController 實現登入 store() 和登出 destroy() ,以及相應的表單驗證類 AuthenticationRequest 和表單驗證基類 FormRequest 。程式碼如下

app\Http\Controllers\AuthenticationController.php

<?php

namespace App\Http\Controllers;

use App\Enums\StatusCode;
use App\Http\Requests\AuthenticationRequest;
use App\Models\User;
use Tymon\JWTAuth\Facades\JWTAuth;

class AuthenticationController extends BaseController
{
    /**
     * 登入
     * @param AuthenticationRequest $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function store(AuthenticationRequest $request)
    {
        $is_freeze = User::where('account', $request->account)->value('status');
        if ($is_freeze) {
            return $this->response(StatusCode::USER_IS_FROZEN);
        }

        $token = JWTAuth::attempt([
            'account' => $request->account,
            'password' => $request->password,
        ]);
        if (!$token) {
            return $this->response(StatusCode::LOGIN_ERROR);
        }

        return $this->response(StatusCode::SUCCESS, ['token' => 'bearer ' . $token], '登入成功');
    }

    /**
     * 登出登入
     * @return \Illuminate\Http\JsonResponse
     */
    public function destroy()
    {
        JWTAuth::parseToken()->invalidate();

        return $this->response(StatusCode::SUCCESS, [], '登出登入成功');
    }
}

app\Http\Requests\AuthenticationRequest.php

<?php

namespace App\Http\Requests;

class AuthenticationRequest extends FormRequest
{
    /**
     * 驗證規則
     * @return array
     */
    public function rules()
    {
        switch ($this->method())
        {
            case 'POST':
                return [
                    'account' => 'required',
                    'password' => 'required|between:6,12',
                ];
        }
    }

    /**
     * 屬性名稱
     * @return array
     */
    public function attributes()
    {
        return [
            'account' => '賬號',
            'password' => '密碼',
        ];
    }
}

app\Http\Requests\FormRequest.php

<?php

namespace App\Http\Requests;

use App\Enums\StatusCode;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest AS BaseFormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;

class FormRequest extends BaseFormRequest
{
    public function authorize()
    {
        return true;
    }

    /**
     * 自定義驗證失敗處理
     * @param Validator $validator
     */
    public function failedValidation(Validator $validator)
    {
        $error_msg = $validator->errors()->first();

        throw new HttpResponseException(json_response(StatusCode::PARAM_ERROR, [], $error_msg));
    }
}
  1. 此時表單驗證在驗證不通過時返回的英文提示,而我們需要的是中文提示,所以我們可以通過語言包 overtrue/laravel-lang 來解決這個問題。包命令 composer require "overtrue/laravel-lang:~3.0" ,安裝完成後修改 config/app.phplocale 的值為 zh-CN ,以及將 providers 下的 Illuminate\Translation\TranslationServiceProvider::class 替換成 Overtrue\LaravelLang\TranslationServiceProvider::class,程式碼如下

config\app.php

.
.
.
'locale' => 'zh-CN',
.
.
.
'providers' => [
        .
        .
        .
        Illuminate\Session\SessionServiceProvider::class,
        Overtrue\LaravelLang\TranslationServiceProvider::class,
        Illuminate\Validation\ValidationServiceProvider::class,
        .
        .
        .
    ],
.
.
.
  1. 建立全域性輔助函式檔案 Helpers/user.php ,建立方法 me() 使得我們可以在程式中快速獲取登入使用者的資料模型。接著建立控制器 App\Http\Controllers\UserController 及其方法 me() 作為獲取登入使用者資料的介面。程式碼如下

app\Helpers\user.php

<?php

//  獲取我的使用者資料(返回 App\Models\User 模型)
function me()
{
    return auth('api')->user();
}

app\Http\Controllers\UserController.php

<?php

namespace App\Http\Controllers;

use App\Enums\StatusCode;

class UserController extends BaseController
{
    /**
     * 我的登入資訊
     * @return \Illuminate\Http\JsonResponse
     */
    public function me()
    {
        return $this->response(StatusCode::SUCCESS, me());
    }
}
  1. 前面登入、登出、獲取登入使用者資訊的介面路由如下

routes\api.php

<?php

use Illuminate\Http\Request;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/

//  登入
Route::post('login', 'AuthenticationController@store');

Route::middleware('token.check')->group(function () {
    //  登出登入
    Route::delete('logout', 'AuthenticationController@destroy');
    //  我的登入資訊
    Route::get('/users/me', 'UserController@me');
});

補充

  • 修改 config/app.phptimezone 值為 Asia/Shanghai
  • 建立模型基類 app\Model\Model.php ,後續除 App\Model\User::class 以外的模型均繼承該模型基類,程式碼如下
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model AS BaseModel;
use Illuminate\Database\Eloquent\SoftDeletes;

class Model extends BaseModel
{
    use SoftDeletes;
}

  自知身處井底,不敢苟言於世。

相關文章