Laravel + dingoapi + jwt 用户认证无法正确指定 guard 的解决办法

原文链接

由于项目需要,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日更新

当时写这篇文章的时候,确实写的比较随意,也没想到会有同学回复并探讨,当时只是为了解决问题,用了上文中的方法。那么这个问题到底怎么解决比较好,这里来做个比较,通过不同的中间件,来看看各自的结果。

使用 Laravel 自带的中间件 auth:guradName

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中间件进行认证,不会有用户模型找错的问题,但是抛出的异常信息并不友好。

使用 dingoapi.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 自带的 jwt.auth 进行认证

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 协议》,转载必须注明作者和本文链接
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 8
66

我也遇到这样的问题,有没有更好的解决方案呢

5年前 评论

@66 从代码上看,应该使用 laravel 框架的 auth 中间件,我已经把项目全部切换到 auth 中间件了。

5年前 评论
66

@Jeffrey auth:api ? 求教~~能否上段代码

5年前 评论

@66

public function __construct()
{
    return $this->middleware('auth:api');
}
5年前 评论

dingo不是必须的,jwt是比较适合前后端分离的项目,因为你如果用Session你需要使用Redis或其他来存起来,所以看情况使用了,我也发现在LV中间键中没办法使用Guard来实现切接用户认证,不知你JWT中怎么做验证刷新Token的?这个是整个关键,上面代码中,你是使用绑定的方式来改变它,有点暴力

5年前 评论

@sethhu 登录正常,刷新 token 肯定也正常,登录的时候没有使用正确的守卫,token 自然也不能刷新,使用框架自带的认证中间件一切都没有问题。

5年前 评论

你好jeffrey,关于lumen dingo指定guard一块希望请教你一下,按照你的文章我设置了另外的中间件, app()->instance(\Illuminate\Contracts\Auth\Guard::class, auth('doctor'));,但是这个auth函数报错,请问函数是哪里来的呢

5年前 评论

看了你梳理的功能逻辑感觉没说到点上,其实很简单,自己封装个中间件 继承 'Tymon\JWTAuth\Http\Middleware\BaseMiddleware' 中间件即可,因为你在路由上调用'api.auth'本身他最终的验证是要走 'Dingo\Api\Http\Middleware\Auth'中间件的, 在config/api.php 里面配置的auth只是他的验证服务,验证服务也可以自己重写,而他的验证服务实际上也是调用路由实例使用了装饰模式来实现的,在路由实例上获取header上的token以及一些验证,这里验证会抛出异常,然后在通过依赖注入的auth类来实现token验证,这是dingo验证类'Dingo\Api\Auth\Auth',验证不通过会抛出'UnauthorizedHttpException'异常,知道这些了就可以封装自己的中间件了,也可以使用auth函数传入guard来区分数据库, you_auth_middleware:guard 网上有好多无感知刷新token的例子,可以参考.

3年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!