百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

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

ccwgpt 2024-10-15 09:10 27 浏览 0 评论

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)的方式提供了非常灵活的认证,通过简单的配置就可以切换多个认证。

注:本文所讲的都是Lumen的代码,是Laravel精简版,内部实现原理都大差不差

本文所使用的是: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/PHP/Laravel/The-Guard-in-Laravel-framework/

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

相关推荐

RACI矩阵:项目管理中的角色与责任分配利器

作者:赵小燕RACI矩阵RACI矩阵是项目管理中的一种重要工具,旨在明确团队在各个任务中的角色和职责。通过将每个角色划分为负责人、最终责任人、咨询人和知情人四种类型,RACI矩阵确保每个人都清楚自己...

在弱矩阵组织中,如何做好项目管理工作?「慕哲制图」

慕哲出品必属精品系列在弱矩阵组织中,如何做好项目管理工作?【慕哲制图】-------------------------------慕哲制图系列0:一图掌握项目、项目集、项目组合、P2、商业分析和NP...

Scrum模式:每日站会(Daily Scrum)

定义每日站会(DailyScrum)是一个Scrum团队在进行Sprint期间的日常会议。这个会议的主要目的是为了应对Sprint计划中的不断变化,确保团队能够有效应对挑战并达成Sprint目标。为...

大家都在谈论的敏捷开发&Scrum,到底是什么?

敏捷开发作为一种开发模式,近年来深受研发团队欢迎,与瀑布式开发相比,敏捷开发更轻量,灵活性更高,在当下多变环境下,越来越多团队选择敏捷开发。什么是敏捷?敏捷是一种在不确定和变化的环境中,通过创造和响应...

敏捷与Scrum是什么?(scrum敏捷开发是什么)

敏捷是一种思维模式和哲学,它描述了敏捷宣言中的一系列原则。另一方面,Scrum是一个框架,规定了实现这种思维方式的角色,事件,工件和规则/指南。换句话说,敏捷是思维方式,Scrum是规定实施敏捷哲学的...

敏捷项目管理与敏捷:Scrum流程图一览

敏捷开发中的Scrum流程通常可以用一个简单的流程图来表示,以便更清晰地展示Scrum框架的各个阶段和活动。以下是一个常见的Scrum流程图示例:这个流程图涵盖了Scrum框架的主要阶段和活动,其中包...

一张图掌握项目生命周期模型及Scrum框架

Mockito 的最佳实践(mock方法)

记得以前面试的时候,面试官问我,平常开发过程中自己会不会测试?我回答当然会呀,自己写的代码怎么不测呢。现在想想我好像误会他的意思了,他应该是想问我关于单元测试,集成测试以及背后相关的知识,然而当时说到...

EffectiveJava-5-枚举和注解(java枚举的作用与好处)

用enum代替int常量1.int枚举:引入枚举前,一般是声明一组具名的int常量,每个常量代表一个类型成员,这种方法叫做int枚举模式。int枚举模式是类型不安全的,例如下面两组常量:性别和动物种...

Maven 干货 全篇共:28232 字。预计阅读时间:110 分钟。建议收藏!

Maven简介Maven这个词可以翻译为“知识的积累”,也可以翻译为“专家”或“内行”。Maven是一个跨平台的项目管理工具。主要服务于基于Java平台的项目构建、依赖管理和项目信息管理。仔...

Java单元测试框架PowerMock学习(java单元测试是什么意思)

前言高德的技术大佬在谈论方法论时说到:“复杂的问题要简单化,简单的问题要深入化。”这句话让我感触颇深,这何尝不是一套编写代码的方法——把一个复杂逻辑拆分为许多简单逻辑,然后把每一个简单逻辑进行深入实现...

Spring框架基础知识-第六节内容(Spring高级话题)

Spring高级话题SpringAware基本概念Spring的依赖注入的最大亮点是你所有的Bean对Spring容器的存在是没有意识的。但是在实际的项目中,你的Bean必须要意识到Spring容器...

Java单元测试浅析(JUnit+Mockito)

作者:京东物流秦彪1.什么是单元测试(1)单元测试环节:测试过程按照阶段划分分为:单元测试、集成测试、系统测试、验收测试等。相关含义如下:1)单元测试:针对计算机程序模块进行输出正确性检验工作...

揭秘Java代码背后的质检双侠:JUnit与Mockito!

你有没有发现,现在我们用的手机App、逛的网站,甚至各种智能设备,功能越来越复杂,但用起来却越来越顺畅,很少遇到那种崩溃、卡顿的闹心事儿?这背后可不是程序员一拍脑袋写完代码就完事儿了!他们需要一套严谨...

单元测试框架哪家强?Junit来帮忙!

大家好,在前面的文章中,给大家介绍了以注解和XML的方式分别实现IOC和依赖注入。并且我们定义了一个测试类,通过测试类来获取到了容器中的Bean,具体的测试类定义如下:@Testpublicvoid...

取消回复欢迎 发表评论: