关于php:关于Laravel框架中Guard的底层实现

51次阅读

共计 12745 个字符,预计需要花费 32 分钟才能阅读完成。

1. 什么是 Guard

Laravel/Lumen 框架中,用户的登录 / 注册的认证根本都曾经封装好了,开箱即用。而登录 / 注册认证的外围就是:

  1. 用户的注册信息存入数据库(注销)
  2. 从数据库中读取数据和用户输出的比照(认证)

上述两步是登录 / 注册的根本,能够看到都会波及到数据库的操作,这两步框架底层曾经帮咱们做好了,而且思考到了很多状况,比方用户认证的数据表不是 user 表 而是 admin_user,认证字段是phone 而不是 email,等等一些问题都是Guard 所要解决的,通过 Guard 能够指定应用哪个数据表什么字段等,Guard能非常灵活的构建一套本人的认证体系。

艰深地讲,就是这样:Guard就像是小区的门卫大叔,冷酷无情,不认人只认注销信息 。进小区之前大叔须要先查看你的身份,验证不通过大叔就不让你进去。如果是走路 / 骑车进去, 大叔 1 须要查看你的 门禁卡 ,他拿出 记录了小区所有业主门禁卡信息的本子 查看你这个门禁卡信息有没有在这个本子上;如果你开车进去,大叔 2 就从 记录了所有业主车牌号的本子 中查看你的 车牌号 ,所以新业次要小区了须要告知门卫大叔们你的门禁卡信息或者车牌号,要不然 大叔 2 不让你进。如果是物业管理员要进小区,门卫大叔 3 也只认注销信息,管理员出示他的 管理员门禁卡 ,门卫大叔就会查看 记录了管理员门禁卡信息的本子

下面讲的对应了框架中的多用户认证:

  1. 走路 / 骑车的人 -> 门禁卡
  2. 开车的人 -> 车牌号
  3. 物业管理员 -> 门禁卡

门禁卡和车牌号都是不同的认证形式,而门卫大叔查看的本子就对应了不同数据库中的用户信息,这样讲是不是更容易了解了。

Lumen/Laravel中以中间件(Middleware)的形式提供了非常灵活的认证,通过简略的配置就能够切换多个认证。

注:本文所应用的是:Laravel 7.29

2. Guard 工作流程

说了这么多,附上一张手工制作的流程图:

从图中能够看到,一个 Guard 会波及到三个局部,别离是:

  1. Guard实现自身
  2. User Provider用户提供者,指定哪个数据表以什么形式获取(eloquent/database
  3. Authenticatable接口规定那些货色能够被认证,就是实现它的接口嘛

2. 从配置说起

深刻底层代码之前,先从配置文件讲起。认证的配置次要在 /config/auth.php 中,外面能够定义各种认证的 门卫大叔(guard)

// /config/auth.php

'guards' => [
  'user' => [
    'driver' => 'session',
    'provider' => 'users',
   ],
  'admin' => [
    'driver' => 'token',
    'provider' => 'admin_users',
  ],
],

'providers' => [
  'users' => [
    'driver' => 'eloquent',
    'model' => App\Models\User::class,
//    'table' => 'user'
  ],
  
  'admin_users' => [
    'driver' => 'eloquent',
    'model' => App\Models\AdminUser::class,
  ],
],

配置中定义了两个门卫 useradmindriver字段设置门卫的认证零碎,默认提供两种 sessesiontokenprovider定义的就是下面说的 本子 ,保留所有的认证用户,provider 上面的 drive 定义认证用户如何获取,有两种形式 databaseeloquent形式,个别都是用第二种,model定义 eloquent 形式应用的数据模型,如果 driverdatabase,就要设置 table 指定数据库表。如果没有代码中没有指定用哪个门卫,就会应用默认的门卫大爷:

'defaults' => [
  'guard' => 'users',
  'passwords' => 'users',
],

3. 应用 Guard 例子

咱们以 Laravelauth中间件例子来简略说一下:

Route::get('/user/profile', 'UserController@profile')->middleware('auth');

4. 剖析

当发动 /user/profile 这个申请时,在进入 UserController::profile 办法前,会调用 auth 中间件,auth定义在 \app\Http\Kernel.php 中:

// \app\Http\Kernel.php

protected $routeMiddleware = [
  'auth' => \App\Http\Middleware\Authenticate::class,
  'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
  'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
  'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
  ...
];

对应解决脚本是\App\Http\Middleware\Authenticate::class

// \app\Http\Middleware\Authenticate.php

/**
* Handle an incoming request.
*
* @param  \Illuminate\Http\Request  $request
* @param  \Closure  $next
* @param  string[]  ...$guards
* @return mixed
*
* @throws \Illuminate\Auth\AuthenticationException
*/
public function handle($request, Closure $next, ...$guards)
{$this->authenticate($request, $guards);

  return $next($request);
}

Laravel中中间件的解决入口都是 handle 办法,参数中会一数组模式传过来多个应用的guard,比方这样:

Route::get('/user/profile', 'UserController@profile')->middleware('auth:session,foo,bar');

middleware()中冒号前后别离是中间件和参数。

handle办法很简略嘛,就是调用了authenticate()

// \Illuminate\Auth\Middleware\Authenticate.php

/**
* Determine if the user is logged in to any of the given guards.
*
* @param  \Illuminate\Http\Request  $request
* @param  array  $guards
* @return void
*
* @throws \Illuminate\Auth\AuthenticationException
*/
protected function authenticate($request, array $guards)
{if (empty($guards)) {$guards = [null];
  }

  foreach ($guards as $guard) {if ($this->auth->guard($guard)->check()) {return $this->auth->shouldUse($guard);
    }
  }

  $this->unauthenticated($request, $guards);
}

authenticate()办法遍历传过来的 guard,而后check(),只有满足其中一个,就间接返回,否则就会抛出AuthenticationException 异样。

⚠️留神

$this->auth->guard($guard)->check()

这个是 要害 ,它是通过在auth 属性上链式调用的,咱们来「公众号」(正义的程序猿)一步一步剖析下:

// \Illuminate\Auth\Middleware\Authenticate.php

namespace Illuminate\Auth\Middleware;

use Closure;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Contracts\Auth\Factory as Auth;
use Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests;

class Authenticate implements AuthenticatesRequests
{
    /**
     * The authentication factory instance.
     *
     * @var \Illuminate\Contracts\Auth\Factory
     */
    protected $auth;

    /**
     * Create a new middleware instance.
     *
     * @param  \Illuminate\Contracts\Auth\Factory  $auth
     * @return void
     */
    public function __construct(Auth $auth)
    {$this->auth = $auth;}
  
  ...
}

这里的 $auth 其实是 \Illuminate\Contracts\Auth\Factory 接口的一个实例,通过构造函数注入进来,通过 dd($this->auth) 形式发现这个其实就是 Illuminate\Auth\AuthManager 实例,它实现了 Illuminate\Contracts\Auth\Factory 接口:

// \Illuminate\Contracts\Auth\Factory.php

namespace Illuminate\Contracts\Auth;

interface Factory
{
    /**
     * Get a guard instance by name.
     *
     * @param  string|null  $name
     * @return \Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard
     */
    public function guard($name = null);

    /**
     * Set the default guard the factory should serve.
     *
     * @param  string  $name
     * @return void
     */
    public function shouldUse($name);
}

这个接口有 guard() 办法,所以下面能够间接链式调用。

通过接口定义的申明,咱们能够晓得 guard() 返回 \Illuminate\Contracts\Auth\Guard 或者 \Illuminate\Contracts\Auth\StatefulGuard 这两个接口,具体在 AuthManager 中的实现是这样的:

// \Illuminate\Auth\AuthManager.php

/**
* Attempt to get the guard from the local cache.
*
* @param  string|null  $name
* @return \Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard
*/
public function guard($name = null)
{$name = $name ?: $this->getDefaultDriver();

  return $this->guards[$name] ?? $this->guards[$name] = $this->resolve($name);
}

通过咱们在 middleware() 中传过来的参数创立对应的 guard 实例,没有就是默认 driver 对应的guard,最初check()

这节最初讲一下

AuthManager是什么时候创立的?

Laravel框架初始化时,很多服务都是以服务提供者(ServiceProvider)的模式创立的,AuthManager就是 AuthServiceProvider 创立的:

// \Illuminate\Auth\AuthServiceProvider.php

namespace Illuminate\Auth;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {$this->registerAuthenticator();
      ....
    }

    /**
     * Register the authenticator services.
     *
     * @return void
     */
    protected function registerAuthenticator()
    {$this->app->singleton('auth', function ($app) {
        // Once the authentication service has actually been requested by the developer
        // we will set a variable in the application indicating such. This helps us
        // know that we need to set any queued cookies in the after event later.
        $app['auth.loaded'] = true;

        return new AuthManager($app);
      });
      
      ....
    }
  
  ....
}

AuthServiceProvider中在注册时调用 registerAuthenticator(),创立auth 单例指向 AuthManager 实例。

通过下面的一波剖析,咱们晓得 guard 的创立是受 AuthManager 治理的,AuthManager在这里的指摘就是解析 driver 并创立guard

所以当初整个 middleware('auth') 的流程大抵如下:

5. Guard 接口

下面说到 AuthManager 创立了 guard,而后调用check(),我先当初来剖析下Guard。还是那句话,不论下层业务代码如许简单,底层的接口往往是很简略的。Lumen/Laravel 框架中大部分接口被设计成是一种 契约 (Contracts)Guard 也一样的,它的代码在 \vendor\illuminate\contracts\Auth\Guard.php 文件中,只有 6 个办法

// \Illuminate\Contracts\Auth\Guard.php

namespace Illuminate\Contracts\Auth;

interface Guard
{
  // 判断以后用户是否登录
  public function check();
  // 判断以后用户是否是游客(未登录)public function guest();
  // 获取以后认证的用户
  public function user();
  // 获取以后认证用户的 id,严格来说不肯定是 id,应该是这个模型中的主键字段
  public function id();
  // 用户验证
  public function validate(array $credentials = []);
  // 设置以后认证过的用户
  public function setUser(Authenticatable $user);
}

很简略,有木有~同样,还有一个 StatefulGuard 接口,继承自 Guard 接口并加了几个有状态的办法,代表有状态,就是每次申请都带有用户的状态信息比方session,代码如下:

// Illuminate\Contracts\Auth\StatefulGuard.php

namespace Illuminate\Contracts\Auth;

interface StatefulGuard extends Guard
{
  // 指定数据验证
  public function attempt(array $credentials = [], $remember = false);
  // 将这一次 request 验证通过登录,不会保留 session/cookie
  public function once(array $credentials = []);
  // 登录
  public function login(Authenticatable $user, $remember = false);
  // 应用 id 登录
  public function loginUsingId($id, $remember = false);
  // 和 once()一样,不过是用 id
  public function onceUsingId($id);
  // 通过 remember cookie 登录
  public function viaRemember();
  // 登记
  public function logout();}

UML图大抵如下:

6. Guard 接口的相干实现

底层接口着实简略,再来剖析下下层的实现代码,框架中默认实现了几个 Guard,比方 Web 开发用到的SessionGuard,接口开发用到的TokenGuard,这些都实现自\Illuminate\Contracts\Auth 或者\Illuminate\Contracts\Auth\StatefulGuard,曾经满足咱们日常所需了。

几个 Guardcheck()办法都是一样的,都定义在 GuardHelpers 这个 Trait 中:

// \Illuminate\Auth\GuardHelpers.php

/**
* Determine if the current user is authenticated.
*
* @return bool
*/
public function check()
{return ! is_null($this->user());
}

user()就是在不同的 Guard 中实现了,前面也次要看这个办法。

什么是 Trait:

你能够了解成一系列办法的汇合,就是把常常应用到的反复办法整合起来,在 class 外面间接 use 应用,上下文还是援用它的那个 class,缩小了反复代码量,而且比 class 更轻量,不须要 new 在应用。

6.1 RequestGuard.php

RequestGuard认证一个 http 申请,具体怎么认证,它是通过 callback 实现的,认证逻辑在 callback 中间接放到了下层让用户自定义,UML图:

看代码实现也很简略:

// \Illuminate\Auth\RequestGuard.php

/**
* Get the currently authenticated user.
*
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function user()
{
  // If we've already retrieved the user for the current request we can just
  // return it back immediately. We do not want to fetch the user data on
  // every call to this method because that would be tremendously slow.
  if (! is_null($this->user)) {return $this->user;}

  return $this->user = call_user_func($this->callback, $this->request, $this->getProvider()
  );
}

RequestGuard很多文章都是一笔带过,这【公众号)里我说(正义的程序猿)一下,通常咱们应用不到 RequestGuard,只有在自定义Guard 时才用得上。

应用形式如下

  1. AuthServiceProvider中注册自定义的guard,设置名称和callback
// App\Providers\AuthServiceProvider.php

use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

/**
 * Register any application authentication / authorization services.
 *
 * @return void
 */
public function boot()
{$this->registerPolicies();

    Auth::viaRequest('custom-token', function ($request) {return User::where('my-token', $request->my_token)->first();});
}
  1. auth.php中配置自定义 guard
'guards' => [
    'my-api' => ['driver' => 'custom-token',],
],
  1. 应用

还是下面的例子:

Route::get('/user/profile', 'UserController@profile')->middleware('auth:my-api');

最初在认证的时候就会间接应用咱们设置的 callback 了。

下面 viaRequest() 也是定义 AuthManager 中:

// \Illuminate\Auth\AuthManager.php

/**
* Register a new callback based request guard.
*
* @param  string  $driver
* @param  callable  $callback
* @return $this
*/
public function viaRequest($driver, callable $callback)
{return $this->extend($driver, function () use ($callback) {$guard = new RequestGuard($callback, $this->app['request'], $this->createUserProvider());

    $this->app->refresh('request', $guard, 'setRequest');

    return $guard;
  });
}

6.2 SessionGuard

见名知义,此 guard 是基于 session 的,个别最罕用的就是 (公众号:) 这(正义的程序猿)个了。因为是基于 session 所以是有状态的,所以这个类定义的时候实现了 StatefulGuard 接口,而且加了更多逻辑代码和正文加起来有 800+ 行,

// \Illuminate\Auth\SessionGuard.php

namespace Illuminate\Auth;

use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Contracts\Auth\SupportsBasicAuth;

class SessionGuard implements StatefulGuard, SupportsBasicAuth
{...}

UML图:

用户认证的代码略微简单一点,如下:

// \Illuminate\Auth\SessionGuard.php

/**
* Get the currently authenticated user.
*
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function user()
{if ($this->loggedOut) {return;}

  // If we've already retrieved the user for the current request we can just
  // return it back immediately. We do not want to fetch the user data on
  // every call to this method because that would be tremendously slow.
  if (! is_null($this->user)) {return $this->user;}

  $id = $this->session->get($this->getName());

  // First we will try to load the user using the identifier in the session if
  // one exists. Otherwise we will check for a "remember me" cookie in this
  // request, and if one exists, attempt to retrieve the user using that.
  if (! is_null($id) && $this->user = $this->provider->retrieveById($id)) {$this->fireAuthenticatedEvent($this->user);
  }

  // If the user is null, but we decrypt a "recaller" cookie we can attempt to
  // pull the user data on that cookie which serves as a remember cookie on
  // the application. Once we have a user we can return it to the caller.
  if (is_null($this->user) && ! is_null($recaller = $this->recaller())) {$this->user = $this->userFromRecaller($recaller);

    if ($this->user) {$this->updateSession($this->user->getAuthIdentifier());

      $this->fireLoginEvent($this->user, true);
    }
  }

  return $this->user;
}

梳理下,大抵是先从 session 获取用户的主键 id,而后通过特定的UserProvider 查找用户,查找胜利阐明验证胜利,如果没有,就用 recaller 查问用户,这里就是 remember token 查找,就是登录时“记住我”的那个选项,remember token是保留在 cookie 当中的,如果 remember token 查找胜利,就阐明验证胜利,否则验证失败。

6.3 TokenGuard

TokenGuard也实现了 Guard 接口,实用于无状态的 api 认证,UML图:

因为不要保护状态整个代码就简略很多:

// \Illuminate\Auth\TokenGuard.php

namespace Illuminate\Auth;

use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Http\Request;

class TokenGuard implements Guard
{
  ...
  
  /**
  * Get the currently authenticated user.
  *
  * @return \Illuminate\Contracts\Auth\Authenticatable|null
  */
  public function user()
  {
    // If we've already retrieved the user for the current request we can just
    // return it back immediately. We do not want to fetch the user data on
    // every call to this method because that would be tremendously slow.
    if (! is_null($this->user)) {return $this->user;}

    $user = null;

    $token = $this->getTokenForRequest();

    if (! empty($token)) {
      $user = $this->provider->retrieveByCredentials([$this->storageKey => $this->hash ? hash('sha256', $token) : $token,
      ]);
    }

    return $this->user = $user;
  }
  
  ...
}

先从申请中获取 api_token,再用api_token 从指定的 UserProvider 查找 api_token 对应的用户信息。


至此,LaravelGuard 相干的剖析曾经差不多了,通过剖析它的源码,咱们深刻理解了框架背地的思维,梳理的过程也是学习的过程,对老手而言能疾速把握 guard 的相干常识并疾速上手,对老鸟而言,我感觉这篇文章写的曾经很细了,能更好地理解框架背地的精华写出更优雅的代码。

总结

在深刻学习 Guard 源码后,理解到底层演绎为两个外围,一是 UserProvider,认证用户数据起源,通常是本地数据库,二是认证逻辑,逻辑这块次要就是Guard 来做了。对于自定义 Guard,下面也略微讲了一点,通过AuthManagerviaRequest来做,对于用户数据源咱们也不用拘泥于现有的,咱们也能够将数据源指向 redis 或者近程接口,只有实现相干接口,比方这样:

namespace app\Providers;


use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;

class RedisUserProvider implements UserProvider
{

    /**
     * Retrieve a user by their unique identifier.
     *
     * @param  mixed $identifier
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function retrieveById($identifier)
    {// TODO: 通过 id 取 redis 中对应的用户}
  
    ....
}

也能够从近程接口获取:

class ApiUserProvider implements UserProvider
{

    /**
     * Retrieve a user by their unique identifier.
     *
     * @param  mixed $identifier
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function retrieveById($identifier)
    {// TODO: 通过 id 结构 curl 申请后果}
}

最初,附上一张我在学习过程中总结的 UML 图:

文章首发在我本人的博客:
https://xydida.com/2021/2/24/…

欢送大佬们赏光,文章如果有误,还请大家斧正。

正文完
 0