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

原文链接

由于项目需要,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 的形式进行用户认证,我也将我自己的项目全部替换为这种方式了。

《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
讨论数量: 6
66

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

2个月前

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

2个月前
66

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

2个月前

@66

public function __construct()
{
    return $this->middleware('auth:api');
}
2个月前

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

2个月前

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

2个月前

  • 请注意单词拼写,以及中英文排版,参考此页
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`, 更多语法请见这里 Markdown 语法
  • 支持表情,使用方法请见 Emoji 自动补全来咯,可用的 Emoji 请见 :metal: :point_right: Emoji 列表 :star: :sparkles:
  • 上传图片, 支持拖拽和剪切板黏贴上传, 格式限制 - jpg, png, gif
  • 发布框支持本地存储功能,会在内容变更时保存,「提交」按钮点击时清空
  请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!