故事发生月黑风高的…..啊呸,扯题了。简单来说前几天在配置JWT-Auth的时候,因为自己建的用户表没有用国际标准的’password’字段,而是自己随便起了个userPassword,导致不是在挖坑就是在挖坑的路上.不得不阅读其源码解决问题。

前置条件

不过结构复杂的Laravel,里面运用了许多魔术方法及其核心的Ioc容器,阅读前拿到以下几种工具才好动手:

  • 读一下Laravel作者的From Apprentice To Artisan(感谢中文翻译者) ,宛如laravel中的行动纲领
  • Laravel自带helper函数dd(),用于单步调试,运行完立即退出脚本,方便知道某个方法究竟有没有执行或者取值是什么
  • phpstorm ,虽然遇到Laravel一片黄,不过能查到追踪到多少代码算多少,O(∩_∩)O哈哈~
  • 遇到某个类中有依赖注入的,二话不说先找其服务提供者,看看服务提供者的注册方法中将注入的接口(Contract)绑定的是哪个类,为了方便解释,先来个例子~
1
2
3
4
5
6
7
8
9
class A {
protected $test;
// 这个Test不是一个接口算我输
public function __construct(Test $test)
{
$this->test=$test;
}
}

上文注入的Test,用phpstorm的ctrl+b追踪到的肯定是个接口.那么实际传给这个构造方法的实例是什么呢?在这个类附近找找provider字眼的类不会太远(看起来不太靠谱但是却有效,哈哈哈),或者去/config/app.php里面找找

1
2
3
4
5
6
7
8
9
10
11
12
//假装我找到了对应的provider
class TestServiceProvider extends ServiceProvider
{
//关键是找register的内容
public function register()
{
$this->app->bind('Test',function(){
// 假装有个叫做TestImplemet的类实现了上文的Test接口
return new TestImplement();
});
}
}

上文register方法里的bind,都是Laravel的Ioc容器(控制反转容器)的骚操作,简单地理解是Laravel帮你管理类实例,管理他们的生命周期,不用你到处new来new去,耦合代码,目的是彻底模块化,一个类只负责自己的东西,自己类里尽量不new别模块的类。 就是作者所说的严守类的边界(Respect Boundaries)

回归正题原来往A类里注入的是TestImplement类的实例,原来似李啊TestImplement,赶紧给我负责吖混蛋!(感觉污污的..Orz)

所以基本方针就这样,掌握了工具之后,就开始填坑吧!

遇到问题

环境:Laravel5.2
依赖: JWT-Auth 1.0.0-beta.3

当我使用了JWT-Auth之后,由于模型里的密码字段是adminPass,在注册好服务提供者,做好各种配置之后,使用JWTAuth::attempt()方法验证账号密码时,我填入的数组是

1
2
3
4
[
"phone" => 'xxxxxx', // 此处充当账号
"adminPass" => 'xxxxxx'
]

然后attempt方法一直返回false,按照常理账号密码没有错的话,jwt会自动生成token返回,在确认其他配置没问题的情况下,只能硬着头皮去翻源码,逐步测试。

一步一步溯源

根据attempt线索追踪根源

因为使用了先找出JWTAuth::attempt()所在的类.由于此处使用了JWTAuth的门面,我们先去JWTAuth里面看看它要拿的是哪个类

Laravel的门面其实就是为服务类提供”静态”接口,看似我们不需要实例化对象,然后再用A::method()这样的类似静态方法的方式调用服务类的方法。其实质是laravel从用户的门面调用方法中,拿到用户的要调用的方法以及参数,然后从Ioc容器里拿到对应的服务类实例,然后使用php灵活的回调函数,让这个实例执行用户填入的方法及其参数,具体的源码分析会在之后浅读一蛤然后再写篇博客(挖坑)

1
2
3
4
5
6
7
8
9
10
11
12
13
class JWTAuth extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
//经过Facade(门面)一系列操作,向Ioc容器中拿出注册名叫tymon.jwt.auth的实例
return 'tymon.jwt.auth';
}
}

耐心地找到Tymon\JWTAuth\Providers\AbstractServiceProvider,找到以下注册信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Register the bindings for the main JWTAuth class.
*
* @return void
*/
protected function registerJWTAuth()
{
$this->app->singleton('tymon.jwt.auth', function ($app) {
return new JWTAuth(
$app['tymon.jwt.manager'],
$app['tymon.jwt.provider.auth'],
$app['tymon.jwt.parser']
);
});
}

翻翻Tymon\JWTAuth的目录,所以门面拿到的实例就是Tymon\JWTAuth\JWTAuth.php

诚不欺我,果然JWTAuth.php 有attempt方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Attempt to authenticate the user and return the token.
*
* @param array $credentials
*
* @return false|string
*/
public function attempt(array $credentials)
{
if (! $this->auth->byCredentials($credentials)) {
return false;
}
return $this->fromUser($this->user());
}

侦查手段(套路)

$credentials数组就是我们问题里传入验证数组,验证数组又传给了$this->auth->byCredentials($credentials),这个方法,相信大家已经知道套路了吧?没错他这个JWTAuth又注入了一个类实例来执行byCredentials方法,所以执行我们的套路

  1. 先翻一下构造器,注入的是什么接口
  2. 然后找找附近的provider,给这个接口绑定的是什么类实例
  3. 找到类实例,就是实际注入JWTAuth类的实例

    所以通过一顿操作,JWTAuth.php的构造器如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @var \Tymon\JWTAuth\Contracts\Providers\Auth
*/
protected $auth;
/**
* @param \Tymon\JWTAuth\Manager $manager
* @param \Tymon\JWTAuth\Contracts\Providers\Auth $auth
* @param \Tymon\JWTAuth\Http\Parser\Parser $parser
*
* @return void
*/
public function __construct(Manager $manager, Auth $auth, Parser $parser)
{
parent::__construct($manager, $parser);
$this->auth = $auth;
}

所以锁定\Tymon\JWTAuth\Contracts\Providers\Auth,继续翻AbstractServiceProvider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
protected function registerAliases()
{
$this->app->alias('tymon.jwt', JWT::class);
$this->app->alias('tymon.jwt.auth', JWTAuth::class);
$this->app->alias('tymon.jwt.provider.jwt', JWTContract::class);
$this->app->alias('tymon.jwt.provider.auth', Auth::class);
$this->app->alias('tymon.jwt.provider.storage', Storage::class);
$this->app->alias('tymon.jwt.manager', Manager::class);
$this->app->alias('tymon.jwt.blacklist', Blacklist::class);
$this->app->alias('tymon.jwt.payload.factory', Factory::class);
$this->app->alias('tymon.jwt.validators.payload', PayloadValidator::class);
}
protected function registerAuthProvider()
{
$this->app->singleton('tymon.jwt.provider.auth', function () {
return $this->getConfigInstance('providers.auth');
});
}
protected function getConfigInstance($key)
{
$instance = $this->config($key);
if (is_string($instance)) {
return $this->app->make($instance);
}
return $instance;
}
protected function config($key, $default = null)
{
return config("jwt.$key", $default);
}
// 此方法在helper中
function config($key = null, $default = null)
{
if (is_null($key)) {
return app('config');
}
if (is_array($key)) {
return app('config')->set($key);
}
return app('config')->get($key, $default);
}

上面一连串貌似很复杂,其实就是\Tymon\JWTAuth\Contracts\Providers\Auth::class起个别名叫做tymon.jwt.provider.auth,然后registerAuthProvider方法就是正式的绑定方法,通过一连串操作,最终是从/config/jwt.php(如果没用publish命令的,则访问tymon目录里的config) 拿出providers键名里,auth键名的东西

1
2
3
4
5
6
7
8
9
10
//就是Tymon\JWTAuth\Providers\Auth\Illuminate::class
'providers' => [
'jwt' => Tymon\JWTAuth\Providers\JWT\Namshi::class,
'auth' => Tymon\JWTAuth\Providers\Auth\Illuminate::class,
'storage' => Tymon\JWTAuth\Providers\Storage\Illuminate::class,
]

laravel自带的helper中的config十分实用,一般我们开发是都是把配置数据以数组形式存入/config/脚本里,然后用config函数来通过键名(通常写法是xxxx.xxx.xx)读取对应的值

所以回到我们的目的attempt需要注入的auth,而注入的auth实际是Tymon\JWTAuth\Providers\Auth\Illuminate::class,同样的套路,里面的byCredentials方法里面注入了\Illuminate\Contracts\Auth\Guard,这个接口是属于Laravel的,而不是JWT内部的,这就是为什么JWT-auth切合了Laravel自带的Auth的原因,然后通过AbstractServiceProvider,我们可以知道,Laravel注册了自己的Auth时会翻一下配置文件/config/auth.php,然后根据guard里的driver来选择验证驱动,这就是为什么配置JWT-auth时需要配置/config/auth.php的原因

举一反三,找出罪魁祸首

选择jwt作为guard驱动时,根据上面一流程的套路,就可以发现JWTAuth里面用到的byCredentials方法来自于JWTGuard,然后JWTGuard属于Laravel的拓展Guard,也是使用Laravel的一些功能,例如EloquentUserProvider,这个就是将我们的传入的数组与数据作对比的服务提供类,用到这两个方法retrieveByCredentials,validateCredentials

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//EloquentUserProvider.php
public function retrieveByCredentials(array $credentials)
{
if (empty($credentials)) {
return;
}
$query = $this->createModel()->newQuery();
foreach ($credentials as $key => $value) {
if (! Str::contains($key, 'password')) {
$query->where($key, $value);
}
}
return $query->first();
}
public function validateCredentials(UserContract $user, array $credentials)
{
$plain = $credentials['password'];
return $this->hasher->check($plain, $user->getAuthPassword());
}

介绍下这两个方法的作用:

  • retrieveByCredentials,通过验证数组,查询数据库(查询条件除去password)
  • validateCredentials,拿到的数据与验证数组作对比,包括密码,当然密码是使用hash的check方法来检查的

结案陈词

可见validateCredentials方法里$credentials[‘password’]这里就是限定我们attempt一定填入password字段名的原因,不填入这个字段名会报password index not found的异常,至于我传入的[‘phone’=>’xxx’,’adminPass’=>’xxxx’],没有报异常,是因为上面的retrieveByCredentials方法,正常会把非’password’字段纳入查询,所以把adminPass,导致方法返回false,导致于下面的validateCredentials方法因主调方(没记错的话主调方是JWTGuard里的attempt方法)的条件控制而进入不了,因而不报异常,直接返回false

总结&收获

通过这次阅读,让我接触到了laravel的核心部分,服务提供者,依赖注入等等,以及大概了解了laravel自带的权限认证时怎样的。其内部十分复杂,突出的一点是子模块特别的多!其实这体现了Laravel作者Taylor Otwell的设计理念,严守边界,一个类只实现自己的东西,绝不干涉其他类,以及利用了大量的面向接口编程,这种松耦合在开发拓展(遵循开放封闭原则)的时候,也能易于维护。唯一的缺点感觉就是不易于快速入门了,需要慢慢翻看。总的来说耐心是第一要务,没有耐心研究,什么都白谈~