Laravel passport 多端使用者使用

____發表於2019-09-30

說明

使用 passport 進行 admin 端和 customer 端的使用者認證。

雖然教程很多,但是我並沒有參照其他教程完整的走下來,所以記錄了自己的開發流程,希望能對其他人有所幫助。

github

安裝專案

laravel new passport

安裝 passport

composer require laravel/passport

xn7Qi1hF8X.png!large

資料遷移

首先我們需要建立 admins 和 customers 表,並填充假資料


php artisan make:migration create_admins_table --create=admins

php artisan make:migration create_customers_table --create=customers

php artisan make:seeder AdminsTableSeeder

php artisan make:seeder CustomersTableSeeder

pP2utXzfEm.png!large0bNzNqD1hv.png!large6r5x6i6rJ4.png!largeGX2ljwUzdx.png!largeKOgzzzC5u9.png!large9Cfex3Trxz.png!large

執行遷移

php artisan migrate --seed

IfnofuBEae.png!large

passport 初始化

php artisan passport:install

qZeg3ZjtjZ.png!large

此時在 storage 下會生成 oauth-private.key 和 oauth-public.key

5WfwipHHnA.png!large

生成認證

目前我們只是為前後端分離的後臺使用,所以 password 模式足夠

php artisan passport:client --password --name='passport-admin'

php artisan passport:client --password --name='passport-customer'

ndeZJKSbCz.png!large

備註

原先我以為這裡採用不一樣的資料之後下面 token 不會出現複用的情況,然而和這個沒有關係

token 複用是指 admin 端生成的 1 號使用者的 token 去請求 customer 端時,依然有效

解決方法下文會有介紹

修改路由配置

找到 app/Providers/RouteServiceProvider.php, 增加如下程式碼


 public function map()
    {
        ·
        ·
        ·

        // admin 路由
        $this->mapAdminRoutes();
        // customer 路由
        $this->mapCustomerRoutes();
    }

    protected function mapAdminRoutes()
    {
        Route::prefix('admin')
            ->namespace($this->namespace . '\Admin')
            ->group(base_path('routes/admin.php'));
    }

    protected function mapCustomerRoutes()
    {
        Route::prefix('customer')
            ->namespace($this->namespace . '\Customer')
            ->group(base_path('routes/customer.php'));
    }

在 routes 下新建 admin.php 和 customer.php

分別增加如下程式碼

admin.php


<?php

Route::group([
    'middleware' => 'passport-guard'
], function () {
    // 登入
    Route::post('login', 'AuthController@login');
    // 重新整理 token
    Route::put('refresh', 'AuthController@refresh');
    Route::group([
        'middleware' => ['auth:api', 'scopes:admin']
    ], function () {
        // 退出
        Route::delete('logout', 'AuthController@logout');
        // 詳情
        Route::get('admins/current', 'AdminsController@current');
    });
});

customer.php


<?php

Route::group([
    'middleware' => 'passport-guard'
], function () {
    // 登入
    Route::post('login', 'AuthController@login');
    // 重新整理 token
    Route::put('refresh', 'AuthController@refresh');
    Route::group([
        'middleware' => ['auth:api', 'scopes:customer']
    ], function () {
        // 退出
        Route::delete('logout', 'AuthController@logout');
        // 詳情
        Route::get('customers/current', 'CustomersController@current');
    });
});

備註

此處 auth:api 是檢驗 token

scopes:admin, scopes:customer 是給 token 指定作用域,即防止上文 token 複用的情況出現

建立中介軟體

php artisan make:middleware PassportGuard

F3s7tI3Ubo.png!large

增加如下程式碼

因為 passport 預設使用的是 api 守衛,並且不支援傳參修改,所以需要通過中介軟體修改 provider

 public function handle($request, Closure $next)
 {

        try {
            if ($request->is('admin/*')) {// 如果是 admin 路由
                config(['auth.guards.api.provider' => 'admins']);
            } elseif ($request->is('customer/*')) { // 如果是 customer 路由
                config(['auth.guards.api.provider' => 'customers']);
            }
        } catch (\Exception $exception) {
            throw new $exception;
        }

        return $next($request);
}

找到 app/Http/Kernel.php,註冊中介軟體

 protected $routeMiddleware = [
        ·
        ·
        ·
        // passport 認證路由
        'passport-guard' => \App\Http\Middleware\PassportGuard::class
       // token 作用域
        'scopes' => \Laravel\Passport\Http\Middleware\CheckScopes::class,
        'scope' => \Laravel\Passport\Http\Middleware\CheckForAnyScope::class,
    ];

修改 Providers

找到 app/Http/Providers

增加如下程式碼

use Laravel\Passport\Passport;
use Laravel\Passport\RouteRegistrar;

public function boot()
    {
        ·
        ·
        ·
        // Passport 路由註冊
        $prefix = '';
        if (request()->is('admin/*')) {
            $prefix = 'admin';
        } elseif (request()->is('customer/*')) {
            $prefix = 'customer';
        }

        // 我們只需要前後端分離的形式, 而不需要認證
        Passport::routes(function (RouteRegistrar $router) {
            $router->forAccessTokens();
        }, ['prefix' => $prefix . '/oauth', 'middleware' => 'passport-guard']);
        // token 作用域
        Passport::tokensCan([
            'admin' => 'admin',
            'customer' => 'customer'
        ]);
         // access_token 過期時間
        Passport::tokensExpireIn(Carbon::now()->addDays(15));
        // refreshTokens 過期時間
        Passport::refreshTokensExpireIn(Carbon::now()->addDays(30));
    }

此時還需修改 config/auth.php, 修改為如下程式碼

'guards' => [
        ·
        ·
        ·

        'api' => [
            'driver' => 'passport',
            'provider' => 'users',
            'hash' => false,
        ],

        'admin' => [
            'driver' => 'passport',
            'provider' => 'admins',
        ],

        'customer' => [
            'driver' => 'passport',
            'provider' => 'customers',
        ],
    ],

  'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\User::class,
        ],

        'admins' => [
            'driver' => 'eloquent',
            'model' => App\Admin::class,
        ],

        'customers' => [
            'driver' => 'eloquent',
            'model' => App\Customer::class,
        ],
    ],

建立 Model

php artisan make:model Admin
php artisan make:model Customer

xRlk0O5hDa.png!large

分別修改為如下程式碼

Admin.php


<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

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

class Admin extends Authenticatable
{
    use HasApiTokens;

    /**
     * Passport 多認證欄位
     */
    public function findForPassport($username)
    {
        return self::orWhere('email', $username)->orWhere('username', $username)->first();
    }
}

Customer.php


<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Passport\HasApiTokens;
class Customer extends Authenticatable
{
    use HasApiTokens;

    /**
     * Passport 多認證欄位
     */
    public function findForPassport($username)
    {
        return self::orWhere('email', $username)->orWhere('username', $username)->first();
    }
}

建立控制器

php artisan make:controller Admin/AuthController
php artisan make:controller Admin/AdminsController
php artisan make:controller Customer/AuthController
php artisan make:controller Customer/CustomersController

p49WSdebVj.png!large

安裝 guzzle 擴充套件包

composer require guzzlehttp/guzzle

在 app/Http/Controllers/Admin 下新建 Traits/TokenTrait,新增如下程式碼

<?php

namespace App\Http\Controllers\Admin\Traits;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;

trait TokenTrait
{
    public function authenticate()
    {
        $client = new Client();

        try {
            // 請求本地的 passport token
            $url = request()->root() . '/admin/oauth/token';
            $password_client = \DB::table('oauth_clients')->where('name', 'passport-admin')->first();
            $params = [
                'grant_type' => 'password', // 認證型別 passport
                'client_id' => $password_client->id,
                'client_secret' => $password_client->secret,
                'scope' => 'admin', // 設定 token 作用域
                'username' => request('username'),
                'password' => request('password'),
            ];

            $respond = $client->request('POST', $url, ['form_params' => $params]);
        } catch (RequestException $exception) {
            abort(401, '系統異常');
        }

        if ($respond->getStatusCode() !== 401) {
            return json_decode($respond->getBody()->getContents(), true);
        }

        abort(401, '賬號或密碼錯誤');
    }

    public function getRefreshToken()
    {
        $client = new Client();

        try {
            // 請求本地的 passport token
            $url = request()->root() . '/admin/oauth/token';
            $password_client = \DB::table('oauth_clients')->where('name', 'passport-admin')->first();
            $params = [
                'grant_type' => 'refresh_token',// 認證型別 refresh_token
                'client_id' => $password_client->id,
                'client_secret' => $password_client->secret,
                'scope' => 'admin', // 設定 token 作用域
                'refresh_token' => request('refresh_token')
            ];
            $respond = $client->request('POST', $url, ['form_params' => $params]);
        } catch (RequestException $exception) {
            abort(401, '系統異常');
        }
        if ($respond->getStatusCode() !== 401) {
            return json_decode($respond->getBody()->getContents(), true);
        }
        abort(401, 'refresh token 錯誤');
    }
}

在 app/Http/Controllers/Customer 下新建 Traits/TokenTrait,新增如下程式碼


namespace App\Http\Controllers\Customer\Traits;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;

trait TokenTrait
{
    public function authenticate()
    {
        $client = new Client();

        try {
            $url = request()->root() . '/customer/oauth/token';
            $password_client = \DB::table('oauth_clients')->where('name', 'passport-customer')->first();
            $params = [
                'grant_type' => 'password', // 認證型別 passport
                'client_id' => $password_client->id,
                'client_secret' => $password_client->secret,
                'scope' => 'customer', // 設定 token 作用域
                'username' => request('username'),
                'password' => request('password'),
            ];

            $respond = $client->request('POST', $url, ['form_params' => $params]);
        } catch (RequestException $exception) {
            abort(401, '系統異常');
        }

        if ($respond->getStatusCode() !== 401) {
            return json_decode($respond->getBody()->getContents(), true);
        }

        abort(401, '賬號或密碼錯誤');
    }

    public function getRefreshToken()
    {
        $client = new Client();

        try {
            // 請求本地的 passport token
            $url = request()->root() . '/customer/oauth/token';
            $password_client = \DB::table('oauth_clients')->where('name', 'passport-customer')->first();
            $params = [
                'grant_type' => 'refresh_token',// 認證型別 refresh_token
                'client_id' => $password_client->id,
                'client_secret' => $password_client->secret,
                'scope' => 'customer', // 設定 token 作用域
                'refresh_token' => request('refresh_token')
            ];
            $respond = $client->request('POST', $url, ['form_params' => $params]);
        } catch (RequestException $exception) {
            abort(401, '系統異常');
        }
        if ($respond->getStatusCode() !== 401) {
            return json_decode($respond->getBody()->getContents(), true);
        }
        abort(401, 'refresh token 錯誤');
    }
}

備註

其實此處請求的時候是有點問題的,用 guzzle 請求時如果不正確的引數是會返回 http 401 狀態碼以及報錯,然後 guzzle 如果不是 http 200 的返回,都是會丟擲異常的

所以此處丟擲的異常更準確的是 賬號密碼或 refresh_token 錯誤,最下面的 abort() 也是不會執行的。

在 app/Http/Controllers/Admin 下新建 Controller.php,新增如下程式碼

<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller as BaseController;

class Controller extends BaseController
{

}

在 app/Http/Controllers/Customer 下新建 Controller.php,新增如下程式碼

<?php

namespace App.ttp.ontrollers.ustomer;

use App.ttp.ontrollers.ontroller as BaseController;

class Controller extends BaseController

{

}

修改 app/Http/Controller/Admin/AuthController.php 為如下程式碼

<?php

namespace App\Http\Controllers\Admin;

use Illuminate\Http\Request;

use App\Admin;
use App\Http\Controllers\Admin\Traits\TokenTrait;
use Auth;
use Illuminate\Support\Facades\Hash;

class AuthController extends Controller
{
    use TokenTrait;

    public function login(Request $request)
    {
        // 根據使用者名稱或者郵箱登入
        $admin = Admin::orWhere('username', $request->username)
            ->orwhere('email', $request->username)
            ->firstOrFail();

        // 檢驗密碼是否正確,錯誤返回 401 和報錯資訊
        if (!Hash::check($request->password, $admin->password)) {
            return response()->json([
                'message' => '使用者名稱或密碼錯誤'
            ], 401);
        }

        $token = $this->authenticate();
        return response()->json($token);
    }

    public function refresh()
    {
        // 獲取 token
        $token = $this->getRefreshToken();
        return response()->json($token);
    }

    public function logout()
    {
        if (Auth::guard('admin')->check()) {
            Auth::guard('admin')->user()->token()->delete();
        }

        return  response()->noContent();
    }
}

修改 app/Http/Controller/Customer/AuthController.php 為如下程式碼

<?php

namespace App\Http\Controllers\Customer;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

use App\Customer;
use App\Http\Controllers\Customer\Traits\TokenTrait;
use Auth;
use Illuminate\Support\Facades\Hash;
class AuthController extends Controller
{
    use TokenTrait;

    public function login(Request $request)
    {
        // 根據使用者名稱或者郵箱登入
        $customer = Customer::orWhere('username', $request->username)
            ->orwhere('email', $request->username)
            ->firstOrFail();

        // 檢驗密碼是否正確,錯誤返回 401 和報錯資訊
        if (!Hash::check($request->password, $customer->password)) {
            return response()->json([
                'message' => '使用者名稱或密碼錯誤'
            ], 401);
        }

        $token = $this->authenticate();
        return response()->json($token);
    }

    public function refresh()
    {
        // 獲取 token
        $token = $this->getRefreshToken();
        return response()->json($token);
    }

    public function logout()
    {
        if (Auth::guard('customer')->check()) {
            Auth::guard('customer')->user()->token()->delete();
        }

        return  response()->noContent();
    }
}

好了,激動人心的時刻到了!

開啟 postman 測試, 我分別建立了 6 個請求,具體看 url 和引數應該能明白

2lbONWWfDg.png!largeJJTjBhtls6.png!largeIgxZis6ah2.png!largeNT5DTYaShW.png!largep8Rq9va6ys.png!large9t9y3UBIyb.png!largeUBvu4gDEmG.png!large

ok,按照預期的該返回的返回,不通過的也沒通過

接下來我們用這些 token 獲取使用者資訊

p54YTCOupW.png!largeK6sF5dKXdR.png!largehxE2SR69nR.png!largeMwHzY22Pec.png!large1JQavAbVSk.png!largezpdtODkDK8.png!large

圖 5 為我用 admin1 的 token 請求 customer 的介面

圖 6 為我用 customer1 的 token 請求 admin 的介面

都是無效的。

接下來驗證重新整理 token

admin1 的 refresh_token

hHj4dVzNQ5.png!large

再請求一下

KEY0x47SeD.png!large

customer1 的 refersh_token

WAv3vnbJrG.png!largejZXsOS83oX.png!large

ok, 完工!

總結

  • 雖然 refresh_token 不能重新刷出來,但是之前沒過期的 access_token 其實依然會有效

  • 個人覺得這個並不如 dingoapi + jwt (也就是第三本 api 的教程)好用,本文只說明怎麼使用 passport 進行多端驗證。像丟擲異常,沒有自定義返回碼等還有一大堆未完善的東西。

參考資料

部落格:Laravel5.5+passport 放棄 dingo 開發 API 實戰,讓 API 開發更省心 重點感謝

部落格:Laravel Passport API 認證使用小結

部落格:Laravel Passport 多表使用者認證踩坑

部落格:passport API 認證 -- 多表登入

部落格:Laravel 5.5 使用 Passport 實現 Auth 認證

Passport OAuth 認證

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

相關文章