如何在Laravel中创建一个自定义的认证防护

209 阅读10分钟

在这篇文章中, 我们将介绍Laravel框架中的认证系统.这篇文章的主要目的是通过扩展核心认证系统来创建一个自定义的认证防护。

Laravel在核心部分提供了一个非常坚实的认证系统,使基本认证的实现变得轻而易举。事实上, 你只需要运行几个artisan命令就可以建立一个认证系统的脚手架.

此外,该系统本身的设计方式是,你可以扩展它并插入你的自定义认证适配器。这就是我们将在本文中详细讨论的内容。在我们继续深入实现自定义认证防护之前, 我们先来讨论一下Laravel认证系统中的基本元素--防护和提供者.

核心要素:警卫和提供者

Laravel认证系统的核心是由两个元素组成的--卫兵和提供者。

警卫

你可以把守卫看成是一种提供逻辑的方式, 用来识别认证的用户.在核心部分, Laravel提供了不同的守卫, 如session和token.会话防护通过cookies来维护用户在每个请求中的状态, 另一方面, 令牌防护通过检查每个请求中的有效令牌来验证用户的身份.

因此,正如你所看到的,守护定义了认证的逻辑,它没有必要总是通过从后端检索有效的凭证来处理这个问题。你可以实现一个卫兵,它只需检查请求头中是否有特定的东西,并根据这些东西对用户进行认证。

在本文后面,我们将实现一个卫兵,检查请求头中的某些JSON参数,并从MongoDB后端检索有效用户。

提供者

如果防护措施定义了认证逻辑,那么认证提供者就负责从后端存储中检索用户。如果防护要求用户必须对后端存储进行验证,那么检索用户的实现就会进入认证提供者。

Laravel有两个默认的认证提供者-数据库和Eloquent.数据库认证提供者处理的是直接从后端存储中检索用户证书, 而Eloquent提供了一个抽象层来完成必要的工作。

在我们的例子中,我们将实现一个MongoDB认证提供者,从MongoDB后端获取用户凭证。

这就是对Laravel认证系统中的守护者和提供者的基本介绍。从下一节开始, 我们将专注于开发定制的认证防护和提供者!

快速浏览一下文件的设置

让我们快速浏览一下我们将在本文过程中实现的文件列表.

  • config/auth.php。这是一个认证配置文件,我们将在其中添加我们的自定义卫士的条目。
  • config/mongo.php:这是一个保存MongoDB配置的文件。
  • app/Services/Contracts/NosqlServiceInterface.php。这是一个接口,我们的自定义Mongo数据库类实现。
  • app/Database/MongoDatabase.php。它是一个与MongoDB交互的主要数据库类。
  • app/Models/Auth/User.php。它是用户模型类,实现了Authenticable契约。
  • app/Extensions/MongoUserProvider.php。它是一个认证提供者的实现。
  • app/Services/Auth/JsonGuard.php。它是认证保护驱动的实现。
  • app/Providers/AuthServiceProvider.php。这是一个现有的文件,我们将用它来添加我们的服务容器绑定。
  • app/Http/Controllers/MongoController.php。这是一个演示控制器文件,我们将实现它来测试我们的自定义防护。

如果这些文件的列表还没有什么意义,请不要担心,因为我们会在讨论的过程中详细讨论一切。

深入实施

在本节中,我们将讨论所需文件的实现。

我们需要做的第一件事是通知Laravel关于我们的自定义防护。如图所示, 在config/auth.php文件中输入自定义防护的详细信息.

...
...
'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],
 
    'api' => [
        'driver' => 'token',
        'provider' => 'users',
        'hash' => false,
    ],
     
    'custom' => [
      'driver' => 'json',
      'provider' => 'mongo',
    ],
],
...
...

正如你所看到的, 我们已经在custom 关键下添加了我们的自定义卫兵。

接下来,我们需要在providers 部分添加一个相关的提供者条目。

...
...
'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model' => App\User::class,
    ],
    'mongo' => [
        'driver' => 'mongo'
    ],
 
    // 'users' => [
    //     'driver' => 'database',
    //     'table' => 'users',
    // ],
],
...
...

我们已经在mongo 密钥下添加了我们的提供者条目。

最后,让我们把默认的认证防护从web 改为custom

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

当然,这还不能工作,因为我们还没有实现必要的文件。而这正是我们将在接下来的几节中讨论的内容。

设置MongoDB驱动

在本节中,我们将实现与底层MongoDB实例对话的必要文件。

首先,让我们创建config/mongo.php配置文件,它保存了默认的MongoDB连接设置。

<?php
return [
  'defaults' => [
    'host' => '{HOST_IP}',
    'port' => '{HOST_PORT}',
    'database' => '{DB_NAME}'
  ]
];

当然,你需要根据你的设置来改变占位符的数值。

我们不直接创建一个与MongoDB交互的类,而是首先创建一个接口。

创建一个接口的好处是,它提供了一个开发者在实现它时必须遵守的契约。另外,如果需要的话,我们对MongoDB的实现可以很容易地与其他NoSQL实现互换。

继续创建app/Services/Contracts/NosqlServiceInterface.php接口文件,内容如下。

<?php
// app/Services/Contracts/NosqlServiceInterface.php
namespace App\Services\Contracts;
 
Interface NosqlServiceInterface
{
  /**
   * Create a Document
   *
   * @param string $collection Collection/Table Name
   * @param array  $document   Document
   * @return boolean
   */
  public function create($collection, Array $document);
  
  /**
   * Update a Document
   *
   * @param string $collection Collection/Table Name
   * @param mix    $id         Primary Id
   * @param array  $document   Document
   * @return boolean
   */
  public function update($collection, $id, Array $document);
 
  /**
   * Delete a Document
   *
   * @param string $collection Collection/Table Name
   * @param mix    $id         Primary Id
   * @return boolean
   */
  public function delete($collection, $id);
  
  /**
   * Search Document(s)
   *
   * @param string $collection Collection/Table Name
   * @param array  $criteria   Key-value criteria
   * @return array
   */
  public function find($collection, Array $criteria);
}

这是一个相当简单的接口,它声明了一个实现该接口的类必须定义的基本CRUD方法。

现在,让我们用下面的内容来定义app/Database/MongoDatabase.php 类。

<?php
// app/Database/MongoDatabase.php
namespace App\Database;
 
use App\Services\Contracts\NosqlServiceInterface;
 
class MongoDatabase implements NosqlServiceInterface
{
  private $manager;
  private $database;
     
  public function __construct($host, $port, $database)
  {
    $this->database = $database;
    $this->manager = new \MongoDB\Driver\Manager( "mongodb://".$host.":".$port."/".$database );
  }

  /**
   * @see \App\Services\Contracts\NosqlServiceInterface::find()
   */
  public function find($collection, Array $criteria)
  {
    $query = new \MongoDB\Driver\Query($criteria);
    $result = $this->manager->executeQuery($this->database.".".$collection, $query);

    $user = array();
    foreach ($result as $row) {
        $user['username'] = $row->username;
        $user['password'] = $row->password;
    }

    return $user;
  }
 
  public function create($collection, Array $document) {}
  public function update($collection, $id, Array $document) {}
  public function delete($collection, $id) {}
}

当然,我假设你已经安装了MongoDB和相应的MongoDB PHP扩展。

__construct 方法用必要的参数实例化了MongoClient 类。我们感兴趣的另一个重要方法是find 方法,它根据作为方法参数提供的标准来检索记录。

所以这就是MongoDB驱动的实现,我试图让它尽可能的简单。

设置User 模型

遵循认证系统的标准,我们需要实现User 模型,它必须实现Illuminate\Contracts\Auth\Authenticatable 合同。

继续创建一个文件app/Models/Auth/User.php,内容如下。

<?php
// app/Models/Auth/User.php
namespace App\Models\Auth;
 
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use App\Services\Contracts\NosqlServiceInterface;
 
class User implements AuthenticatableContract
{
  private $conn;
  
  private $username;
  private $password;
  protected $rememberTokenName = 'remember_token';
 
  public function __construct(NosqlServiceInterface $conn)
  {
    $this->conn = $conn;
  }
 
  /**
   * Fetch user by Credentials
   *
   * @param array $credentials
   * @return Illuminate\Contracts\Auth\Authenticatable
   */
  public function fetchUserByCredentials(Array $credentials)
  {
    $arr_user = $this->conn->find('users', ['username' => $credentials['username']]);
     
    if (! is_null($arr_user)) {
      $this->username = $arr_user['username'];
      $this->password = $arr_user['password'];
    }
 
    return $this;
  }
 
  /**
   * {@inheritDoc}
   * @see \Illuminate\Contracts\Auth\Authenticatable::getAuthIdentifierName()
   */
  public function getAuthIdentifierName()
  {
    return "username";
  }
  
  /**
   * {@inheritDoc}
   * @see \Illuminate\Contracts\Auth\Authenticatable::getAuthIdentifier()
   */
  public function getAuthIdentifier()
  {
    return $this->{$this->getAuthIdentifierName()};
  }
 
  /**
   * {@inheritDoc}
   * @see \Illuminate\Contracts\Auth\Authenticatable::getAuthPassword()
   */
  public function getAuthPassword()
  {
    return $this->password;
  }
 
  /**
   * {@inheritDoc}
   * @see \Illuminate\Contracts\Auth\Authenticatable::getRememberToken()
   */
  public function getRememberToken()
  {
    if (! empty($this->getRememberTokenName())) {
      return $this->{$this->getRememberTokenName()};
    }
  }
 
  /**
   * {@inheritDoc}
   * @see \Illuminate\Contracts\Auth\Authenticatable::setRememberToken()
   */
  public function setRememberToken($value)
  {
    if (! empty($this->getRememberTokenName())) {
      $this->{$this->getRememberTokenName()} = $value;
    }
  }
 
  /**
   * {@inheritDoc}
   * @see \Illuminate\Contracts\Auth\Authenticatable::getRememberTokenName()
   */
  public function getRememberTokenName()
  {
    return $this->rememberTokenName;
  }
}

你应该已经注意到,App\Models\Auth\User 实现了Illuminate\Contracts\Auth\Authenticatable 合同。

我们的类中实现的大部分方法都是不言自明的。说到这,我们已经定义了fetchUserByCredentials 方法,它从可用的后端检索用户。在我们的例子中,这将是一个MongoDatabase 类,它将被调用以检索必要的信息。

所以这就是User 模型的实现。

设置认证提供者

正如我们前面所讨论的,Laravel认证系统由两个元素组成-guards和provider。

在本节中, 我们将创建一个认证提供者, 处理从后端检索用户的问题.

继续创建一个app/Extensions/MongoUserProvider.php文件,如下图所示。

<?php
// app/Extensions/MongoUserProvider.php
namespace App\Extensions;
 
use Illuminate\Support\Str;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Contracts\Auth\Authenticatable;
 
class MongoUserProvider implements UserProvider
{
  /**
   * The Mongo User Model
   */
  private $model;
 
  /**
   * Create a new mongo user provider.
   *
   * @return \Illuminate\Contracts\Auth\Authenticatable|null
   * @return void
   */
  public function __construct(\App\Models\Auth\User $userModel)
  {
    $this->model = $userModel;
  }
 
  /**
   * Retrieve a user by the given credentials.
   *
   * @param  array  $credentials
   * @return \Illuminate\Contracts\Auth\Authenticatable|null
   */
  public function retrieveByCredentials(array $credentials)
  {
      if (empty($credentials)) {
          return;
      }
 
    $user = $this->model->fetchUserByCredentials(['username' => $credentials['username']]);
 
      return $user;
  }
  
  /**
   * Validate a user against the given credentials.
   *
   * @param  \Illuminate\Contracts\Auth\Authenticatable  $user
   * @param  array  $credentials  Request credentials
   * @return bool
   */
  public function validateCredentials(Authenticatable $user, Array $credentials)
  {
      return ($credentials['username'] == $user->getAuthIdentifier() &&
    md5($credentials['password']) == $user->getAuthPassword());
  }
 
  public function retrieveById($identifier) {}
 
  public function retrieveByToken($identifier, $token) {}
 
  public function updateRememberToken(Authenticatable $user, $token) {}
}

同样,你需要确保自定义提供者必须实现Illuminate\Contracts\Auth\UserProvider 合同。

继续前进,它定义了两个重要的方法--retrieveByCredentialsvalidateCredentials

retrieveByCredentials 方法用于使用User 模型类来检索用户证书,这在前面的章节中已经讨论过。另一方面,validateCredentials 方法被用来根据给定的证书集验证用户。

这就是我们自定义认证提供者的实现。在下一节中,我们将继续创建一个与MongoUserProvider 认证提供者交互的卫兵。

设置认证防护

正如我们前面所讨论的, Laravel认证系统中的卫兵规定了用户是如何被认证的.在我们的案例中, 我们将检查jsondata 请求参数的存在,它应该包含证书的JSON编码的字符串。

在这一节中, 我们将创建一个防护装置,与上一节中刚创建的认证提供者进行交互。

继续创建一个文件app/Services/Auth/JsonGuard.php,内容如下。

<?php
// app/Services/Auth/JsonGuard.php
namespace App\Services\Auth;
 
use Illuminate\Http\Request;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Auth\UserProvider;
use GuzzleHttp\json_decode;
use phpDocumentor\Reflection\Types\Array_;
use Illuminate\Contracts\Auth\Authenticatable;
 
class JsonGuard implements Guard
{
  protected $request;
  protected $provider;
  protected $user;
 
  /**
   * Create a new authentication guard.
   *
   * @param  \Illuminate\Contracts\Auth\UserProvider  $provider
   * @param  \Illuminate\Http\Request  $request
   * @return void
   */
  public function __construct(UserProvider $provider, Request $request)
  {
    $this->request = $request;
    $this->provider = $provider;
    $this->user = NULL;
  }
 
  /**
   * Determine if the current user is authenticated.
   *
   * @return bool
   */
  public function check()
  {
    return ! is_null($this->user());
  }
 
  /**
   * Determine if the current user is a guest.
   *
   * @return bool
   */
  public function guest()
  {
    return ! $this->check();
  }
 
  /**
   * Get the currently authenticated user.
   *
   * @return \Illuminate\Contracts\Auth\Authenticatable|null
   */
  public function user()
  {
    if (! is_null($this->user)) {
      return $this->user;
    }
  }
     
  /**
   * Get the JSON params from the current request
   *
   * @return string
   */
  public function getJsonParams()
  {
    $jsondata = $this->request->query('jsondata');
 
    return (!empty($jsondata) ? json_decode($jsondata, TRUE) : NULL);
  }
 
  /**
   * Get the ID for the currently authenticated user.
   *
   * @return string|null
  */
  public function id()
  {
    if ($user = $this->user()) {
      return $this->user()->getAuthIdentifier();
    }
  }
 
  /**
   * Validate a user's credentials.
   *
   * @return bool
   */
  public function validate(Array $credentials=[])
  {
    if (empty($credentials['username']) || empty($credentials['password'])) {
      if (!$credentials=$this->getJsonParams()) {
        return false;
      }
    }
 
    $user = $this->provider->retrieveByCredentials($credentials);
       
    if (! is_null($user) && $this->provider->validateCredentials($user, $credentials)) {
      $this->setUser($user);
 
      return true;
    } else {
      return false;
    }
  }
 
  /**
   * Set the current user.
   *
   * @param  Array $user User info
   * @return void
   */
  public function setUser(Authenticatable $user)
  {
    $this->user = $user;
    return $this;
  }
}

首先,我们的类需要实现Illuminate\Contracts\Auth\Guard 接口。因此,我们需要定义该接口中声明的所有方法。

这里需要注意的是,__construct 函数需要一个Illuminate\Contracts\Auth\UserProvider 的实现。在我们的例子中,我们将传递一个App\Extensions\MongoUserProvider 的实例,我们将在后面的章节中看到。

接下来,有一个函数getJsonParams ,从名为jsondata 的请求参数中检索用户凭证。由于预计我们会收到一个JSON编码的用户凭证字符串,我们使用json_decode 函数来解码JSON数据。

在验证函数中,我们首先检查的是$credentials 参数的存在。如果它不存在,我们将调用getJsonParams 方法,从请求参数中检索用户凭证。

接下来,我们调用MongoUserProvider 提供者的retrieveByCredentials 方法,从MongoDB数据库后端检索用户。最后,是MongoUserProvider 提供者的validateCredentials 方法,检查用户的有效性。

因此,这就是我们的自定义卫兵的实现。下一节将介绍如何将这些部分拼接起来,形成一个成功的认证系统。

把所有东西放在一起

到目前为止,我们已经开发了自定义认证防护的所有元素,应该可以为我们提供一个新的认证系统。然而,它不会开箱即用,因为我们首先需要使用Laravel服务容器绑定来注册它。

你应该已经知道, Laravel服务提供者是实现必要绑定的正确地方.

继续打开app/Providers/AuthServiceProvider.php文件,允许我们添加认证服务容器绑定。如果它不包含任何自定义的修改,你可以直接用下面的内容替换它。

<?php
// app/Providers/AuthServiceProvider.php
namespace App\Providers;
 
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use App\Services\Auth\JsonGuard;
use App\Extensions\MongoUserProvider;
use App\Database\MongoDatabase;
use App\Models\Auth\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
 
class AuthServiceProvider extends ServiceProvider
{
  /**
   * The policy mappings for the application.
   *
   * @var array
   */
  protected $policies = [
    'App\Model' => 'App\Policies\ModelPolicy',
  ];
 
  /**
   * Register any authentication / authorization services.
   *
   * @return void
   */
  public function boot()
  {
    $this->registerPolicies();
     
    $this->app->bind('App\Database\MongoDatabase', function ($app) {
      return new MongoDatabase(config('mongo.defaults.host'), config('mongo.defaults.port'), config('mongo.defaults.database'));
    });
     
    $this->app->bind('App\Models\Auth\User', function ($app) {
      return new User($app->make('App\Database\MongoDatabase'));
    });
 
    // add custom guard provider
    Auth::provider('mongo', function ($app, array $config) {
      return new MongoUserProvider($app->make('App\Models\Auth\User'));
    });
 
    // add custom guard
    Auth::extend('json', function ($app, $name, array $config) {
      return new JsonGuard(Auth::createUserProvider($config['provider']), $app->make('request'));
    });
  }
 
  public function register()
  {
    $this->app->bind(
      'App\Services\Contracts\NosqlServiceInterface',
      'App\Database\MongoDatabase'
    );
  }
}

让我们来看看boot 方法,其中包含大部分的提供者绑定。

首先,我们将为App\Database\MongoDatabaseApp\Models\Auth\User 元素创建绑定。

$this->app->bind('App\Database\MongoDatabase', function ($app) {
  return new MongoDatabase(config('mongo.defaults.host'), config('mongo.defaults.port'), config('mongo.defaults.database'));
});
 
$this->app->bind('App\Models\Auth\User', function ($app) {
  return new User($app->make('App\Database\MongoDatabase'));
});

我们已经谈论了一段时间的提供者和守护者,现在是时候把我们的自定义守护者插入到Laravel认证系统中。

我们已经使用了Auth Facade的提供者方法来添加我们的自定义认证提供者在键mongo 。回顾一下,这个键反映了之前在auth.php 文件中添加的设置。

Auth::provider('mongo', function ($app, array $config) {
  return new MongoUserProvider($app->make('App\Models\Auth\User'));
});

以类似的方式,我们将使用Auth Facade的extend方法注入我们的自定义Guard实现。

Auth::extend('json', function ($app, $name, array $config) {
  return new JsonGuard(Auth::createUserProvider($config['provider']), $app->make('request'));
});

接下来是register 方法,我们用它将App\Services\Contracts\NosqlServiceInterface 接口与App\Database\MongoDatabase 实现绑定。

$this->app->bind(
  'App\Services\Contracts\NosqlServiceInterface',
  'App\Database\MongoDatabase'
);

所以,每当需要解决App\Services\Contracts\NosqlServiceInterface 的依赖性时,Laravel就会用App\Database\MongoDatabase 适配器的实现来回应。

使用这种方法的好处是,人们可以很容易地用一个自定义的实现来交换给定的实现。例如, 假设有人想在将来用CouchDB 适配器来替换App\Database\MongoDatabase 的实现.在这种情况下,他们只需要在register 方法中添加相应的绑定。

所以,这就是你所支配的服务提供者。此时此刻,我们已经拥有了测试我们的自定义守护实现所需的一切,所以下一节也就是最后一节就是关于这个的。

它起作用了吗?

你已经做了所有艰苦的工作,建立了你的第一个自定义认证防护,现在是时候收获好处了,因为我们将继续前进并试一试。

让我们快速实现app/Http/Controllers/MongoController.php控制器,如下所示。

<?php
// app/Http/Controllers/MongoController.php
namespace App\Http\Controllers;
 
use App\Http\Controllers\Controller;
use Illuminate\Contracts\Auth\Guard;
 
class MongoController extends Controller
{
  public function login(Guard $auth_guard)
  {
    if ($auth_guard->validate()) {
      // get the current authenticated user
      $user = $auth_guard->user();
     
      echo 'Success!';
    } else {
      echo 'Not authorized to access this page!';
    }
  }
}

仔细看一下login 方法的依赖性,它需要实现Illuminate\Contracts\Auth\Guard guard。由于我们在auth.php 文件中设置了自定义的卫兵作为默认的卫兵,所以实际上将注入的是App\Services\Auth\JsonGuard!

接下来,我们已经调用了App\Services\Auth\JsonGuard 类的validate 方法,这又启动了一系列的方法调用。

  • 它调用了App\Extensions\MongoUserProvider 类的retrieveByCredentials 方法。
  • retrieveByCredentials 方法调用App\Models\Auth\User 类的fetchUserByCredentials 方法。
  • fetchUserByCredentials 方法调用App\Database\MongoDatabasefind 方法,以检索用户证书。
  • 最后,App\Database\MongoDatabasefind 方法返回响应!

如果一切按预期进行,我们应该通过调用我们的卫士的user 方法来获得认证的用户。

要访问控制器,你应该在routes/web.php文件中添加一个相关的路由。

Route::get('/custom/mongo/login', 'MongoController@login');

尝试访问URLhttps://your-laravel-site/custom/mongo/login,不传递任何参数,你应该看到not authorized

另一方面,尝试像http://your-laravel-site/custom/mongo/login?jsondata={"username": "admin", "password": "admin"},如果用户存在于你的数据库中,应该会返回success 信息。

请注意,这只是为了举例说明自定义防护措施的工作原理。你应该为登录这样的功能实现一个万无一失的解决方案。事实上,我只是提供了一个关于认证流程的洞察力;你应该负责为你的应用程序建立一个强大而安全的解决方案。

我们今天的旅程到此结束,希望我还会带着更多有用的东西回来。

总结

Laravel框架在核心部分提供了一个坚实的认证系统,如果你想实现一个自定义的认证系统,可以进行扩展。这就是今天文章的主题: 实现一个自定义的卫士,并将其插入到Laravel的认证工作流程中.

在这个过程中, 我们继续开发了一个系统,根据请求中的JSON有效载荷对用户进行认证,并将其与MongoDB数据库进行匹配。为了实现这一目标,我们最终创建了一个自定义的卫士和一个自定义的提供者实现。

我希望这个练习能让你深入了解Laravel的认证流程, 你现在应该对它的内部运作更有信心.

对于那些刚刚开始使用Laravel或者希望通过扩展来扩展你的知识, 网站或应用程序的人来说, 我们在 Envato市场上有各种东西可以研究.