csun-metalab / laravel-directory-authentication
为Laravel 5.0及以上版本提供的Composer包,允许基于目录进行认证
Requires
- php: >=5.5.9
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-stability
为 dev
,不能作为具有 minimum-stability
为 stable
的项目的依赖项。
环境
现在,将以下行添加到您的 .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_DN
和LDAP_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_USERNAME
、LDAP_SEARCH_MAIL
和LDAP_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_DN
和LDAP_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
值修改子树下对象的修改方法。允许的值是 self
和 admin
。
如果值是 self
,则绑定用户将能够修改目录中自己的属性。
如果值是 admin
,则绑定的用户将是 LDAP_MODIFY_DN
和 LDAP_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();
}
}
?>
在这个子类中发生了一些额外的事情
- 已经将表和主键更改为使用自定义值
- 已经重写了两个身份验证后方法,以执行状态检查,以确保只有活跃用户才能使用应用程序
认证
使用 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_DN
和LDAP_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}";
}