我的这个函数在我的活动目录下进行身份验证,它的工作正常,除了包含像这样的特殊字符的一些密码:
Xefeéà和放大器;”
总是返回’无效凭据’
我在互联网上看过几个帖子,但似乎都没有正确地逃过这些帖子.
我正在使用PHP版本5.3.8-ZS(Zend服务器)
这是我的类构造函数中的设置:
ldap_set_option($this->_link, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($this->_link, LDAP_OPT_REFERRALS, 0);
和登录功能:
public function login($userlogin,$userpassword){
$return=array();
$userlogin.="@".$this->_config["domaine"];
$ret=@ldap_bind($this->_link , $userlogin ,utf8_decode($userpassword));
if(!$ret){
$return[]=array("status"=>"error","message"=>"Erreur d'authentification ! <br/> Veuillez vérifier votre nom et mot de passe SVP","ldap_error"=>ldap_error($this->_link));
}
if($ret)
{
$return[]=array("status"=>"success","message"=>"Authentification réussie");
}
return json_encode($return);
}
任何帮助将不胜感激.
解决方法:
我们发现自己处于类似的状态,我们的企业环境LDAP认证设置对于我们的一个内部托管的Web应用程序似乎工作得很好,直到用户的密码中有特殊字符 – 字符如? &安培; *’ – 想要使用该应用程序,然后很明显,某些东西实际上没有按预期工作.
我们发现当受影响的用户密码中存在特殊字符时,我们的身份验证请求在调用ldap_bind()时失败,并且通常会看到诸如无效凭据或NDS错误之类的错误:身份验证失败(-669)(来自LDAP的扩展)错误日志记录)输出到日志.事实上,这是NDS错误:失败的身份验证(-669)错误导致发现失败的绑定可能是由于密码中存在特殊字符 – 通过此Novell支持文章https://www.novell.com/support/kb/doc.php?id=3335671,其中最引人注目的部分摘录如下:
Admin password had characters that LDAP does not understand, ie:”_”
and “$”
一系列修复已经过广泛测试,包括通过html_entity_decode(),utf8_encode()等“清理”密码,并确保Web应用程序和各种服务器之间没有密码错误编码,但无论如何为调整或保护输入文本以确保其原始UTF-8编码保持不变而做了什么,当密码中存在一个或多个特殊字符时,ldap_bind()总是失败(我们没有测试用户名中包含特殊字符的情况) ,但是当用户名包含特殊字符而不是密码时,其他人报告了类似的问题.
我们还使用命令行上运行的最小PHP脚本直接测试了ldap_bind(),其中包含用于密码中包含特殊字符(包括问号,星号和感叹号)的测试LDAP帐户的硬编码用户名和密码,试图消除尽可能多的潜在失败原因,但绑定操作仍然失败.因此很明显,密码不是Web应用程序和服务器之间错误编码的问题,所有这些都是针对端到端的UTF-8兼容而设计和构建的.相反,似乎ldap_bind()存在潜在问题及其对特殊字符的处理.相同的测试脚本成功绑定了另一个密码中没有任何特殊字符的帐户,进一步证实了我们的怀疑.
早期版本的LDAP身份验证的工作原理如下,因为大多数PHP LDAP身份验证教程都建议:
$success = false; // could we authenticate the user?
if(!defined("LDAP_OPT_DIAGNOSTIC_MESSAGE")) {
define("LDAP_OPT_DIAGNOSTIC_MESSAGE", 0x0032); // needed for more detailed logging
}
if(($ldap = ldap_connect($ldap_server)) !== false && is_resource($ldap)) {
ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0);
if(@ldap_bind($ldap, sprintf("cn=%s,ou=Workers,o=Internal", $username), $password)) {
$success = true; // login succeeded...
} else {
error_log(sprintf("* Unable to authenticate user (%s) against LDAP!", $username));
error_log(sprintf(" * LDAP Error: %s", ldap_error($ldap)));
if(ldap_get_option($ldap, LDAP_OPT_DIAGNOSTIC_MESSAGE, $extended_error)) {
error_log(sprintf(" * LDAP Extended Error: %s\n", $extended_error));
unset($extended_error);
}
}
}
@ldap_close($ldap); unset($ldap);
很明显,我们使用ldap_connect()和ldap_bind()的简单解决方案不允许具有复杂密码的用户进行身份验证,我们必须找到替代方案.
我们选择了一种解决方案,该解决方案基于首先使用系统帐户绑定到我们的LDAP服务器(使用必要的权限创建以搜索用户,但故意配置为有限的只读帐户).然后,我们使用ldap_search()搜索具有提供的用户名的用户帐户,然后使用ldap_compare()将用户提供的密码与存储在LDAP中的密码进行比较.该解决方案已被证明是可靠和有效的,我们现在能够在密码中使用特殊字符对用户进行身份验证,就像没有密码的用户一样!
新的LDAP过程如下:
if(!defined("LDAP_OPT_DIAGNOSTIC_MESSAGE")) {
define("LDAP_OPT_DIAGNOSTIC_MESSAGE", 0x0032); // needed for more detailed logging
}
$success = false; // could we authenticate the user?
if(($ldap = @ldap_connect($ldap_server)) !== false && is_resource($ldap)) {
ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0);
// instead of trying to bind the user, we bind to the server...
if(@ldap_bind($ldap, $ldap_username, $ldap_password)) {
// then we search for the given user...
if(($search = ldap_search($ldap, "ou=Workers,o=Internal", sprintf("(&(uid=%s))", $username))) !== false && is_resource($search)) {
// they should be the first and only user found for the search...
if((ldap_count_entries($ldap, $search) == 1) && ($entry = ldap_first_entry($ldap, $search)) !== false && is_resource($entry)) {
// we ensure this is the case by obtaining the user identifier (UID) from the search which must match the provided $username...
if(($uid = ldap_get_values($ldap, $entry, "uid")) !== false && is_array($uid)) {
// ensure that just one entry was returned by ldap_get_values() and ensure the obtained value matches the provided $username (excluding any case-differences)
if((isset($uid["count"]) && $uid["count"] == 1) && (isset($uid[0]) && is_string($uid[0]) && (strcmp(mb_strtolower($uid[0], "UTF-8"), mb_strtolower($username, "UTF-8")) === 0))) {
// once we have compared the provied $username with the discovered username, we get the DN for the user, which we need to provide to ldap_compare()...
if(($dn = ldap_get_dn($ldap, $entry)) !== false && is_string($dn)) {
// we then use ldap_compare() to compare the user's password with the provided $password
// ldap_compare() will respond with a boolean true/false depending on if the comparison succeeded or not, and -1 on error...
if(($comparison = ldap_compare($ldap, $dn, "userPassword", $password)) !== -1 && is_bool($comparison)) {
if($comparison === true) {
$success = true; // user successfully authenticated, if we don't get this far, it failed...
}
}
}
}
}
}
}
}
if(!$success) { // if not successful, either an error occurred or the credentials were actually incorrect
error_log(sprintf("* Unable to authenticate user (%s) against LDAP!", $username));
error_log(sprintf(" * LDAP Error: %s", ldap_error($ldap)));
if(ldap_get_option($ldap, LDAP_OPT_DIAGNOSTIC_MESSAGE, $extended_error)) {
error_log(sprintf(" * LDAP Extended Error: %s\n", $extended_error));
unset($extended_error);
}
}
}
@ldap_close($ldap);
unset($ldap);
原始问题之一是这种新方法,所有额外步骤都需要更长时间才能执行,但事实证明(至少在我们的环境中)运行简单的ldap_bind()与更复杂的解决方案之间的时序差异使用ldap_search()和ldap_compare()实际上是微不足道的,平均可能长0.1秒.
需要注意的是,只有在用户输入数据(即用户名和密码输入)经过验证和测试后,才能运行早期版本和后一版本的LDAP认证代码,以确保没有提供空的或无效的用户名或密码字符串,以及没有无效字符(如空字节).此外,理想情况下,应通过来自应用程序的安全HTTPS请求发送用户凭证,并且可以使用安全LDAPS协议在身份验证过程中进一步保护用户凭据.我们发现直接使用LDAPS,通过ldaps://连接到服务器而不是通过ldap:// …连接,然后切换到TLS连接已被证明更可靠.如果您的设置有所不同,则需要相应调整上面的代码以包含对ldap_start_tls()的支持.
最后,值得注意的是,LDAP协议要求使用UTF-8根据RFC(RFC 4511, p16, first paragraph)对密码进行编码 – 因此,如果在请求期间使用的任何系统未将用户的凭据处理为UTF-8从初始入口一直到身份验证,这也可能是一个值得研究的领域.
希望这个解决方案对于那些努力用许多其他报告的解决方案解决这个问题的人来说是有用的,但仍然发现用户无法进行身份验证的情况.可能需要针对您自己的LDAP环境调整代码,尤其是ldap_search()中使用的DN,因为这取决于您的LDAP目录的配置方式以及您为应用程序支持的用户组.很高兴,它在我们的环境中运作良好,并希望该解决方案在其他地方证明是有用的.