Laravel + dingoapi + jwt 用户认证无法正确指定 guard 的解决办法
由于项目需要,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日更新
当时写这篇文章的时候,确实写的比较随意,也没想到会有同学回复并探讨,当时只是为了解决问题,用了上文中的方法。那么这个问题到底怎么解决比较好,这里来做个比较,通过不同的中间件,来看看各自的结果。
使用 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,
],
...
],
至于为什么 调用 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.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 协议》,转载必须注明作者和本文链接
我也遇到这样的问题,有没有更好的解决方案呢
@66 从代码上看,应该使用
laravel
框架的auth
中间件,我已经把项目全部切换到auth
中间件了。@Jeffrey auth:api ? 求教~~能否上段代码
@66
dingo不是必须的,jwt是比较适合前后端分离的项目,因为你如果用Session你需要使用Redis或其他来存起来,所以看情况使用了,我也发现在LV中间键中没办法使用Guard来实现切接用户认证,不知你JWT中怎么做验证刷新Token的?这个是整个关键,上面代码中,你是使用绑定的方式来改变它,有点暴力
@sethhu 登录正常,刷新 token 肯定也正常,登录的时候没有使用正确的守卫,token 自然也不能刷新,使用框架自带的认证中间件一切都没有问题。
你好jeffrey,关于lumen dingo指定guard一块希望请教你一下,按照你的文章我设置了另外的中间件, app()->instance(\Illuminate\Contracts\Auth\Guard::class, auth('doctor'));,但是这个auth函数报错,请问函数是哪里来的呢
看了你梳理的功能逻辑感觉没说到点上,其实很简单,自己封装个中间件 继承 '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的例子,可以参考.