ADAM 和 LDAP Client.Net - ASP.NET 的 LDAP 访问控制






4.71/5 (10投票s)
使用 LDAP 进行 ASP.NET 身份验证和授权。
引言
这是一个如何使用 ADAM 构建 ASP.NET LDAP 身份验证应用程序的示例。
微软的 ADAM(Active Directory 应用程序模式)是应用程序开发人员常用的 LDAP 服务器。它免费、易于安装,并且可以“成长”为功能完备的 Active Directory。如果您的应用程序在仅限 Windows 的环境中使用 NTLM 身份验证运行,它也易于使用。
然而,许多 LDAP 应用程序的客户端运行在 Windows 以外的操作系统上,以及不在与服务器相同域中的 Windows 客户端。当 ADAM 配置用于这些类型的场景时,它变得更难以使用,因为它需要一些额外的管理设置和一些额外的代码才能连接到它。
本文描述了如何配置 ADAM 进行常规 LDAP 通信,并在此基础上构建一个样板 ASP.NET 应用程序。我们将使用不使用 NTLM 身份验证的 LDAP 客户端,以表明这可以在任何客户端和任何平台上完成。
在本教程结束时,您将拥有一个能够针对目录进行用户身份验证的 ASP.NET 应用程序
创建新用户
并显示活动用户的凭据
注意:ADAM 在属于域的 Windows Server、不属于域的 Windows Server 和 Windows XP 上的行为存在细微差异。此示例基于在 Windows XP 上运行的 ADAM。在 Windows Server 上,可能需要额外的步骤才能使此示例正常工作。
必备组件
为了运行此示例,您需要
- ADAM - ADAM 可从 http://www.microsoft.com/downloads/details.aspx?FamilyId=9688F8B9-1034-4EF6-A3E5-2A2A57B5C8E4&displaylang=en 下载。
- LDAP Client.Net - 为了展示不使用 NTLM 进行身份验证的场景,我们将使用 LDAP Services 的 LDAP Client.Net。LDAP Client.Net 具有直观的 API,因此这些示例易于理解。如果您还没有它,可以从 http://www.ldapservices.com/Products/LdapClient.Net/default.aspx 下载。
创建 ADAM 目录实例
下载并安装上述先决条件后,使用所有默认设置创建 ADAM 目录实例。点击 开始 启动 ADAM 设置向导
创建新实例
实例名称是任意的
默认的 LDAP 端口是 389。如果它已经被另一个实例占用,我们可以选择一个不同的端口
为您的应用程序数据创建分区。默认情况下,ADAM 不会创建分区,因为应用程序通常会自行创建,但我们的示例没有这个需求
选择安装目录
同样,为了简化配置,使用 Network Service 帐户作为服务帐户
将自己添加为 LDAP 实例的初始管理员
选择要导入的 LDIF 文件。这些文件包含模式定义,并描述您将能够在目录实例中存储什么类型的数据。您可以稍后导入更多 LDIF 文件,现在我们只需要用户信息
在摘要页上单击下一步,然后坐下来等待 ADAM 设置您的 LDAP 实例
此过程完成后,您将拥有一个安装了 MS-User 架构的空目录。您只能使用 ADSI 并且只能以您自己的身份连接到它。接下来,我们将一些用户放入目录中,之后,我们将使用这些用户进行目录本身的身份验证。
插曲
此时,我们必须进行配置更改以放宽一些锁定的默认设置:我们需要启用通过非 SSL 连接更改用户密码。
我们为什么需要这样做?因为我们想在 ADAM 中创建用户,我们想给这些用户密码,并且我们不想设置 SSL。如果我们要让 ADAM 使用 Windows 主体(例如域用户)进行目录身份验证,那么我们就不必进行此更改,因为 ADAM 不会存储密码(或用户,就此而言)。但是,由于本练习的重点是将我们自己与 Active Directory 和 NTLM 解耦,并且因为我们想避免设置 SSL 的额外复杂性,我们将禁用阻止通过非 SSL 通道进行密码操作的安全设置。
虽然在开发和原型环境中令人头疼,但这种安全配置是您应该在生产服务器中使用的。在这些情况下,您应该配置 SSL 而不是禁用此设置。然而,作为奖励,这给了我们一个机会来介绍一个不可或缺的 ADAM 工具:ADSIEdit。
要运行 ADSIEdit,请单击 开始
右键单击 ADAM ADSI Edit 并选择 连接到... 选择 配置 命名上下文,给此连接一个有用的名称,然后单击 确定。
ADAM 使用一个单独的命名上下文(也称为“分区”)来存储该 ADAM 服务实例的设置。这里的设置使用与几分钟前我们创建的另一个分区中存储用户、组织单元、计算机和组完全相同的机制存储。
我们正在寻找的设置位于名为 CN=Directory Service,CN=Windows NT,CN=Services,CN=Configuration,CN={配置命名上下文 GUID} 的对象中。确切的名称因 ADAM 实例而异,但它始终位于相同的位置。
右键单击名为 CN=Directory Service 的对象,然后单击 属性。您现在正在查看 Directory Service 对象的所有属性。我们感兴趣的属性名为 dsHeuristics。与它的名称相反,此属性存储安全策略,而不是存储启发式。通过选择它,单击 编辑,键入(或粘贴值 0000000001001),然后单击两次确定,将其值更改为 0000000001001
。
现在我们已经修改了 ADAM 目录服务实例的配置,以便我们可以为即将创建的用户设置密码。让我们继续!
创建新的 ADAM 用户
此时,只有您可以登录您的目录实例,并且因为您的用户帐户是 Windows 帐户
- ADAM 不允许您在没有 SSL 的情况下通过通用 LDAP 接口使用您的网络凭据登录,并且
- 无论如何,您都不应该在没有 SSL 的情况下使用您的网络凭据,因为有人可能会嗅探它们,从而不仅危及您的目录,还会危及您有权访问的任何网络资源。
我们将使用 ADSIEdit 创建两个新用户。第一个用户将是初始管理员,第二个用户将是 ASP.NET 应用程序的服务帐户。
按照与上面相同的步骤连接到目录实例,但这次,我们将连接到您的新命名上下文 (cn=Sandbox,dc=ITOrg
),而不是配置命名上下文。请注意,我们仍然以当前用户身份连接,即通过 NTLM 作为 Windows 用户。
现在我们需要创建一个容器来存储我们新的(以及未来的)用户。导航到 CN=Sandbox,DC=ITOrg 容器,右键单击它,然后选择 新建->对象... 选择 容器,然后单击 下一步。
使用名称 People
。单击 下一步 和 完成 创建容器。
接下来,导航到 CN=People 容器,右键单击它,然后选择 新建->对象... 选择 用户,然后单击 下一步。
为您即将成为管理员的用户指定一个 cn(常用名)。我使用了名称 superuser
。
然后,单击 下一步 和 完成 创建用户。
注意:默认情况下,在 Windows Server 上,当 ADAM 用户创建时,它们被禁用。要启用用户,请将其 msDS-UserAccountDisabled
属性的值更改为 false。
此时,超级用户并不是很超级。她在目录中没有任何权限,我们也没有给她密码,所以她甚至无法登录。让我们首先解决密码问题。右键单击此用户并单击 重置密码...
点击 确定。现在,我们的超级用户可以登录,但无法查看或执行任何操作。让我们通过将她添加到 CN=Roles 容器中的 Administrators 角色来赋予她一些权限。
右键单击 CN=Administrators,然后选择 属性。滚动到 成员 属性并编辑它。您将看到安全主体选择器。这里有两点值得注意
- 您可以添加两种类型的用户 - Windows 和 ADAM。
- 配置命名上下文中的 Administrators 组已添加到此组。作为家庭作业,返回到配置命名上下文并查看那里管理员组中的成员。
点击 添加 ADAM 帐户... 并输入我们超级用户的 DN (cn=superuser,cn=People,cn=Sandbox,dc=ITOrg
)
接下来,我们将重复创建用户的步骤来创建我们的服务帐户用户。我们将使用 ServiceAccount
作为用户的 cn,密码为 p@ssw0rd
,而不是使用 superuser
。请注意,我们在演示中使用服务帐户用户添加新条目,因此我们也需要将她添加到管理组。
瞧!我们现在有一个具有管理权限的 LDAP 用户,可以使用明文密码绑定到我们的目录,以及一个可以创建新用户的服务帐户用户。
使用 LDAP Client.Net 进行身份验证
既然我们现在有一个可以被任何 LDAP v3 客户端访问的目录,那么就来做这件事吧。我们已经构建了一个使用 Forms 身份验证 进行身份验证的样板 ASP.NET 应用程序。
注意:Forms 身份验证是 ASP.NET 的一个平台功能,它简化了构建对选定资源具有受限访问的应用程序。要理解本文的其余部分,不需要了解 Forms 身份验证的特定知识(上面提供的链接是那些想了解更多的人的良好起点)。
使用 Forms 身份验证确定用户是否被允许访问资源需要两个任务
- 验证输入的用户名和密码是否正确
- 验证用户是否属于有权访问所需资源的角色
我们将使用 LDAP Client.Net 来执行这些任务。下面的 Authenticate
方法验证用户的凭据
private bool Authenticate(string username, string password)
{
bool authenticated = false;
实例化 LDAP Client.Net
using (LdapServices.Ldap.Client client = new LdapServices.Ldap.Client())
{
try
{
// Connect to the directory as the user who is trying to
// authenticate. If this fails we know the username
// and/or password is incorrect.
LdapConnectionConfigurationSection config =
LdapConnectionConfigurationSection.Current;
使用我们尝试进行身份验证的凭据连接到目录
client.Connect(config.Server, config.Port, username, password);
// Force a re-fetch of the user's role information.
LdapRoleCache.Current.Remove(username);
authenticated = true;
}
catch (LdapException)
{
如果使用给定的用户名和密码进行身份验证失败,则会抛出 LdapException
。
authenticated = false;
}
}
return authenticated;
}
假设用户的凭据已验证,我们需要检查用户属于哪些角色。下面的 GetRolesForUser
方法完成此任务
private string[] GetRolesForUser(string username)
{
string[] roles = LdapRoleCache.Current[username];
if (roles == null)
{
// Use the LDAP connection information defined in web.config to
// retrieve the role membership information for this user.
using (LdapServices.Ldap.Client client = new LdapServices.Ldap.Client())
{
LdapConnectionConfigurationSection config =
LdapConnectionConfigurationSection.Current;
client.Connect(config.Server, config.Port, config.User, config.Password);
获取所有 distinguishedName
与我们用户名匹配的目录条目。应该只有一个。
LdapServices.Ldap.EntryCollection userEntries = client.Search(
config.BaseDn, "distinguishedName=" + username);
if (userEntries.Count == 1)
{
// Now retrieve the roles to which this user belongs and
// store them in the Application object.
memberof
属性包含条目所属的组的集合。每个组都被视为一个角色。
// We treat the memberof attribute as the collection of
// roles to which this user belongs.
LdapServices.Ldap.Attribute memberOf = userEntries[0].Attributes["memberof"];
// Copy the values from the memberof attribute into a string
// array.
roles = new string[memberOf.Values.Count];
for (int i = 0; i < memberOf.Values.Count; i++)
{
string adamGroupName = memberOf.Values[i].StringValue;
// Roles cannot contain commas in ASP.Net, so we are
// mapping commas to periods. The cache contains the
// original role names so we need to transform them
// before handing them off to ASP.Net.
roles[i] = adamGroupName.Replace(',', '.');
}
// Save the user's roles in our cache. We will access this
// cache later during the Application-level
// AuthenticateRequest event.
LdapRoleCache.Current[username] = roles;
}
}
}
// If we could not retrieve the roles for this user then return an
// empty array, indicating that the user is not a member of any roles.
if (roles == null)
{
roles = new string[] { };
}
return roles;
}
使用 LDAP Client.Net 创建用户
上面的示例展示了如何从我们的目录服务器读取数据。在此示例中,我们将使用 LDAP Client.Net 创建新用户,修改他们的密码属性,并将他们添加到角色中。
下面的 createButton_Click
方法是用于在我们示例应用程序中创建新用户的事件处理程序
protected void createButton_Click(object sender, EventArgs e)
{
try
{
LdapConnectionConfigurationSection config =
LdapConnectionConfigurationSection.Current;
using (LdapServices.Ldap.Client client = new LdapServices.Ldap.Client())
{
client.Connect(config.Server, config.Port, config.User, config.Password);
// Create the user.
NameValueCollection
用于指定用户的属性。此集合中的每个条目对应一个属性
NameValueCollection attributes = new NameValueCollection();
attributes.Add("objectClass", "user");
string userDn = "cn=" + cnTextBox.Text + "," + config.NewUsersContainerDn;
AddNewEntry
方法使用指定的属性创建一个新用户
LdapServices.Ldap.Entry newUser = client.AddNewEntry(userDn, attributes);
将属性添加到条目的另一种方法是通过其 Attributes
属性
newUser.Attributes.Add("userPassword", passwordTextBox.Text);
// Add the user to each specified role.
foreach (ListItem roleListItem in rolesCheckBoxList.Items)
{
if (roleListItem.Selected)
{
AddUserToRole
的实现如下所示
AddUserToRole(client, config.BaseDn, roleListItem.Value, userDn);
}
}
}
this.cnTextBox.Text = string.Empty;
this.rolesCheckBoxList.Items.Clear();
this.messageLabel.Text = "User created.";
this.messageLabel.Visible = true;
}
catch (LdapException ex)
{
this.messageLabel.Text = ex.ToString();
this.messageLabel.Visible = true;
}
}
AddUserToRole
方法将添加用户到角色的代码抽象出来
private void AddUserToRole(LdapServices.Ldap.Client client,
string baseDn, string roleDn, string userDn)
{
用户的 memberof
属性是只读的,所以我们必须将用户添加到角色的 member
属性。
LdapServices.Ldap.EntryCollection roles =
client.Search(baseDn, "distinguishedName=" + roleDn);
LdapServices.Ldap.AttributeCollection roleAttributes = roles[0].Attributes;
LdapServices.Ldap.Attribute memberAttribute = roleAttributes["member"];
如果 member
属性存在,那么我们将用户添加到其中。否则,我们必须创建该属性。
if (memberAttribute != null)
{
memberAttribute.Values.Add(userDn);
}
else
{
roleAttributes.Add("member", userDn);
}
}
历史
- 2006 年 10 月 8 日 - 初始版本。