Laravel 使用 passport 開發 API 介面

xingkong12138發表於2020-05-25

該文章轉載自我的部落格:我的部落格

在現在的情境下,前後端分離是越來越普遍了,而laravel本身就對API有良好的支援,所以我在這裡記錄一下我使用laravel的官方擴充套件包passport去做API開發。

本文的laravel版本是laravel 6.*,當然你也可以使用laravel的其他版本。

如果你使用 laravel 5.4 或者 更低版本,你需要在 config/app.php 檔案中為Passport註冊服務。就這樣,在這個檔案中的providers陣列中新增註冊服務。

本文只是作為一個參考,告訴你如何使用passport。實際開發按照你自己的來。

通過Composer去安裝

composer require laravel/passport
php artisan migrate

注意:這裡的遷移命令會把所有的遷移檔案都執行,所以如果你只想遷移passport的檔案,在vendor/laravel/passport/database/migrations目錄裡面,是與passport有關的遷移檔案。

執行下面命令:

php artisan migrate --path=./vendor/laravel/passport/database/migrations/

如果遷移的時候報這種型別的錯

Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes

原因

Laravel 5.4對預設資料庫字符集進行了更改,現在utf8mb4它包含了對儲存表情符號的支援。這隻會影響新的應用程式,只要您執行MySQL v5.7.7及更高版本,就不需要做任何事情。

解決方法

  1. 第一種方法:把MySQL升級為MySQL8版本,如果是生產環境,就建議你升級到MySQL8

  2. 第二種方法:就是在app/Providers/AppServiceProvider.php檔案中,修改boot方法:

public function boot()
{
    Schema::defaultStringLength(191);//新增這行程式碼
}

此命令會建立祕鑰以用來生成安全的Access Token。除此之外,它也會建立用來生成Access Tokenpersonal access【私有授權】password grant【密碼授權】

php artisan passport:install

我建議你直接使用上面的命令去生成

config/auth.php 配置檔案中,你應該設定 api 許可權認證守衛的 driver 選項為 passport。當需要許可權認證的 API 請求進來時會告訴你的應用去使用 Passport'sTokenGuard

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

Laravel\Passport\HasApiTokens trait 新增到你的 App\User 模型中。這個 trait 會為模型新增一系列助手函式用來驗證使用者的祕鑰和作用域:

<?php

namespace App\Models;

use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Passport\HasApiTokens;

class User extends Authenticatable
{
    use Notifiable, HasApiTokens;
}

如果你的不是App\User這個表,在config/auth.php 配置檔案中修改:

'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\User::class, //這裡修改成你的模型
        ],
    ],

接下來,你應該在 AuthServiceProvider 中的 boot 方法中呼叫 Passport::routes 方法。這個方法會註冊必要的路由去頒發訪問令牌,撤銷訪問令牌,客戶端和個人令牌:

<?php

namespace App\Providers;

use Laravel\Passport\Passport;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        'App\Model' => 'App\Policies\ModelPolicy',
    ];

    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        Passport::routes();
    }
}

由於passport生成的令牌預設是長期有效的,所以我們把過期時間設定為24小時:

    <?php

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Gate;
use Laravel\Passport\Passport;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        // 'App\Model' => 'App\Policies\ModelPolicy',
    ];

    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        Passport::routes();

        Passport::tokensExpireIn(Carbon::now()->addDays(1));//令牌過期時間
        Passport::refreshTokensExpireIn(Carbon::now()->addDays(1));//重新整理令牌過期時間
        Passport::personalAccessTokensExpireIn(Carbon::now()->addDays(1));//個人訪問令牌過期時間
    }
}

然後Passport的配置告一段落。接下來就是測試是否有用。

建立模型

php artisan make:model Models/User

模型程式碼:

<?php

namespace App\Models;

use App\Helper\Utils;
use Illuminate\Database\Eloquent\Model;
use Laravel\Passport\HasApiTokens;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

//注意,這裡繼承的是Authenticatable而不是Model,否則會出現findForPassport,validateForPassportPasswordGrant不存在的報錯

class User extends Authenticatable
{
    use HasApiTokens,Notifiable;//Notifiable這個可有可無
    protected $table="users";

    public function findForPassport($account)
    {
        //token驗證預設是email,我改成了account
        return $this->where('account', $account)->first();
    }

    public function validateForPassportPasswordGrant($password)
    {
        //這裡使用的是自定義的密碼驗證
        $md5_password=Utils::GetMd5Password($password,$this->salt);
        return Utils::CheckHashPassword($md5_password,$this->password);
    }
}

建立路由

route/api.php檔案下新增:

Route::prefix('v1')->namespace('Api')->group(function (){
    Route::post("/register","UserController@register");
    Route::post("/login","UserController@Login");
    //這裡使用passport中介軟體驗證token
    Route::middleware('auth:api')->group(function (){
        Route::post("/user_info","UserController@UserInfo");
        Route::post("/logout","UserController@Logout");
    });
});

建立控制器

php artisan make:controller Api/UserController

控制器程式碼

<?php

namespace App\Http\Controllers\Api;

use App\Helper\Utils;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Validator;

class UserController extends Controller
{
    //註冊
    public function register(Request $request){
        $post_data=$request->post();

        $rule=[
            "name"=>"required",
            "account"=>"required",
            "password"=>"required",
            "phone"=>"required",
        ];
        $message=[
            "required"=>":attribute 不能為空"
        ];
        $attr=[
            "name"=>"使用者名稱稱",
            "account"=>"賬號",
            "password"=>"密碼",
            "phone"=>"手機號",
        ];

        $validator = Validator::make($post_data,$rule,$message,$attr);

        if($validator->fails())
        {
            return Utils::JsonData(400,$validator->errors()->first());
        }

        //驗證使用者是否已存在
        $is_user=User::query()->where("account",trim($post_data["account"]))->count();
        if($is_user > 0){
            return Utils::JsonData(400,"使用者已存在");
        }

        $salt=Utils::GetRandomString();
        $password=Utils::GetHashPassword($post_data["password"],$salt);
        $data_info=[
            "name"=>trim($post_data["name"]),
            "account"=>trim($post_data["account"]),
            "password"=>$password,
            "salt"=>$salt,
            "phone"=>$post_data["phone"],
            "create_time"=>date("Y-m-d H:s:i",time()),
            "update_time"=>date("Y-m-d H:s:i",time()),
        ];
        try{
            User::query()->insert($data_info);
            return Utils::JsonData(200,"註冊成功");
        }catch (\Exception $e){
            return Utils::JsonData(400,"註冊失敗");
        }
    }

    //登入
    public function Login(Request $request){
        $post_data=$request->post();
        //驗證使用者是否存在
        $user=User::query()->where("account",$post_data["account"])->first();
        if(!isset($user->id)){
            return Utils::JsonData(400,"使用者不存在");
        }

        //驗證密碼
        $md5_password=Utils::GetMd5Password($post_data["password"],$user->salt);
        if(!Utils::CheckHashPassword($md5_password,$user->password)){
            return Utils::JsonData(400,"密碼錯誤");
        }

        //生成token
        $tokenResult = $user->createToken('LaravelApi');
        $token = $tokenResult->token;
        if ($post_data["remember_me"]) {
            $token->expires_at = Carbon::now()->addWeeks(1);//設定令牌過期時間為一週
        }
        $token->save();//儲存令牌
        $data_info=[
            'access_token' => $tokenResult->accessToken,
            'token_type' => 'Bearer',
            'expires_at' => Carbon::parse(
                $tokenResult->token->expires_at
            )->toDateTimeString()
        ];

        return Utils::JsonData(200,"請求成功",$data_info);
    }


    //退出
    public function Logout(Request $request){
        $request->user()->token()->revoke();
        return Utils::JsonData(200,"退出成功");
    }

    //獲取使用者資訊
    public function UserInfo(Request $request){
        $user=$request->user();
        return Utils::JsonData(200,"請求成功",$user);
    }
}

建立輔助函式類 Utils

app/建立個Helper/Utils.php,程式碼如下:

<?php
/**
 * Created by PhpStorm.
 * User: Administrator
 * Date: 2020/5/25
 * Time: 10:24
 */

namespace App\Helper;


use Illuminate\Support\Facades\Hash;

class Utils
{
    /**
     * 請求返回資料封裝
     * @param int $code 狀態碼 200:成功,400:錯誤,300:未授權
     * @param string $msg
     * @param null $data
     */
    public static function JsonData($code=200,$msg="",$data=null,$is_return=true){
        $return_data=array(
            "code"=>$code,
            "msg"=>$msg,
            "data"=>$data
        );
        if($is_return){
            return json_encode($return_data);
        }else{
            echo json_encode($return_data);
        }
    }

    /**
     * 生成雜湊密碼
     * @param $password //密碼
     * @param $salt //鹽值
     */
    public static function GetHashPassword($password,$salt){
        //加密規則,先生成MD5的加密密碼,然後再做雜湊處理
        $md5_password=self::GetMd5Password($password,$salt);

        //該方法還有一個引數,可以調整工作因子等配置,當然你也可以在config/hashing.php配置檔案中更改,參考官方文件
        $hash_password=Hash::make($md5_password);

        return $hash_password;
    }

    /**
     * 生成MD5的加密密碼
     * @param $password //密碼
     * @param $salt
     */
    public static function GetMd5Password($password,$salt){
        $md5_password=md5(md5($password.$salt).$salt);
        return $md5_password;
    }


    /**
     * 驗證密碼是否正確
     * @param $md5_password
     * @param $hash_password
     */
    public static function CheckHashPassword($md5_password,$hash_password){
        if(Hash::check($md5_password,$hash_password)){
            return true;
        }else{
            return false;
        }
    }

    /**
     * 檢查hash是否需要更新
     * @param $hash_pass
     */
    public static function InspectHashOverdue($hash_password){
        if(Hash::needsRehash($hash_password)){
            return true;
        }else{
            return false;
        }
    }

    /**
     * 生成隨機的字串
     * @param int $len
     * @param bool $special
     */
    public static function GetRandomString($len=4,$special=false){
        if($special){
            $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_ []{}<>~`+=,.;:/?|";
        }else{
            $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        }
        $str="";
        for ($i=0;$i < $len;$i++){
            // 這裡提供兩種字元獲取方式
            // 第一種是使用 substr 擷取$chars中的任意一位字元;
            // 第二種是取字元陣列 $chars 的任意元素
            // $str .= substr($chars, mt_rand(0, strlen($chars) – 1), 1);
            $str .= $chars[ mt_rand(0, strlen($chars) - 1) ];
        }
        return $str;

    }
}

使用postman測試介面

注意:請求的時候一定要把header頭的Accept設定為application/json

註冊:

登入:

獲取使用者資訊

上一步已經把這些方法完成,如果你有把上面的介面執行一遍,你就會發現,我token如果沒有驗證通過,會丟擲一個異常,而不是我們想要的json格式,所以我們要捕獲到異常,然後返回我們需要的json格式。

app/Helper/下建立異常捕獲類ExceptionReport.php

程式碼如下:

<?php

namespace App\Helper;

use Exception;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\QueryException;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;

/**
 * 自定義異常捕獲
 * Class ExceptionReport
 * @package App\Api\Helpers
 */

class ExceptionReport
{

    /**
     * @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
     */
    public $doReport = [
        AuthenticationException::class => ['未授權',401],
        ModelNotFoundException::class => ['該模型未找到',404],
        AuthorizationException::class => ['沒有此許可權',403],
        ValidationException::class => [],
        UnauthorizedHttpException::class=>['未登入或登入狀態失效',422],
        NotFoundHttpException::class=>['沒有找到該頁面',404],
        MethodNotAllowedHttpException::class=>['訪問方式不正確',405],
        QueryException::class=>['引數錯誤',401],
    ];

    /**
     * @return bool
     */
    public function shouldReturn(){

        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 = current($this->exception->errors());

            return Utils::JsonData(400,current($error));
        }


        $message = $this->doReport[$this->report];
        return Utils::JsonData(400,$message[0]);

    }

}

然後在app/Exceptions/Handler.php中的render方法攔截異常

public function render($request, Exception $exception)
{
    // 將方法攔截到自己的ExceptionReport
    $reporter = ExceptionReport::make($exception);
    if ($reporter->shouldReturn()){
        return $reporter->report();
    }
    return parent::render($request, $exception);
}

然後驗證不通過就不會丟擲一個異常了,而是返回json格式的錯誤。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章