由於專案需要,api 端的登入使用的是 users 之外的另一張表,在配置了Dingo Api
和 jwt
之後,發現登入之後獲取的使用者是users
表中的,這裡使用的中介軟體是Dingo Api
的 api.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,
],
...
],
至於為什麼 呼叫 guard()
方法能返回 JWTGuard
以及 jwt 是如何進行認證的等等問題, 這就要仔細檢視 Illuminate\Auth\AuthManage
和 Tymon\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 協議》,轉載必須註明作者和本文連結