# LDAP 认证

## 简介

`Zend_Auth_Adapter_Ldap` 用 LDAP 服务支持 web 程序认证。它的功能包括用户名和域名规范化、多域认证和实效切（failover）换能力。经测试，它能与 Microsoft Active Directory 和 OpenLDAP 一起工作，它也应该能和其它 LDAP 服务提供者一起工作。

本文档包括使用 `Zend_Auth_Adapter_Ldap` 的指南、它的 API 、各种可用选项的大纲、认证问题故障排除的诊断信息和Active Directory 与 OpenLDAP 服务器的范例选项。

## 用法

为了快速把 `Zend_Auth_Adapter_Ldap` 认证集成到你的程序中，即使你不使用 `Zend_Controller`，代码的基本部分看起来是这样的：

    $username = $this->_request->getParam('username');
    $password = $this->_request->getParam('password');

    $auth = Zend_Auth::getInstance();

    $config = new Zend_Config_Ini('../application/config/config.ini',
                                  'production');
    $log_path = $config->ldap->log_path;
    $options = $config->ldap->toArray();
    unset($options['log_path']);

    $adapter = new Zend_Auth_Adapter_Ldap($options, $username,
                                          $password);

    $result = $auth->authenticate($adapter);

    if ($log_path) {
        $messages = $result->getMessages();

        $logger = new Zend_Log();
        $logger->addWriter(new Zend_Log_Writer_Stream($log_path));
        $filter = new Zend_Log_Filter_Priority(Zend_Log::DEBUG);
        $logger->addFilter($filter);

        foreach ($messages as $i => $message) {
            if ($i-- > 1) { // $messages[2] and up are log messages
                $message = str_replace("\n", "\n  ", $message);
                $logger->log("Ldap: $i: $message", Zend_Log::DEBUG);
            }
        }
    }

虽然日志（ logging ）代码是可选的，还是强烈建议使用日志。`Zend_Auth_Adapter_Ldap` 将记录任何想要的信息细节到 `$messages` （更多的信息看下面），对于难以调试的程序来说，有历史记录这是个很好的功能。

上面用到 `Zend_Config_Ini` 的代码是来加载适配器选项，它也是可选的，使用一个规则的数组来完成工作。下面是带有两个单独的服务器选项的 `application/config/config.ini` 文件的例子。带有多组服务器选项的适配器将按顺序来尝试每个服务器直到资格被成功认证。服务器的名字 （例如 `server1` and `server2`）是任意的，关于选项数组的细节，参见下面 Server Options 一节。 注意 `Zend_Config_Ini` 要求任何带有等号（`=`）的值需要括起来（如下面的 DNs）。

    [production]

    ldap.log_path = /tmp/ldap.log

    ; Typical options for OpenLDAP
    ldap.server1.host = s0.foo.net
    ldap.server1.accountDomainName = foo.net
    ldap.server1.accountDomainNameShort = FOO
    ldap.server1.accountCanonicalForm = 3
    ldap.server1.username = "CN=user1,DC=foo,DC=net"
    ldap.server1.password = pass1
    ldap.server1.baseDn = "OU=Sales,DC=foo,DC=net"
    ldap.server1.bindRequiresDn = true

    ; Typical options for Active Directory
    ldap.server2.host = dc1.w.net
    ldap.server2.useSsl = true
    ldap.server2.accountDomainName = w.net
    ldap.server2.accountDomainNameShort = W
    ldap.server2.accountCanonicalForm = 3
    ldap.server2.baseDn = "CN=Users,DC=w,DC=net"

上述的配置将指示 `Zend_Auth_Adapter_Ldap` 首先来尝试用 OpenLDAP 服务器 `s0.foo.net` 来认证用户，如果不论什么原因认证失败，将尝试 AD 服务器 `dc1.w.net`。

这个配置示例了在不同的域的服务器的多域认证，也可以在同一域中用多个服务器来提供冗余。

注意在这个例子中，即使 OpenLDAP 不需要用于 Windows 的短 NetBIOS 风格的域名，我们仍在这里提供它以保证命名正规化 （参见下面的 Username Canonicalization 一节）。

## The API

`Zend_Auth_Adapter_Ldap` 构造器接受三个参数。

`$options` 参数是必需的并且是一个包含一组或多组选项的数组。注意它是 Zend_Ldap 选项的 数组的数组 。即使你只使用一个 LDAP 服务器，选项仍要包含在另一个数组中。

下面是一个选项参数包含两组服务器选项（ `s0.foo.net` 和 `dc1.w.net` （和上面 INI 表示法一样的选项））的例子的 `print_r()` 输出：

    Array
    (
        [server2] => Array
            (
                [host] => dc1.w.net
                [useSsl] => 1
                [accountDomainName] => w.net
                [accountDomainNameShort] => W
                [accountCanonicalForm] => 3
                [baseDn] => CN=Users,DC=w,DC=net
            )

        [server1] => Array
            (
                [host] => s0.foo.net
                [accountDomainName] => foo.net
                [accountDomainNameShort] => FOO
                [accountCanonicalForm] => 3
                [username] => CN=user1,DC=foo,DC=net
                [password] => pass1
                [baseDn] => OU=Sales,DC=foo,DC=net
                [bindRequiresDn] => 1
            )

    )

上述每组选项提供的信息是不同的，主要是因为当绑定（参见下面 服务器选项 一节中的 `bindRequiresDn` 选项）时 AD 不要求在 DN 表单中的有用户名，这意味着我们可以忽略很多和为认证用户名获取 DN 相关的选项。

### 什么是 DN?

DN 或者 "distinguished name" 是一个字符串，表示在 LDAP 目录中到一个对象的路径。每个用逗号分隔的组件是一个属性并且它的值表示一个节点。组件是按反顺序来算的，例如：用户账户 CN=Bob Carter,CN=Users,DC=w,DC=net 直接位于 CN=Users,DC=w,DC=net container 里的。这种结构用 LDAP 浏览器如 ADSI Edit MMC snap-in for Active Directory 或 phpLDAPadmin 可以最好地浏览。

服务器名（如上面的 '`server1`' 和 '`server2`'）是任意的，但因为使用 `Zend_Config`，标识符（identifiers）应当以数字索引的相反出现并且不应当包含任何用于相关文件格式（例如，'`.`' INI 属性分隔符，XML 条目参考 '`&`' 等）的特殊字符。

用多组服务器选项，适配器可以在多域的环境中认证用户并提供 failover （估计是失败后尝试下一个服务器），所以如果一个服务器不可用，将查询另一个。

### 非常详细的介绍 （The Gory Details）－ 在认证方法中到底发生了什么？

当调用 `authenticate()` 方法，适配器反复把每组服务器选项设置到内部 `Zend_Ldap` 实例并带用于认证的用户名和密码调用 `Zend_Ldap::bind()` 方法。`Zend_Ldap` 类检查用户名是否在域中合格 （例如，有域的组件如 alice@foo.net 或 FOO\alice）。如果域存在，但它不匹配任何一种服务器的域名（foo.net 或 FOO），就抛出一个特殊的异常并由 `Zend_Auth_Adapter_Ldap` 捕捉，这样那个服务器就被忽略并且选择下个服务器选项。如果域名 确实 匹配，但是如果用户没有提供一个合格的用户名，`Zend_Ldap` 继续尝试绑定被提供的证书（credentials）。如果绑定不成功，`Zend_Ldap` 抛出一个由 `Zend_Auth_Adapter_Ldap` 捕捉的 `Zend_Ldap_Exception` 并尝试下一组服务器选项。如果绑定成功，反复尝试（迭代？(iteration)）就停止，并且适配器的 `authenticate()` 方法返回一个成功的结果。如果所有服务器选项都试过了而且都不成功，认证就失败了，`authenticate()` 返回一个失败的结果并带有最后一个尝试的错误消息。

`Zend_Auth_Adapter_Ldap` 构造器的用户名和密码参数是要被认证的证书（例如，用户通过 HTML 登录表单提供的证书（credentials））。另外，也可以通过 `setUsername()` 和 `setPassword()` 方法来设置。

## 服务器选项

在 Zend_Auth_Adapter_Ldap 的上下文中 的每组服务器选项包含下列选项，它们基本上不可修改地传递给 `Zend_Ldap::setOptions()`：

### 服务器选项

名称 | 描述
--- | ---
host | 这些选项表示的 LDAP 服务器的主机名，该选项是必需的。
port | LDAP 服务器监听的端口，如果 useSsl 是 `true`，缺省 端口 值是 636。如果 useSsl 是 `false`，缺省 端口 值是 389。
useSsl | 如果是 `true`，表示 LDAP 客户端应当使用 SSL / TLS 加密传输。在生产环境中强烈建议使用 `true` 值以防止明文传输密码。缺省值为 `false` 是因为服务器经常在安装之后请求被分别安装的证书。它也改变缺省 端口 值（ 见上面 端口 的描述）。
username | 账户的 DN， 用来执行账户 DN 查找。LDAP servers that require the username to be in DN form when performing the "bind" require this option （这句没有理解）。 如果 bindRequiresDn 是 `true`，这个选项是必需的。这个账户不需要是优先账户 － a account with read-only access to objects under the baseDn is all that is necessary (and preferred based on the Principle of Least Privilege).
password | 账户的密码，用来执行账户 DN 查找。如果没有提供这个选项，当执行账户 DN 查找时，LDAP 客户端将尝试“匿名绑定”。

bindRequiresDn

一些 LDAP 服务器要求用户名以 DN 格式来绑定，如 CN=Alice Baker,OU=Sales,DC=foo,DC=net （基本上 除了 AD 以外所有的服务器）。如果这个选项是 `true`，`Zend_Ldap` 自动获取被认证的用户所对应的 DN。如果它不是 DN 格式，那就重新绑定合适的 DN。缺省值是 `false`。目前，当绑定时，只有微软的 Active Directory 服务器（ADS） 不 要求用户名为 DN 格式，所以和 AD 一起使用，这个选项可以是 `false` （而且应当是，因为获取 DN 需要额外的过程（round trip）到服务器），否则，这个选项必需设置为 `true` （例如，OpenLDAP）。当搜索账户时，这个选项也控制缺省的 acountFilterFormat，参见 accountFilterFormat 选项。

baseDn | 定位所有被认证账户下的 DN，这个选项是必需的。如果你不能确定正确的 baseDn 值，可以用 DC= 组件从用户的 DNS 域来产生它，例如，如果用户的基本名是 alice@foo.net，DC=foo,DC=net 的 baseDn 应当工作。然而更精确的位置（例如 OU=Sales,DC=foo,DC=net ）将更有效。

accountCanonicalForm

一个是 2、3 或 4 的值，用来指示那个账户名在成功认证后需要规范化。值的解释具体如下：2 表示传统的用户名（例如 alice ），3 表示反斜杠式（backslash-style）名称（例如 FOO\alice），或者 4 表示基本式用户名（例如 alice@foo.net）。缺省值为 4 （例如 alice@foo.net ）。例如，当值为 3，由 `Zend_Auth_Result::getIdentity()` （如果使用了 `Zend_Auth`，则是 `Zend_Auth::getIdentity()`，） 返回的身份（identity）将总是 FOO\alice，不论 Alice 提供了什么格式，如 alice、 alice@foo.net、 FOO\alice、FoO\aLicE、 foo.net\alice 等。见 `Zend_Ldap` 中的 Account Name Canonicalization 一节有更多的细节。注意当使用多组服务器选项时，建议但不要求所有服务器选项使用相同的 accountCanonicalForm，这样，用户名对于同一格式总是规范化的（例如，对于 AD 服务器规范化为 EXAMPLE\username，但对于 OpenLDAP 服务器规范化为 username@example.com，对于程序的高水平（high-level）逻辑，这可能很不好用。）

accountDomainName | 目标 LDAP 服务器的 FQDN 域名是一个授权（authority）（例如 `example.com`）。该选项用来规范化名字，这样用户提供的用户名可以为绑定按需转换。它也可用来决定是否服务器对用户名是一个授权（例如 accountDomainName 是 foo.net 并且用户提供了 bob@bar.net，将不查询服务器并导致一个错误）。该选项不是必需的，但如果不提供，那就不支持用户名为基本名（principal name）格式（例如 alice@foo.net）。强烈建议使用该选项，因为许多用例要求生成基本名格式。
accountDomainNameShort | 目标 LDAP 服务器的 ‘短’ 域名是一个授权（authority）（例如 FOO）。注意按 1:1 来映射 accountDomainName 和 accountDomainNameShort。该选项用于为 Windows 网络指定 NetBIOS 名但也可用于非 AD 服务器（例如，当多组服务器选用使用反斜杠风格的 accountCanonicalForm时为了保持一致性）。该选项不是必需的但如果不使用，就不支持反斜杠格式的用户名（例如 FOO\alice）。
accountFilterFormat | 用来搜索账户的 LDAP 搜索过滤器。这个字符串是个 `printf()` 风格的表达式，必需包含一个 '`%s`' 来适合用户名。缺省值为 '`(&(objectClass=user)(sAMAccountName=%s))`'，除非 bindRequiresDn 设置为 `true`，那样缺省值就是 '`(&(objectClass=posixAccount)(uid=%s))`'。例如，如果因为某种原因你想对 AD 使用 `bindRequiresDn = true` ，需要设置 `accountFilterFormat = '(&(objectClass=user)(sAMAccountName=%s))`'。

如果你设置 `useSsl = true` 可能发现 LDAP 客户端会产生一个不能校验服务器证书的错误。假定 PHP LDAP 扩展完全链接到 OpenLDAP 客户库，为解决这个问题你可以在 OpenLDAP 客户 `ldap.conf` 里设置 “`TLS_REQCERT never`” （并重启 web 服务器）来指明 OpenLDAP 客户端库你信任这个服务器。另外如果涉及到服务器可能被欺骗，你可以输出 LDAP 服务器的根证书并把它放到 web 服务器，这样 OpenLDAP 客户端可以校验服务器的身份。

## 收集调试信息

`Zend_Auth_Adapter_Ldap` 在它的 `authenticate()` 方法里收集调试信息。这个信息存储在 `Zend_Auth_Result` 对象里。下面描述由 `Zend_Auth_Result::getMessages()` 返回的数组：

### 调试信息 （Messages）

信息（Messages） 数组索引 | 描述
--- | ---
Index 0 | 显示给用户的用户友好的一般信息（例如无效的证书（credentials））。如果认证成功，这个字符串是空的。
Index 1 | 更详细的错误信息，不适合显示给用户但作为服务器操错日志。如果认证成功，这个字符串是空的。
Indexes 2 and higher | 所有日志信息按顺序从 index 2 开始。

实践上，index 0 显示给用户（例如使用 FlashMessenger 助手）， index 1 作为日志，如果收集到调试信息， index 2 和它以后的 index 也作为日志（尽管最终的信息总是从 index 1 的字符串开始）。

## 特定服务器的通用选项

### Active Directory 的选项

对于 ADS，下列选项值得注意：

#### Active Directory 的选项

名字 | 另外的注释
--- | ---
host | 适用所有的服务器，该选项必需。
useSsl | 因为安全的缘故，如果服务器安装了必要的证书，这个应该是 `true`。
baseDn | 适用所有的服务器，该选项必需。缺省地 AD 把所有用户账户放在 Users 容器中 （例如 CN=Users,DC=foo,DC=net），但在大型组织里缺省不常见，要询问 AD 管理员你的程序账户的最好的 DN 是什么。
accountCanonicalForm | 几乎可以确定你想要这个值为 3 来使用反斜杠式的名称（例如 FOO\alice），这对于 Windows 用户来说是最熟悉的。你 不 应该使用不合格的格式 2 （例如 alice），因为它可能授权在其它信任域里（例如 BAR\alice 和 FOO\alice 将被当作相同的用户）相同名字的用户访问你的程序。（参见下面的注释）
accountDomainName | 使用 AD 时这是必需的除非使用 accountCanonicalForm 2 ，再强调一下，我们不鼓励这样用。
accountDomainNameShort | AD 服务器是授权的域用户的 NetBIOS 名称。如果使用反斜杠风格 accountCanonicalForm，这个是必需的。

从技术角度讲，用当前的 `Zend_Auth_Adapter_Ldap` 实现进行跨域认证是没有危险的，因为服务器域是被显式检查的，但对将来的实现未必是对的，如在运行时发现域名或者如果使用替代的适配器（例如 Kerberos）。一般来说，含糊的账户名是安全问题的来源，所以最好使用合格的账户名称。

### OpenLDAP 的选项

对于 OpenLDAP 或一般的使用典型的 posixAccount 风格的 LDAP 服务器，下面的选项值得注意：

#### OpenLDAP 的选项

名字 | 另外的注释
--- | ---
host | 适用所有的服务器，该选项必需。
useSsl | 因为安全的缘故，如果服务器安装了必要的证书，这个应该是 `true`。
username | 必需并一定是一个 DN，因为当执行绑定时 OpenLDAP 要求 DN 格式的用户名。设法使用无特权的账户。
password | 对应上述用户名的密码，如果 LDAP 服务器支持匿名绑定，这个也许会忽略。
bindRequiresDn | 必需并一定是 `true`，因为当执行绑定时 OpenLDAP 要求 DN 格式的用户名。
baseDn | 适用所有的服务器，该选项是必需的并指示所有被认证的账户的 DN 的定位。
accountCanonicalForm | 可选但缺省值是 4 （基本风格名如 alice@foo.net），如果适用反斜杠式的名字（如 FOO\alice）这个也许不是理想的。对于反斜杠式名字值为 3。
accountDomainName | 必需，除非使用不推荐的 accountCanonicalForm 2，
accountDomainNameShort | 如果不使用 AD ，这个值不是必需的。否则，如果使用 accountCanonicalForm 3 ，该选项必需并是个完全对应 accountDomainName 的短名 （例如如果 accountDomainName 是 foo.net，一个好的 accountDomainNameShort 值可能是 FOO）。