Laravel + dingoapi + jwt 使用者認證無法正確指定 guard 的解決辦法

Jeffrey發表於2018-07-11

原文連結

由於專案需要,api 端的登入使用的是 users 之外的另一張表,在配置了Dingo Apijwt 之後,發現登入之後獲取的使用者是users表中的,這裡使用的中介軟體是Dingo Apiapi.auth。當然,直接使用框架的auth:api中介軟體是沒有問題的,但是這樣一來是有違使用Dingo的初衷,二來是返回的錯誤資訊永遠是Unauthenticated

於是研究了一下這兩個擴充套件的原始碼,過程很無聊也很漫長,雖然問題很快就找到了,但是沒找到合適(或者說優雅的)解決辦法,總感覺Dingo整合jwt不是很完美,或者有可能是沒有及時作出更新,也不知道對不對。下面是我的解決辦法:

寫了一箇中介軟體,然後在 api.auth 之前呼叫,來更改 Guard 的繫結:

<?php

namespace App\Http\Middleware;

use Closure;

class BindJWTGuard
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        app()->instance(\Illuminate\Contracts\Auth\Guard::class, auth('api'));
        return $next($request);
    }
}

也看見有同學說動態修改配置,改掉預設的guard,這當然也能實現啦!


2018.8.19日更新

當時寫這篇文章的時候,確實寫的比較隨意,也沒想到會有同學回覆並探討,當時只是為了解決問題,用了上文中的方法。那麼這個問題到底怎麼解決比較好,這裡來做個比較,通過不同的中介軟體,來看看各自的結果。

project\app\Http\Kernel.php 中定義了 auth 這個路由中介軟體

protected $routeMiddleware = [
        'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'auth.jwt' => \App\Http\Middleware\BindJWTGuard::class,
        'ip.white' => \App\Http\Middleware\WhiteList::class,
    ];

所以 auth 這個中介軟體位於 \Illuminate\Auth\Middleware\Authenticate。開啟這個檔案,檢視 handle 方法:

public function handle($request, Closure $next, ...$guards)
    {
        $this->authenticate($guards); // 如果這樣使用中介軟體 auth:api ,那麼 $guards 的值就是 [api]

        return $next($request);
    }

可以看到,guard 以陣列的形式傳入,從這裡可以看出,守衛不僅僅可以傳入一個。接著檢視 authenticate 方法:

 protected function authenticate(array $guards)
    {
        if (empty($guards)) {
            return $this->auth->authenticate();
        }

        foreach ($guards as $guard) {
            if ($this->auth->guard($guard)->check()) {
                return $this->auth->shouldUse($guard);
            }
        }

        throw new AuthenticationException('Unauthenticated.', $guards);
    }

authenticate 方法中可以看出,只要傳入的 guard 中一個生效,就結束並返回,否則就將丟擲 AuthenticationException 異常。那麼這個中間是如何進行使用者認證的呢?這就是通過 Illuminate\Auth\AuthManager 進行統一管理,畫個簡單的流程圖吧!
這裡我使用的 guard 配置如下,認證驅動使用 jwt,使用者供應商使用 wechat_user

// config/auth.php

'guards' => [
      ...
        'client' => [
            'driver' => 'jwt',
            'provider' => 'wechat_users',
        ],
        ...
    ],
'providers' => [
            ...
        'wechat_users' => [
            'driver' => 'eloquent',
            'model' => App\Models\WechatUser::class,
        ],
        ...
    ],

file

至於為什麼 呼叫 guard() 方法能返回 JWTGuard 以及 jwt 是如何進行認證的等等問題, 這就要仔細檢視 Illuminate\Auth\AuthManageTymon\JWTAuth\Providers\LaravelServiceProvider , 然後找到對應的處理類,相信只要細心一定都能理解。

那我們使用這個中介軟體會不會有使用者模型找錯的問題呢?答案是不會的,AuthManage 中呼叫 resolve() 方法時會呼叫如下方法,並傳入正確的名稱和配置。

// Tymon\JWTAuth\Providers\AbstractServiceProvider

 protected function extendAuthGuard()
    {
        $this->app['auth']->extend('jwt', function ($app, $name, array $config) {
            $guard = new JwtGuard(
                $app['tymon.jwt'],
                $app['auth']->createUserProvider($config['provider']), // 這裡正確指定了使用者的供應商
                $app['request']
            );

            $app->refresh('request', $guard, 'setRequest');

            return $guard;
        });
    }

但是使用這個中介軟體,無論什麼原因導致的認證失敗,永遠丟擲AuthenticationException異常,導致這個情況的原因是使用JWTGuard進行認證的時候捕獲了所有的 JWTException 異常並且直接返回了 false(這一點可以翻看翻看原始碼,不貼程式碼了),所以在 auth 中介軟體的authenticate方法中只要認證不通過就執行throw new AuthenticationException('Unauthenticated.', $guards); 這一行。

總結:使用 Laravel 框架自帶的auth中介軟體進行認證,不會有使用者模型找錯的問題,但是丟擲的異常資訊並不友好。

Dingo\Api\Provider\LaravelServiceProvider 入手,找到 api.auth 這個中介軟體其實就是 Dingo\Api\Http\Middleware\Auth
dingo 文件 中可以看到處理 jwt 認證的類為 Dingo\Api\Auth\Provider\JWT,這也是我們寫在 config/api.php 配置中的值。

api.auth 的認證核心為Dingo\Api\Auth\Provider\JWT中的如下方法:

public function authenticate(Request $request, Route $route)
    {
        $token = $this->getToken($request);

        try {
            if (! $user = $this->auth->setToken($token)->authenticate()) {
                throw new UnauthorizedHttpException('JWTAuth', 'Unable to authenticate with invalid token.');
            }
        } catch (JWTException $exception) {
            throw new UnauthorizedHttpException('JWTAuth', $exception->getMessage(), $exception);
        }

        return $user;
    }

通過該方法呼叫 jwt 中的Tymon\JWTAuth\JWTAuth,並且捕獲了所有的異常資訊,然後統一丟擲UnauthorizedHttpException 異常類。Tymon\JWTAuth\JWTAuth 經過了一系列的呼叫最終還是使用 Tymon\JWTAuth\JWTGuard 進行認證,但是使用的是byId() 方法尋找使用者,在這之前解析使用者的一系列操作都已經呼叫,該丟擲的異常都已經丟擲,所以dingo才能捕獲到認證過程中丟擲的異常。但是這樣一來,例項 Tymon\JWTAuth\JWTGuard的時候並沒有正確的傳入我們的守衛配置,所以最後使用了 預設守衛,就會導致使用者模型錯誤。我之前文章裡強制重新繫結了認證守衛,就是為了修改 Tymon\JWTAuth\JWTGuard(已經有同學說我做法太暴力,o(╥﹏╥)o,當時也是為了解決眼前問題嘛!),這樣做其實在一些情況下還是會出錯的,比如在控制器中使用 $this->authorize(),因為在AuthManage 中並沒有正確設定 $userResolver 這個函式。

jwt 也是有認證中介軟體的,我們同樣從服務提供者入手,檢視 Tymon\JWTAuth\Providers\LaravelServiceProvider,檢視該類整合的父類Tymon\JWTAuth\Providers\AbstractServiceProvider,有如下程式碼:

protected $middlewareAliases = [
        'jwt.auth' => Authenticate::class,
        'jwt.check' => Check::class,
        'jwt.refresh' => RefreshToken::class,
        'jwt.renew' => AuthenticateAndRenew::class,
    ];

當我們使用 jwt.auth 的時候,其實和 dingo 中差不多,最終也是呼叫Tymon\JWTAuth\JWTGuard,也會遇到相同的問題(無法找到正確的使用者模型),具體程式碼可以翻看一下原始碼。

我之前一直在想,肯定有地方可以給 JWTGuard 傳入正確的配置的,可惜找來找去也沒有找到,這裡提醒一下初學者,看擴充套件包一般從這個擴充套件的 ServiceProvider 入手,這樣比較容易理解。我的文件能力確實很弱,也許很多人看不明白我寫的啥,請見諒!我也不能確定我寫的全對,如有錯誤之處,還請友好的指出,畢竟大家都是接受不了批評的人嘛,哈哈哈...

我最終的結論是應該使用 auth:guardName 的形式進行使用者認證,我也將我自己的專案全部替換為這種方式了。

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

相關文章