csun-metalab/laravel-directory-authentication

为Laravel 5.0及以上版本提供的Composer包,允许基于目录进行认证

1.6.0 2018-04-30 21:56 UTC

This package is auto-updated.

Last update: 2024-09-26 04:04:42 UTC


README

为Laravel 5.0及以上版本提供的Composer包,允许基于目录进行认证。

此包增加了执行本地数据库和基于LDAP认证的能力。

一旦通过目录服务(如LDAP)验证了用户,就会在本地数据库中执行查找,以解析可以通过 Auth::user() 访问的用户模型实例。

如果您只想使用本地数据库认证(但仍想利用包的功能),请参阅Laravel Directory Authentication (Database Only) Readme。

注意:授权是在Laravel 5.1中添加的,因此 MetaUser 类发生了破坏性更改。如果您需要为Laravel 5.0使用此包,请使用此包的任何版本,直到 1.6.0

目录

安装

注意:根据您使用的Laravel版本,以下有不同配置项。

Composer、环境和服务提供者

Composer

要从Composer安装,请按以下顺序使用以下命令

composer require tiesa/ldap:dev-master
composer require csun-metalab/laravel-directory-authentication

有两个命令,因为 tiesa/ldap 不能在这个包的 require 子句中,因为这个依赖项只公开了 dev-master 发布,没有版本化发布。因此,它评估为 minimum-stabilitydev,不能作为具有 minimum-stabilitystable 的项目的依赖项。

环境

现在,将以下行添加到您的 .env 文件中

LDAP_HOST=
LDAP_BASE_DN=

您还可以选择将以下可选行添加到您的 .env 文件中,以进一步自定义功能

LDAP_VERSION=3
LDAP_ALLOW_NO_PASS=false
LDAP_DN=
LDAP_PASSWORD=

LDAP_SEARCH_USER_ID=employeeNumber
LDAP_SEARCH_USERNAME=uid
LDAP_SEARCH_MAIL=mail
LDAP_SEARCH_MAIL_ARRAY=mailLocalAddress
LDAP_SEARCH_USER_QUERY=

LDAP_DB_USER_ID_PREFIX=
LDAP_DB_RETURN_FAKE_USER=false

LDAP_OVERLAY_DN=

LDAP_ADD_BASE_DN=
LDAP_ADD_DN=
LDAP_ADD_PW=

LDAP_MODIFY_METHOD=self
LDAP_MODIFY_BASE_DN=
LDAP_MODIFY_DN=
LDAP_MODIFY_PW=

服务提供者

接下来,将服务提供者添加到Laravel中的 config/app.php 文件的 providers 数组中,如下所示

'providers' => [
   //...

   CSUNMetaLab\Authentication\Providers\AuthServiceProvider::class,

   // You can also use this based on Laravel convention:
   // 'CSUNMetaLab\Authentication\Providers\AuthServiceProvider',

   //...
],

配置(Laravel 5.2及以上)

接下来,将 driver 更改为 ldap,并在 config/auth.php 中的 users 数组中添加用于数据库查找的用户模型的完整类名,如下所示

'providers' => [
    'users' => [
        // LDAP authentication method
        'driver' => 'ldap',

        // this can be any subclass of the CSUNMetaLab\Authentication\MetaUser class
        // or the MetaUser class itself since it works out of the box
        'model' => CSUNMetaLab\Authentication\MetaUser::class,
    ],
],

配置(Laravel 5.0和5.1)

接下来,将 driver 更改为 ldap,并将 model 属性更改为用于数据库查找的用户模型的完整类名,在 config/auth.php 中进行以下更改

// LDAP authentication method
'driver' => 'ldap',

// this can be any subclass of the CSUNMetaLab\Authentication\MetaUser class
// or the MetaUser class itself since it works out of the box 
'model' => 'CSUNMetaLab\Authentication\MetaUser',

发布配置

最后,运行以下Artisan命令来发布配置

php artisan vendor:publish

必需的环境变量

您在 .env 文件中添加了两个环境变量,用于控制与LDAP服务器的连接以及其搜索子树。

LDAP_HOST

这是LDAP服务器的主机名或IP地址。

LDAP_BASE_DN

这是所有要搜索的人所在的基DN。这可能如下所示

ou=People,ou=Auth,o=Organization

此基DN下可能存在以下记录的人

uid=person,ou=People,ou=Auth,o=Organization

注意:环境变量LDAP_BASE_DNLDAP_OVERLAY_DN不兼容。请使用其中一个。理想情况下,如果您的LDAP服务器使用overlay来为多个子树创建逻辑根,则应使用LDAP_OVERLAY_DN。否则,使用LDAP_BASE_DN作为搜索基。

可选的环境变量

还有一些可选的环境变量可以添加,以进一步定制包的功能。

LDAP_VERSION

执行操作时使用的LDAP版本。默认为3

LDAP_ALLOW_NO_PASS

设置为True以关闭用户密码验证(因此使用admin DN和密码进行人员的搜索)。当为false时,绑定和搜索使用传递给Auth::attempt()的用户名和密码。

默认为false

LDAP_DN

绑定和搜索人员时使用的admin DN。仅在关闭用户密码验证时使用(因此仍然可以进行搜索)。

LDAP_PASSWORD

绑定和搜索人员时使用的admin密码。仅在关闭用户密码验证时使用(因此仍然可以进行搜索)。

LDAP_SEARCH_USER_ID

在LDAP中根据用户ID查找人员时使用的字段;这通常是一个数字字段。

默认为employeeNumber。这是在相关数据模型和数据库表/视图中检查用户时使用的字段值。

如果想要使用相同的值执行LDAP和数据库查找,这也可以与LDAP_SEARCH_USERNAME的值相同。

LDAP_SEARCH_USERNAME

在LDAP中根据用户名查找人员时使用的字段;这通常是对应的POSIX ID。

默认为uid。这是作为传递给Auth::attempt()调用的用户名使用的值,以执行搜索操作。

如果启用密码验证,这也是与基本DN结合时用于绑定操作的username。

LDAP_SEARCH_MAIL

在LDAP中根据电子邮件地址查找人员时使用的字段。

默认为mail

LDAP_SEARCH_MAIL_ARRAY

在LDAP中根据所有有效电子邮件地址和别名查找人员时使用的字段;这通常是一个数组属性。

默认为mailLocalAddress

LDAP_SEARCH_USER_QUERY

可选的搜索查询,将在包在用户目录搜索期间执行的默认查询中替换。如果未指定,则将使用查询为(|(uid=%s)(mail=%s)(mailLocalAddress=%s))的等效查询,具体取决于LDAP_SEARCH_USERNAMELDAP_SEARCH_MAILLDAP_SEARCH_MAIL_ARRAY的值。

如果指定,则需要是vsprintf()兼容的字符串,并使用%s作为搜索值的占位符。

LDAP_DB_USER_ID_PREFIX

在关联的数据库表/视图中的员工ID主键值之前可选的限定符。

默认为空白(无前缀)。

例如,LDAP可能将员工ID存储为数字(XXXXXXXXX),但您的数据库将其存储为带前缀的文本值(例如:members:XXXXXXXXX)。您将设置此值为members:,您的数据库查找将正常工作。

LDAP_DB_RETURN_FAKE_USER

确定是否在目录中找到用户但不在数据库中时返回实际用户实例。

如果为true,则将返回一个用户实例,其中包含可以用于在数据库中创建用户的LDAP属性。

如果为false,则如果用户不在数据库中,则认证尝试将直接失败,因为Auth::attempt()将返回false

默认为false

LDAP_OVERLAY_DN

Overlay DN,为目录中的搜索、添加和修改子树提供一致的逻辑根。

默认为空白字符串。

注意:环境变量LDAP_BASE_DNLDAP_OVERLAY_DN不兼容。请使用其中一个。理想情况下,如果您的LDAP服务器使用overlay来为多个子树创建逻辑根,则应使用LDAP_OVERLAY_DN。否则,使用LDAP_BASE_DN作为搜索基。

LDAP_ADD_BASE_DN

用于向子树添加对象时使用的基DN。

如果此值留空,将使用 LDAP_BASE_DN 的值。

默认为空白字符串。

LDAP_ADD_DN

LDAP_ADD_BASE_DN 子树下添加对象时使用的管理员DN。

如果此值留空,将使用 LDAP_DN 的值。

默认为空白字符串。

LDAP_ADD_PW

LDAP_ADD_BASE_DN 子树下添加对象时使用的密码。

如果此值留空,将使用 LDAP_PASSWORD 的值。

默认为空白字符串。

LDAP_MODIFY_METHOD

用于从 LDAP_MODIFY_BASE_DN 值修改子树下对象的修改方法。允许的值是 selfadmin

如果值是 self,则绑定用户将能够修改目录中自己的属性。

如果值是 admin,则绑定的用户将是 LDAP_MODIFY_DNLDAP_MODIFY_PW 的组合。

默认值是 self

LDAP_MODIFY_BASE_DN

用于修改子树下对象的基DN。如果此值留空,将使用 LDAP_ADD_BASE_DN 的值。

默认为空白字符串。

LDAP_MODIFY_DN

LDAP_MODIFY_BASE_DN 子树下修改对象时使用的管理员DN。

如果此值留空,将使用 LDAP_ADD_DN 的值。

默认为空白字符串。

LDAP_MODIFY_PW

LDAP_MODIFY_BASE_DN 子树下修改对象时使用的密码。

如果此值留空,将使用 LDAP_ADD_PW 的值。

默认为空白字符串。

HandlerLDAP

核心LDAP功能由 CSUNMetaLab\Authentication\Handlers\HandlerLDAP 类提供。该类包含在认证过程中使用的连接、绑定和搜索功能。

此类还可以单独使用,以提供通用的LDAP搜索功能,并提供一致的接口来执行查找操作。

您可以通过对其工厂类 CSUNMetaLab\Authentication\Factories\HandlerLDAPFactory 的调用来返回 HandlerLDAP 的实例,如下所示

$ldap = HandlerLDAPFactory::fromDefaults();

现在您将有一个加载了来自 config/ldap.php 的默认配置的类实例。如果您想为了认证以外的目的搜索某人,您可以这样做,例如

// this assumes the $ldap variable already holds an instance of HandlerLDAP

// you can optionally set a new base DN to use during the connection and bind
// $ldap->setBaseDN("ou=Administrators,ou=Auth,o=Organization");

// you can now connect and subsequently bind to the directory in one of two ways:
$ldap->connect(); // will use the values of LDAP_DN and LDAP_PASSWORD
//$ldap->connect($username, $password); // will use explicit credentials

// you can also set a new base DN to use prior to a search too
// $ldap->setBaseDN("ou=Managers,ou=Auth,o=Organization");

// now that you have a connection, you can perform any valid search query within
// the base DN specified in the configuration
$result = $ldap->searchByQuery("uid=manager");

// you can then retrieve the relevant attributes that you want
$name = $ldap->getAttributeFromResults($result, "displayName");
$email = $ldap->getAttributeFromResults($result, "mail");
if(!empty($name) && !empty($email)) {
  return "Manager {$name} has the email of {$email}";
}

MetaUser

此包包含一个配置好的 CSUNMetaLab\Authentication\MetaUser 类,它可以与目录认证方法正确配合工作。它还支持开箱即用的 伪装成其他用户,无需额外配置。

它提供了查找用户的方法的基本实现,无论是通过数据库中的标识符还是通过标识符和“记住我”令牌的组合。只有 findForAuth() 方法会在认证成功后自动调用;其他 findForAuthToken() 方法提供方便,如果您正在实现应用程序中的“记住我”功能。

此类期望有一个名为 users 的本地数据库表,其主键为 user_id。只要您满足这两个要求,就可以直接使用此类。

创建自定义用户类

建议您至少创建一个从 CSUNMetaLab\Authentication\MetaUser 扩展的类,因为这将使您对认证和数据库功能有更大的控制权。

简单子类

一个简单的子类如下

<?php

namespace App\Models;

use CSUNMetaLab\Authentication\MetaUser;

class User extends MetaUser
{
  protected $fillable = ['user_id', 'first_name', 'last_name', 'display_name', 'email'];

  // this must be set for models that do not use an auto-incrementing PK
  public $incrementing = false;
}

?>

此类仍然使用 users 表,但还将主键定义为非自增。此外,它定义了一个 fillable 数组,并允许通过批量分配创建 User 实例。

尽管如此,它仍然依赖于存在于 MetaUser 类中的 findForAuth()findForAuthToken() 的实现。

综合子类

重要:如果您的模型的主键不是 user_id,则需要实现一个综合子类,特别是 findForAuth()findForAuthToken() 方法。当执行身份验证检查时(例如,当使用 auth 中间件时),$identifier 参数将是模型实例主键的值。否则,您的初始登录将正常工作,但会话值将不正确,因此下一次请求中的后续登录检查将不会通过。

以下是一个更全面的子类示例

<?php

namespace App\Models;

use CSUNMetaLab\Authentication\MetaUser;

class User extends MetaUser
{
  protected $fillable = ['user_id', 'first_name', 'last_name', 'display_name', 'email'];
  protected $primaryKey = "uid";
  protected $table = "people";

  // this must be set for models that do not use an auto-incrementing PK
  public $incrementing = false;

  // implements MetaAuthenticatableContract#findForAuth
  public static function findForAuth($identifier) {
    return self::where('uid', '=', $identifier)
      ->where('status', 'Active')
      ->first();
  }

  // implements MetaAuthenticatableContract#findForAuthToken
  public static function findForAuthToken($identifier, $token) {
    return self::where('uid', '=', $identifier)
      ->where('remember_token', '=', $token)
      ->where('status', 'Active')
      ->first();
  }
}

?>

在这个子类中发生了一些额外的事情

  1. 已经将表和主键更改为使用自定义值
  2. 已经重写了两个身份验证后方法,以执行状态检查,以确保只有活跃用户才能使用应用程序

认证

使用 MetaUser 或您的自定义子类进行身份验证与 Laravel 中的常规数据库身份验证一样简单。

此包区分用户是否仅在目录中存在或在目录和数据库中都存在。

仅身份验证

不返回假用户实例

如果将 LDAP_DB_RETURN_FAKE_USER 设置为 false,您可以通过以下方式调用 Auth::attempt()

$creds = ['username' => 'admin', 'password' => '123'];

if(Auth::attempt($creds)) {
  // valid user in the directory and the database, so proceed into the application!
  redirect('home');
}
else
{
  // not a valid user (either in the directory or the database) so return back
  // to the login page with an error
  redirect('login')->withErrors([
    'Invalid username or password'
  ]);
}

返回假用户实例

如果将 LDAP_DB_RETURN_FAKE_USER 设置为 true,您可以通过以下方式调用 Auth::attempt()

$creds = ['username' => 'admin', 'password' => '123'];

if(Auth::attempt($creds)) {
  // valid user in the directory, so let's check to see whether the user is
  // valid within the database too
  if(Auth::user()->isValid) {
    // user also exists within the database, so proceed into the application!
    redirect('home');
  }
  else
  {
    // user does not exist within the database, so go ahead and return back
    // to the login screen with an error
    Auth::logout();
    redirect('login')->withErrors([
      'Local user record could not be found'
    ]);
  }
}
else
{
  // not a valid user in the directory, so go ahead and return back to the
  // login screen with an error
  redirect('login')->withErrors([
    'Invalid username or password'
  ]);
}

具有预配的身份验证

注意:仅预配的身份验证功能仅在将 LDAP_DB_RETURN_FAKE_USER 设置为 true 时才有效。

由于区分了有效的目录用户和有效的数据库用户,您可以选择在本地数据库中预配用户,如果该用户仅存在于目录中但尚未存在于数据库中。

Auth::user() 表示的用户实例也从 LDAP 返回一个属性数组。此数组可以访问为 searchAttributes。它包含以下键/值对

  • uid(与搜索属性 LDAP_SEARCH_USERNAME 的值匹配)
  • user_id(与搜索属性 LDAP_SEARCH_USER_ID 的值匹配)
  • first_name(与 givenName 匹配)
  • last_name(与 sn 匹配)
  • display_name(与 display_name 匹配)
  • email(与 LDAP_SEARCH_MAIL 匹配)

现在,您可以将您的身份验证过程从上面的修改为以下内容

$creds = ['username' => 'admin', 'password' => '123'];

if(Auth::attempt($creds)) {
  // valid user in the directory, so let's check to see whether the user is
  // valid within the database too
  if(Auth::user()->isValid) {
    // user also exists within the database, so proceed into the application!
    redirect('home');
  }
  else
  {
    // user does not exist within the database, so go ahead and provision the
    // record and perform an automatic login; we are assuming the user instance
    // uses a model called User here
    $attrs = Auth::user()->searchAttributes;
    $user = User::create([
      'user_id' => $attrs['user_id'],
      'first_name' => $attrs['first_name'],
      'last_name' => $attrs['last_name'],
      'display_name' => $attrs['display_name'],
      'email' => $attrs['email'];
    ]);

    // perform the automatic login and the redirect
    Auth::login($user);
    redirect('home');
  }
}
else
{
  // not a valid user in the directory, so go ahead and return back to the
  // login screen with an error
  redirect('login')->withErrors([
    'Invalid username or password'
  ]);
}

伪装

此包还支持登录用户能够直接成为其他用户的功能。这在管理员级用户可能需要直接进入其他用户的帐户以进行故障排除和解决问题的场景中特别有用。

成为另一个用户

为了成为另一个用户,只需找到其他用户,然后切换登录用户即可。之前的用户将保持会话状态,以便切换回原始用户可以无缝进行。

// findOrFail is used here with a custom User instance but you can use any
// subclass of MetaUser that you would like or MetaUser itself
$switchUser = User::findOrFail('employee');

if(Auth::user()->masqueradeAsUser($switchUser)) {
  // successfully masquerading!
}
else
{
  // masquerade attempt failed
}

我在伪装吗?

可能需要确定由 Auth::user() 报告的用户是否实际上是伪装的用户。

if(Auth::user()->isMasquerading()) {
  // I am acting as someone else
}
else
{
  // it's the original logged-in user
}

您还可以通过以下方式检索伪装用户(原始登录用户)的实例

if(Auth::user()->isMasquerading()) {
  $originalUser = Auth::user()->getMasqueradingUser();
  return "This account is really " . $originalUser->display_name;
}
else
{
  // not masquerading
  return "Not masquerading";
}

停止伪装

最后,停止伪装并返回到原始用户帐户非常简单。

if(Auth::user()->stopMasquerading()) {
  // I have returned to my original user account
}
else
{
  // this account was not masquerading
}

Auth::user() 的调用将再次报告原始登录用户。

添加新的LDAP记录

在执行添加操作时,此包将首先尝试使用以下三个 .env 中的配置信息

  • LDAP_ADD_BASE_DN:新条目将被添加的子树。如果留空,则将使用 LDAP_BASE_DN 的值。
  • LDAP_ADD_DN:添加条目时使用的管理员DN。如果为空,则使用LDAP_DN的值。
  • LDAP_ADD_PW:添加条目时使用的管理员密码。如果为空,则使用LDAP_PASSWORD的值。

您可以使用HandlerLDAP类向LDAP子树添加新记录。

use CSUNMetaLab\Authentication\Factories\HandlerLDAPFactory;
use CSUNMetaLab\Authentication\Factories\LDAPPasswordFactory;

public function addNewObject() {
  $name = "New User";
  $email = "newuser@example.com";
  $pw = "1234";

  $pwhash = LDAPPasswordFactory::SSHA($pw); // SSHA hash
  $uid = 'ex_' . bin2hex(random_bytes(4)); // random UID

  // retrieve a new HandlerLDAP instance and connect to the configured
  // host
  $ldap = HandlerLDAPFactory::fromDefaults();
  $ldap->connect();

  // set up the attributes to be added to the new record
  $nameArr = explode(" ", $name);
  $attrs = [
    'objectClass' => 'inetOrgPerson',
    'uid' => $uid,
    'mail' => $email,
    'displayName' => $name,
    'cn' => $name,
    'sn' => (!empty($nameArr[1]) ? $nameArr[1] : "Example"),
    'givenName' => $nameArr[0],
    'userPassword' => $pwhash,
  ];

  // add the object to the add subtree
  $success = $ldap->addObject($uid, $attrs);
  if($success) {
    return "Successfully registered account {$name}! UID: {$uid}";
  }

  return "Could not register {$name}. UID: {$uid}. Please try again";
}

修改现有的LDAP记录

在执行修改操作时,此包将首先尝试使用以下四个.env值配置的信息。

  • LDAP_MODIFY_METHOD:如果为self,则用户可以绑定自己并修改自己的属性。如果为admin,则在执行修改绑定时将使用LDAP_MODIFY_DNLDAP_MODIFY_PW的值。
  • LDAP_MODIFY_BASE_DN:修改条目的子树。如果为空,则使用LDAP_ADD_BASE_DN的值。
  • LDAP_MODIFY_DN:修改条目时使用的管理员DN。如果为空,则使用LDAP_ADD_DN的值。
  • LDAP_MODIFY_PW:修改条目时使用的管理员密码。如果为空,则使用LDAP_ADD_PW的值。

您可以使用HandlerLDAP类在LDAP子树中修改现有记录。

use CSUNMetaLab\Authentication\Factories\HandlerLDAPFactory;
use CSUNMetaLab\Authentication\Factories\LDAPPasswordFactory;

public function modifyObject() {
  // this lets a logged-in user modify their own attributes due to the bind
  // as "self" as opposed to the LDAP_MODIFY_DN and LDAP_MODIFY_PW values
  // from the .env file
  $cur_pw = "1234";

  $email = "newuser@example.com";
  $newName = "Different Name";
  $newNameArr = explode(" ", $newName);

  // retrieve a new HandlerLDAP instance and connect to the configured
  // host
  $ldap = HandlerLDAPFactory::fromDefaults();
  $ldap->connect();

  // get the matching object for the logged-in user and resolve its DN
  $obj = $ldap->searchByAuth($email);
  $dn = $ldap->getAttributeFromResults($obj, 'dn');

  // modification method of "self": make sure the user has the correct
  // password by performing another bind; this will not be executed if
  // the admin modify DN and password are being used instead
  if(config('ldap.modify_method') == "self") {
    $ldap->connectByDN($dn, $cur_pw);
  }

  // set a new name for the user
  $success = $ldap->modifyObject($dn, [
    'displayName' => $newName,
    'cn' => $newName,
    'givenName' => $newNameArr[0],
    'sn' => $newNameArr[1],
  ]);

  if($success) {
    return "Successfully changed the name for {$email}! DN: {$dn}";
  }

  return "Could not change the name for {$email}. DN: {$dn}";
}

修改LDAP用户密码

虽然您可以使用具有userPassword属性的modifyObject()并遵循修改现有LDAP记录中的类似步骤,但HandlerLDAP类还提供了一个方便的方法来更改用户密码。

密码将使用基于openssl_random_pseudo_bytes()输出的四字节字符串的加密安全盐生成SSHA散列。

public function modifyUserPassword() {
  // this lets a logged-in user modify their own attributes due to the bind
  // as "self" as opposed to the LDAP_MODIFY_DN and LDAP_MODIFY_PW values
  // from the .env file
  $cur_pw = "1234";

  $email = "newuser@example.com";
  $new_pw = "2345";

  // retrieve a new HandlerLDAP instance and connect to the configured
  // host
  $ldap = HandlerLDAPFactory::fromDefaults();
  $ldap->connect();

  // get the matching object for the logged-in user and resolve its DN
  $obj = $ldap->searchByAuth($email);
  $dn = $ldap->getAttributeFromResults($obj, 'dn');

  // modification method of "self": make sure the user has the correct
  // password by performing another bind; this will not be executed if
  // the admin modify DN and password are being used instead
  if(config('ldap.modify_method') == "self") {
    $ldap->connectByDN($dn, $cur_pw);
  }

  // set a new password for the user
  $success = $ldap->modifyObjectPassword($dn, $new_pw);

  if($success) {
    return "Successfully changed the password for {$email}! DN: {$dn}";
  }

  return "Could not change the password for {$email}. DN: {$dn}";
}