从 ASP .NET 进行 Active Directory 域服务身份验证
本主题说明 ASP.NET 应用程序如何使用 Forms 身份验证来允许用户使用轻型目录访问协议 (LDAP) 对 Active Directory 域服务进行身份验证。在对用户进行身份验证和重定向后,可以使用 Global.asax 文件的 Application_AuthenticateRequest 方法在 HttpContext.User 属性中存储 GenericPrincipal 对象(该对象贯穿整个请求)。
创建新的 ASP.NET Web 应用程序
启动 Microsoft Visual Studio .NET。
在“文件”菜单上,指向“新建”,然后单击“项目”。
在“项目类型”下,单击“Visual C# 项目”,然后在“模板”下单击“ASP.NET Web 应用程序”。
在“名称”框中,键入 FormsAuthAd。
如果您使用的是本地服务器,则在“服务器”框中保留默认的 http://localhost。否则,添加指向您所用服务器的路径。单击“确定”。
在“解决方案资源管理器”中,右键单击“引用”节点,然后单击“添加引用”。
在“添加引用”对话框中的 .NET 选项卡上,依次单击 System.DirectoryServices.dll、“选择”和“确定”。
添加 System.DirectoryServices 身份验证代码
在“解决方案资源管理器”中,右键单击项目节点,指向“添加”,然后单击“添加新项”。
在“模板”下,单击“类”。
在“名称”框中键入 LdapAuthentication.cs,然后单击“打开”。
用下面的代码替换 LdapAuthentication.cs 文件中的现有代码:
using System;
using System.Text;
using System.Collections;
using System.Web.Security;
using System.Security.Principal;
using System.DirectoryServices;
namespace FormsAuth
{
public class LdapAuthentication
{
private string _path;
private string _filterAttribute;
public LdapAuthentication(string path)
{
_path = path;
}
public bool IsAuthenticated(string domain, string username, string pwd)
{
string domainAndUsername = domain + @"/" + username;
DirectoryEntry entry = new DirectoryEntry(_path, domainAndUsername, pwd);
try
{
//Bind to the native AdsObject to force authentication.
object obj = entry.NativeObject;
DirectorySearcher search = new DirectorySearcher(entry);
search.Filter = "(SAMAccountName=" + username + ")";
search.PropertiesToLoad.Add("cn");
SearchResult result = search.FindOne();
if(null == result)
{
return false;
}
//Update the new path to the user in the directory.
_path = result.Path;
_filterAttribute = (string)result.Properties["cn"][0];
}
catch (Exception ex)
{
throw new Exception("Error authenticating user. " + ex.Message);
}
return true;
}
public string GetGroups()
{
DirectorySearcher search = new DirectorySearcher(_path);
search.Filter = "(cn=" + _filterAttribute + ")";
search.PropertiesToLoad.Add("memberOf");
StringBuilder groupNames = new StringBuilder();
try
{
SearchResult result = search.FindOne();
int propertyCount = result.Properties["memberOf"].Count;
string dn;
int equalsIndex, commaIndex;
for(int propertyCounter = 0; propertyCounter < propertyCount; propertyCounter++)
{
dn = (string)result.Properties["memberOf"][propertyCounter];
equalsIndex = dn.IndexOf("=", 1);
commaIndex = dn.IndexOf(",", 1);
if(-1 == equalsIndex)
{
return null;
}
groupNames.Append(dn.Substring((equalsIndex + 1), (commaIndex - equalsIndex) - 1));
groupNames.Append("|");
}
}
catch(Exception ex)
{
throw new Exception("Error obtaining group names. " + ex.Message);
}
return groupNames.ToString();
}
}
}
在前面的过程中,身份验证代码接受域、用户名、密码和指向 Active Directory 域服务中的树的路径。此代码使用 LDAP 目录提供程序。Logon.aspx 页中的代码调用 LdapAuthentication.IsAuthenticated 方法并传入从该用户收集的凭据。然后,创建一个 DirectoryEntry 对象,该对象包含指向目录树的路径、用户名和密码。用户名必须采用“域/用户名”格式。
然后,DirectoryEntry 对象通过获取 NativeObject 属性,尝试强制绑定 AdsObject。如果上述操作成功,则通过创建 DirectorySearcher 对象并按 sAMAccountName 进行筛选,获取用户的 CN 属性。有关 Active Directory 域服务架构中 有关 sAMAccountName 的详细信息,请参阅 MSDN Library 中的“sAMAccountName”或“SAM-Account-Name attribute(SAM-Account-Name 属性)”。在对用户进行身份验证后,IsAuthenticated 方法返回 true。为获取用户所属的组列表,此代码调用 LdapAuthentication.GetGroups 方法。LdapAuthentication.GetGroups 方法通过创建 DirectorySearcher 对象并根据 memberOf 属性进行筛选,获取用户所属的安全和通讯组的列表。有关 Active Directory 域服务架构中 有关 memberOf 的详细信息,请参阅 MSDN Library 中的“memberOf”或“Is-Member-Of-DL attribute(Is-Member-Of-DL 属性)”。此方法返回由竖线 (|) 分隔的组列表。请注意,LdapAuthentication.GetGroups 方法会对字符串进行截断处理。这将缩短在身份验证 cookie 中存储的字符串的长度。如果字符串未被截断,则每组的格式将按如下显示:
CN=...,...,DC=domain,DC=com
LdapAuthentication.GetGroups 方法可能返回非常长的字符串。如果此字符串的长度大于 cookie 的长度,可能不会创建身份验证 cookie。如果此字符串可能超出 cookie 的长度,则您可能需要在 ASP.NET 缓存对象或数据库中存储组信息。或者,您可能需要对组信息进行加密,并在隐藏的窗体域中存储此信息。
Global.asax 文件中的代码提供 Application_AuthenticateRequest 事件处理程序。此事件处理程序从 Context.Request.Cookies 集合检索身份验证 cookie,对 cookie 进行解密,并且检索将在 FormsAuthenticationTicket.UserData 属性中存储的组的列表。这些组显示在 Logon.aspx 页中创建的、用竖线分隔的列表中。代码对字符串数组中的字符串进行分析,以创建 GenericPrincipal 对象。在创建 GenericPrincipal 对象后,将该对象放置于 HttpContext.User 属性中。
编写 Global.asax 代码
在“解决方案资源管理器”中,右键单击 Global.asax,然后单击“查看代码”。
将以下代码添加到 Global.asax.cs 文件的代码隐藏的顶部:
using System.Web.Security;
using System.Security.Principal;
使用以下代码替换 Application_AuthenticateRequest 的现有空事件处理程序:
void Application_AuthenticateRequest(object sender, EventArgs e)
{
string cookieName = FormsAuthentication.FormsCookieName;
HttpCookie authCookie = Context.Request.Cookies[cookieName];
if(null == authCookie)
{
//There is no authentication cookie.
return;
}
FormsAuthenticationTicket authTicket = null;
try
{
authTicket = FormsAuthentication.Decrypt(authCookie.Value);
}
catch(Exception ex)
{
//Write the exception to the Event Log.
return;
}
if(null == authTicket)
{
//Cookie failed to decrypt.
return;
}
//When the ticket was created, the UserData property was assigned a
//pipe-delimited string of group names.
string[] groups = authTicket.UserData.Split(new char[]{'|'});
//Create an Identity.
GenericIdentity id = new GenericIdentity(authTicket.Name, "LdapAuthentication");
//This principal flows throughout the request.
GenericPrincipal principal = new GenericPrincipal(id, groups);
Context.User = principal;
}
在本节中,您将配置 Web.config 文件中的 <forms>、<authentication> 和 <authorization> 元素。通过这些更改,只有经过了身份验证的用户才能访问该应用程序,并且未经身份验证的请求将被重定向到 Logon.aspx 页。您可以修改此配置,以便只允许某些用户和组访问该应用程序。
修改 Web.config 文件
在记事本中打开 Web.config。
用下面的代码替换现有代码:
<?xml version="1.0" encoding="utf-8"?> <configuration> <system.web> <authentication mode="Forms"> <forms loginUrl="logon.aspx" name="adAuthCookie" timeout="10" path="/"> </forms> </authentication> <authorization> <deny users="?"/> <allow users="*"/> </authorization> <identity impersonate="true"/> </system.web> </configuration>
请注意以下配置元素:
<identity impersonate="true"/>
对于配置为来自 Microsoft Internet 信息服务 (IIS) 的匿名帐户的帐户,上述元素将导致 ASP.NET 模拟该帐户。此配置导致的结果是,对此应用程序的所有请求都基于所配置的帐户的安全上下文运行。该用户提供凭据以针对 Active Directory 域服务进行身份验证,但访问 Active Directory 域服务的帐户是已配置的帐户。
为匿名身份验证配置 IIS
在 IIS 管理器(在“管理工具”中)或用于 IIS 的 MMC 管理单元中,右键单击您要为其配置身份验证的网站,然后单击“属性”。
单击“目录安全性”选项卡,然后在“身份验证和访问控制”下,单击“编辑”。
选中“匿名身份验证”复选框(在 Windows Server 2003 中标记为“启用匿名访问”)。
使应用程序的匿名帐户成为对 Active Directory 域服务具有权限的帐户。
如果启用了“允许 IIS 控制密码”复选框,则清除该复选框。默认的 IUSR_<computername> 帐户对 Active Directory 域服务不具有权限。
创建 Logon.aspx 页
在“解决方案资源管理器”中,右键单击项目节点,指向“添加”,然后单击“添加 Web 窗体”。
在“名称”框中,键入 Logon.aspx,然后单击“打开”。
在“解决方案资源管理器”中,右键单击 Logon.aspx,然后单击“视图设计器”。
单击“设计器”中的“HTML”选项卡。
用下面的代码替换现有代码:
<%@ Page language="c#" AutoEventWireup="true" %> <%@ Import Namespace="FormsAuth" %> <html> <body> <form id="Login" method="post" runat="server"> <asp:Label ID="Label1" Runat=server >Domain:</asp:Label> <asp:TextBox ID="txtDomain" Runat=server ></asp:TextBox><br> <asp:Label ID="Label2" Runat=server >Username:</asp:Label> <asp:TextBox ID=txtUsername Runat=server ></asp:TextBox><br> <asp:Label ID="Label3" Runat=server >Password:</asp:Label> <asp:TextBox ID="txtPassword" Runat=server TextMode=Password></asp:TextBox><br> <asp:Button ID="btnLogin" Runat=server Text="Login" OnClick="Login_Click"></asp:Button><br> <asp:Label ID="errorLabel" Runat=server ForeColor=#ff3300></asp:Label><br> <asp:CheckBox ID=chkPersist Runat=server Text="Persist Cookie" /> </form> </body> </html> <script runat=server> void Login_Click(object sender, EventArgs e) { string adPath = "LDAP://" + txtDomain.Text; LdapAuthentication adAuth = new LdapAuthentication(adPath); try { if(true == adAuth.IsAuthenticated(txtDomain.Text, txtUsername.Text, txtPassword.Text)) { string groups = adAuth.GetGroups(txtDomain.Text, txtUsername.Text, txtPassword.Text); //Create the ticket, and add the groups. bool isCookiePersistent = chkPersist.Checked; FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(1, txtUsername.Text,DateTime.Now, DateTime.Now.AddMinutes(60), isCookiePersistent, groups); //Encrypt the ticket. string encryptedTicket = FormsAuthentication.Encrypt(authTicket); //Create a cookie, and then add the encrypted ticket to the cookie as data. HttpCookie authCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket); if(true == isCookiePersistent) authCookie.Expires = authTicket.Expiration; //Add the cookie to the outgoing cookies collection. Response.Cookies.Add(authCookie); //You can redirect now. Response.Redirect(FormsAuthentication.GetRedirectUrl(txtUsername.Text, false)); } else { errorLabel.Text = "Authentication did not succeed. Check user name and password."; } } catch(Exception ex) { errorLabel.Text = "Error authenticating. " + ex.Message; } } </script>
修改 Logon.aspx 页中的路径,以指向您的 LDAP 目录服务器。
Logon.aspx 页是一个用于收集来自用户的信息并调用 LdapAuthentication 类上的方法的页面。在代码对用户进行身份验证并获取了组列表后,该代码创建一个 FormsAuthenticationTicket 对象,对票证进行加密,向一个 cookie 添加加密的票证,向 HttpResponse.Cookies 集合添加该 cookie,然后将请求重定向到最初请求的 URL。
WebForm1.aspx 页是最初请求的页。在用户请求此页时,请求被重定向到 Logon.aspx 页。在对请求进行身份验证后,该请求被重定向到 WebForm1.aspx 页。
修改 WebForm1.aspx 页
在“解决方案资源管理器”中,右键单击 WebForm1.aspx,然后单击“视图设计器”。
单击“设计器”中的“HTML”选项卡。
用下面的代码替换现有代码:
<%@ Page language="c#" AutoEventWireup="true" %> <%@ Import Namespace="System.Security.Principal" %> <html> <body> <form id="Form1" method="post" runat="server"> <asp:Label ID="lblName" Runat=server /><br> <asp:Label ID="lblAuthType" Runat=server /> </form> </body> </html> <script runat=server> void Page_Load(object sender, EventArgs e) { lblName.Text = "Hello " + Context.User.Identity.Name + "."; lblAuthType.Text = "You were authenticated using " + Context.User.Identity.AuthenticationType + "."; } </script>
保存所有文件,然后编译该项目。
请求 WebForm1.aspx 页。请注意,您将被重定向到 Logon.aspx。
键入登录凭据,然后单击“提交”。在您重定向到 WebForm1.aspx 后,请注意,您的用户名将出现并且 LdapAuthentication 是用于 Context.User.Identity.AuthenticationType 属性的身份验证类型。
注意:
建议您在使用窗体身份验证时使用安全套接字层 (SSL) 加密。这是因为用户是基于身份验证 cookie 标识的;所以,对此应用程序进行 SSL 加密将防止任何人损害传输的身份验证 cookie 以及任何其他重要信息。