Windows身份认证做为ASP.NET的默认认证方式,与Forms身份认证在许多基础方面是一样的。
要使用Windows身份认证模式,需要在web.config设置:
authentication mode= Windows /
Windows身份认证做为ASP.NET的默认认证方式,与Forms身份认证在许多基础方面是一样的。 上篇说过:认为ASP.NET的身份认证的最核心部分其实就是HttpContext.User这个属性所指向的对象。 在接下来的部分,将着重分析这个对象在二种身份认证中有什么差别。
在ASP.NET身份认证过程中,IPrincipal和IIdentity这二个接口有着非常重要的作用。前者定义用户对象的基本功能,后者定义标识对象的基本功能,不同的身份认证方式得到的这二个接口的实例也是不同的。
ASP.NET Windows身份认证是由WindowsAuthenticationModule实现的。 WindowsAuthenticationModule在ASP.NET管线的AuthenticateRequest事件中,使用从IIS传递到ASP.NET的Windows访问令牌(Token)创建一个WindowsIdentity对象,Token通过调用context.WorkerRequest.GetUserToken()获得,然后再根据WindowsIdentity 对象创建WindowsPrincipal对象,然后把它赋值给HttpContext.User。
在Forms身份认证中,们需要创建登录页面,让用户提交用户名和密码,然后检查用户名和密码的正确性,接下来创建一个包含FormsAuthenticationTicket对象的登录Cookie供后续请求使用。 FormsAuthenticationModule在ASP.NET管线的AuthenticateRequest事件中,解析登录Cookie并创建一个包含FormsIdentity的GenericPrincipal对象,然后把它赋值给HttpContext.User。
上面二段话简单了概括了二种身份认证方式的工作方式。
们可以发现它们存在以下差别:
1. Forms身份认证需要Cookie表示登录状态,Windows身份认证则依赖于IIS
2. Windows身份认证不需要们设计登录页面,不用编写登录验证逻辑,因此更容易使用。
在授权阶段,UrlAuthorizationModule仍然会根据当前用户检查将要访问的资源是否得到许可。接下来,FileAuthorizationModule检查 HttpContext.User.Identity 属性中的 IIdentity 对象是否是 WindowsIdentity 类的一个实例。如果 IIdentity 对象不是 WindowsIdentity 类的一个实例,则 FileAuthorizationModule 类停止处理。如果存在 WindowsIdentity 类的一个实例,则 FileAuthorizationModule 类调用 AccessCheck Win32 函数(通过 P/Invoke)来确定是否授权经过身份验证的客户端访问请求的文件。如果该文件的安全描述符的随机访问控制列表 (DACL) 中至少包含一个 Read 访问控制项 (ACE),则允许该请求继续。否则,FileAuthorizationModule 类调用 HttpApplication.CompleteRequest 方法并将状态码 401 返回到客户端。
在Windows身份认证中,验证工作主要是由IIS实现的,WindowsAuthenticationModule其实只是负责创建WindowsPrincipal和WindowsIdentity而已。顺便介绍一下:Windows 身份验证又分为 NTLM 身份验证 和 Kerberos v5 身份验证 二种,关于这二种Windows身份认证的更多说明可查看MSDN技术文章:解释:ASP.NET 2.0 中的 Windows 身份验证。在看来,IIS最终使用哪种Windows身份认证方式并不影响们的开发过程,因此本文不会讨论这个话题。
根据实际经验来看,使用Windows身份认证时,主要的开发工作将是根据登录名从Active Directory获取用户信息。因为,此时不需要们再设计登录过程,IIS与ASP.NET已经为们准备好了WindowsPrincipal和WindowsIdentity这二个与用户身份相关的对象。
访问 Active Directory
们通常使用LDAP协议来访问Active Directory,在.net framework中提供了DirectoryEntry和DirectorySearcher这二个类型让们可以方便地从托管代码中访问 Active Directory 域服务。
如果们要在 test.corp 这个域中搜索某个用户信息,们可以使用下面的语句构造一个DirectoryEntry对象:
DirectoryEntry entry = new DirectoryEntry( LDAP://test.corp );
在这段代码中,采用硬编码的方式把域名写进了代码。
们如何知道当前电脑所使用的是哪个域名呢?
答案是:查看 电脑 的属性对话框:
注意:这个域名不一定与System.Environment.UserDomainName相同。
除了可以查看 电脑 的属性对话框外,们还可以使用代码的方式获取当前电脑所使用的域名:
private static string GetDomainName() { // 注意:这段代码需要在Windows XP及较新版本的操作系统中才能正常运行。 SelectQuery query = new SelectQuery( Win32_ComputerSystem ); using( ManagementObjectSearcher searcher = new ManagementObjectSearcher(query) ) { foreach( ManagementObject mo in searcher.Get() ) { if( (bool)mo[ partofdomain ] ) return mo[ domain ].ToString(); } } return null; }
当构造了DirectorySearcher对象后,们便可以使用DirectorySearcher来执行对Active Directory的搜索。
们可以使用下面的步骤来执行搜索:
1. 设置 DirectorySearcher.Filter 指示LDAP格式筛选器,这是一个字符串。
2. 多次调用PropertiesToLoad.Add() 设置搜索过程中要检索的属性列表。
3. 调用FindOne() 方法获取搜索结果。
下面的代码演示了如何从Active Directory中搜索登录名为 fl45 的用户信息:
static void Main(string[] args) { Console.WriteLine(Environment.UserDomainName); Console.WriteLine(Environment.UserName); Console.WriteLine( ------------------------------------------------ ); ShowUserInfo( fl45 , GetDomainName()); } private static string AllProperties = name,givenName,samaccountname,mail ; public static void ShowUserInfo(string loginName, string domainName) { if( string.IsNullOrEmpty(loginName) || string.IsNullOrEmpty(domainName) ) return; string[] properties = AllProperties.Split(new char[] { '\r', '\n', ',' }, StringSplitOptions.RemoveEmptyEntries); try { DirectoryEntry entry = new DirectoryEntry( LDAP:// + domainName); DirectorySearcher search = new DirectorySearcher(entry); search.Filter = (samaccountname= + loginName + ) ; foreach( string p in properties ) search.PropertiesToLoad.Add(p); SearchResult result = search.FindOne(); if( result != null ) { foreach( string p in properties ) { ResultPropertyValueCollection collection = result.Properties[p]; for( int i = 0; i collection.Count; i++ ) Console.WriteLine(p + : + collection[i]); } } } catch( Exception ex ) { Console.WriteLine(ex.ToString()); } }
结果如下:
在前面的代码,在搜索Active Directory时,只搜索了 name,givenName,samaccountname,mail 这4个属性。然而,LDAP还支持更多的属性,们可以使用下面的代码查看更多的用户信息:
private static string AllProperties = @ homemdb distinguishedname countrycode cn lastlogoff mailnickname dscorepropagationdata msexchhomeservername msexchmailboxsecuritydescriptor msexchalobjectversion usncreated objectguid whenchanged memberof msexchuseraccountcontrol accountexpires displayname primarygroupid badpwdcount objectclass instancetype objectcategory samaccounttype whencreated lastlogon useraccountcontrol physicaldeliveryofficename samaccountname usercertificate givenname mail userparameters adspath homemta msexchmailboxguid pwdlastset logoncount codepage name usnchanged legacyexchangedn proxyaddresses department userprincipalname badpasswordtime objectsid sn mdbusedefaults telephonenumber showinaddressbook msexchpoliciesincluded textencodedoraddress lastlogontimestamp company
在ASP.NET中访问Active Directory
前面在一个控制台程序中演示了访问Active Directory的方法,通过示例们可以看到:在代码中,用Environment.UserName就可以得到当前用户的登录名。然而,如果是在ASP.NET程序中,访问Environment.UserName就很有可能得不到真正用户登录名。因为:Environment.UserName是使用WIN32API中的GetUserName获取线程相关的用户名,但ASP.NET运行在IIS中,线程相关的用户名就不一定是客户端的用户名了。不过,ASP.NET可以模拟用户方式运行,通过这种方式才可以得到正确的结果。关于 模拟 的话题在本文的后面部分有说明。
在ASP.NET中,为了能可靠的获取登录用户的登录名,们可以使用下面的代码:
/// summary /// 根据指定的HttpContext对象,获取登录名。 /// /summary /// param name= context /param /// returns /returns public static string GetUserLoginName(HttpContext context) { if( context == null ) return null; if( context.Request.IsAuthenticated == false ) return null; string userName = context.User.Identity.Name; // 此时userName的格式为:UserDomainName\LoginName // 们只需要后面的LoginName就可以了。 string[] array = userName.Split(new char[] { '\\' }, StringSplitOptions.RemoveEmptyEntries); if( array.Length == 2 ) return array[1]; return null; }
在ASP.NET中使用Windows身份认证时,IIS和WindowsAuthenticationModule已经做了许多验证用户的相关工作,虽然们可以使用前面的代码获取到用户的登录名,但用户的其它信息即需要们自己来获取。在实际使用Windows身份认证时,们要做的事:基本上就是从Active Directory中根据用户的登录名获取所需的各种信息。
比如:程序在运行时,还需要使用以下与用户相关的信息:
public sealed class UserInfo { public string GivenName; public string FullName; public string Email; }
那么,们可以使用这样的代码来获取所需的用户信息:
public static class UserHelper { /// summary /// 活动目录中的搜索路径,也可根据实际情况来修改这个值。 /// /summary public static string DirectoryPath = LDAP:// + GetDomainName(); /// summary /// 获取与指定HttpContext相关的用户信息 /// /summary /// param name= context /param /// returns /returns public static UserInfo GetCurrentUserInfo(HttpContext context) { string loginName = GetUserLoginName(context); if( string.IsNullOrEmpty(loginName) ) return null; return GetUserInfoByLoginName(loginName); } /// summary /// 根据指定的HttpContext对象,获取登录名。 /// /summary /// param name= context /param /// returns /returns public static string GetUserLoginName(HttpContext context) { if( context == null ) return null; if( context.Request.IsAuthenticated == false ) return null; string userName = context.User.Identity.Name; // 此时userName的格式为:UserDomainName\LoginName // 们只需要后面的LoginName就可以了。 string[] array = userName.Split(new char[] { '\\' }, StringSplitOptions.RemoveEmptyEntries); if( array.Length == 2 ) return array[1]; return null; } /// summary /// 根据登录名查询活动目录,获取用户信息。 /// /summary /// param name= loginName /param /// returns /returns public static UserInfo GetUserInfoByLoginName(string loginName) { if( string.IsNullOrEmpty(loginName) ) return null; // 下面的代码将根据登录名查询用户在AD中的信息。 // 为了提高性能,可以在此处增加一个缓存容器(Dictionary or Hashtable)。 try { DirectoryEntry entry = new DirectoryEntry(DirectoryPath); DirectorySearcher search = new DirectorySearcher(entry); search.Filter = (SAMAccountName= + loginName + ) ; search.PropertiesToLoad.Add( givenName ); search.PropertiesToLoad.Add( cn ); search.PropertiesToLoad.Add( mail ); // 如果还需要从AD中获取其它的用户信息,请参考ActiveDirectoryDEMO SearchResult result = search.FindOne(); if( result != null ) { UserInfo info = new UserInfo(); info.GivenName = result.Properties[ givenName ][0].ToString(); info.FullName = result.Properties[ cn ][0].ToString(); info.Email = result.Properties[ mail ][0].ToString(); return info; } } catch { // 如果需要记录异常,请在此处添加代码。 } return null; } private static string GetDomainName() { // 注意:这段代码需要在Windows XP及较新版本的操作系统中才能正常运行。 SelectQuery query = new SelectQuery( Win32_ComputerSystem ); using( ManagementObjectSearcher searcher = new ManagementObjectSearcher(query) ) { foreach( ManagementObject mo in searcher.Get() ) { if( (bool)mo[ partofdomain ] ) return mo[ domain ].ToString(); } } return null; } }
使用UserHelper的页面代码:
html xmlns= http://www.w3.org/1999/xhtml head title WindowsAuthentication DEMO - http://www.cnblogs.com/fish-li/ /title /head body % if( Request.IsAuthenticated ) { % 当前登录全名: %= Context.User.Identity.Name.HtmlEncode()% br / % var user = UserHelper.GetCurrentUserInfo(Context); % % if( user != null ) { % 用户短名: %= user.GivenName.HtmlEncode()% br / 用户全名: %= user.FullName.HtmlEncode() % br / 邮箱地址: %= user.Email.HtmlEncode() % % } % % } else { % 当前用户还未登录。 % } % /body /html
程序运行的效果如下:
另外,还可以从Active Directory查询一个叫做memberof的属性(它与Windows用户组无关),有时候可以用它区分用户,设计与权限相关的操作。
在设计数据持久化的表结构时,由于此时没有 用户表 ,那么们可以直接保存用户的登录名。剩下的开发工作就与Forms身份认证没有太多的差别了。
使用Active Directory验证用户身份
前面介绍了ASP.NET Windows身份认证,在这种方式下,IIS和WindowsAuthenticationModule为们实现了用户身份认证的过程。然而,有时可能由于各种原因,需要们以编程的方式使用Active Directory验证用户身份,比如:在WinForm程序,或者其它的验证逻辑。
们不仅可以从Active Directory中查询用户信息,也可以用它来实现验证用户身份,这样便可以实现自己的登录验证逻辑。
不管是如何使用Active Directory,们都需要使用DirectoryEntry和DirectorySearcher这二个对象。 DirectoryEntry还提供一个构造函数可让们输入用户名和密码:
// 摘要: // 初始化 System.DirectoryServices.DirectoryEntry 类的新实例。 // // 参数: // Password: // 在对客户端进行身份验证时使用的密码。DirectoryEntry.Password 属性初始化为该值。 // // username: // 在对客户端进行身份验证时使用的用户名。DirectoryEntry.Username 属性初始化为该值。 // // Path: // 此 DirectoryEntry 的路径。DirectoryEntry.Path 属性初始化为该值。 public DirectoryEntry(string path, string username, string password);
要实现自己的登录检查,就需要使用这个构造函数。以下是写用WinForm写的一个登录检查的示例:
private void btnLogin_Click(object sender, EventArgs e) { if( txtUsername.Text.Length == 0 || txtPassword.Text.Length == 0 ) { MessageBox.Show( 用户名或者密码不能为空。 , this.Text, MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } string ldapPath = LDAP:// + GetDomainName(); string domainAndUsername = Environment.UserDomainName + \\ + txtUsername.Text; DirectoryEntry entry = new DirectoryEntry(ldapPath, domainAndUsername, txtPassword.Text); DirectorySearcher search = new DirectorySearcher(entry); try { SearchResult result = search.FindOne(); MessageBox.Show( 登录成功。 , this.Text, MessageBoxButtons.OK, MessageBoxIcon.Information); } catch( Exception ex ) { // 如果用户名或者密码不正确,也会抛出异常。 MessageBox.Show(ex.Message, this.Text, MessageBoxButtons.OK, MessageBoxIcon.Stop); } }
程序运行的效果如下:
安全上下文与用户模拟
在ASP.NET Windows身份认证环境中,与用户相关的安全上下文对象保存在HttpContext.User属性中,是一个类型为WindowsPrincipal的对象,们还可以访问HttpContext.User.Identity来获取经过身份认证的用户标识,它是一个WindowsIdentity类型的对象。
在.NET Framework中,们可以通过WindowsIdentity.GetCurrent()获取与当前线程相关的WindowsIdentity对象,这种方法获取的是当前运行的Win32线程的安全上下文标识。由于ASP.NET运行在IIS进程中,因此ASP.NET线程的安全标识其实是从IIS的进程中继承的,所以此时用二种方法得到的WindowsIdentity对象其实是不同的。
在Windows操作系统中,许多权限检查都是基于Win32线程的安全上下文标识,于是前面所说的二种WindowsIdentity对象会造成编程模型的不一致问题,为了解决这个问题,ASP.NET提供了 模拟 功能,允许线程以特定的Windows帐户的安全上下文来访问资源。
为了能更好的理解模拟的功能,准备了一个示例(ShowWindowsIdentity.ashx):
public class ShowWindowsIdentity : IHttpHandler { public void ProcessRequest (HttpContext context) { // 要观察【模拟】的影响, // 可以启用,禁止web.config中的设置: identity impersonate= true / context.Response.ContentType = text/plain ; context.Response.Write(Environment.UserDomainName + \\ + Environment.UserName + \r\n WindowsPrincipal winPrincipal = (WindowsPrincipal)HttpContext.Current.User; context.Response.Write(string.Format( HttpContext.Current.User.Identity: {0}, {1}\r\n , winPrincipal.Identity.AuthenticationType, winPrincipal.Identity.Name)); WindowsPrincipal winPrincipal2 = (WindowsPrincipal)Thread.CurrentPrincipal; context.Response.Write(string.Format( Thread.CurrentPrincipal.Identity: {0}, {1}\r\n , winPrincipal2.Identity.AuthenticationType, winPrincipal2.Identity.Name)); WindowsIdentity winId = WindowsIdentity.GetCurrent(); context.Response.Write(string.Format( WindowsIdentity.GetCurrent(): {0}, {1} , winId.AuthenticationType, winId.Name)); }
首先,在web.config中设置:
authentication mode= Windows /
注意:要把网站部署在IIS中,否则看不出效果。
此时,访问ShowWindowsIdentity.ashx,将看到如下图所示的结果:
现在修改一下web.config中设置:(注意:后面加了一句配置)
authentication mode= Windows / identity impersonate= true /
此时,访问ShowWindowsIdentity.ashx,将看到如下图所示的结果:
说明:
1. FISH-SRV2003是计算机名。它在一个没有域的环境中。
2. fish-li是一个Windows帐号的登录名。
3. 网站部署在IIS6中,进程以NETWORK SERVICE帐号运行。
4. 打开网页时,输入的用户名是fish-li
前面二张图片的差异之处其实也就是ASP.NET的 模拟 所发挥的功能。
关于模拟,想说四点:
1. 在ASP.NET中,们应该访问HttpContext.User.Identity获取当前用户标识,那么就不存在问题(此时可以不需要模拟),例如FileAuthorizationModule就是这样处理的。
2. 模拟只是在ASP.NET应用程序访问Windows系统资源时需要应用Windows的安全检查功能才会有用。
3. Forms身份认证也能配置模拟功能,但只能模拟一个Windows帐户。
4. 绝大多数情况下是不需要模拟的。
在IIS中配置Windows身份认证
与使用Forms身份认证的程序不同,使用Windows身份认证的程序需要额外的配置步骤。这个小节将主要介绍在IIS中配置Windows身份认证,将常用的IIS6和IIS7.5为例分别介绍这些配置。
IIS6的配置 请参考下图:
IIS7.5的配置 请参考下图:
注意:Windows身份认证是需要安装的,方法请参考下图:
关于浏览器的登录对话框问题
当们用浏览器访问一个使用Windows身份认证的网站时,浏览器都会弹出一个对话框(左IE,右Safari):
此时,要求们输入Windows的登录帐号,然后交给IIS验证身份。
首次弹出这个对话框很正常:因为程序要验证用户的身份。
然而,每次关闭浏览器下次重新打开页面时,又会出现此对话框,此时感觉就很不方便了。
虽然有些浏览器能记住用户名和密码,但发现FireFox,Opera,Chrome仍然会弹出这个对话框,等待们点击确定,只有Safari才不会打扰用户直接打开网页。 IE的那个 记住密码 复选框完全是个摆设,它根本不会记住密码!
因此,所试过的所有浏览器中,只有Safari是最人性化的。
虽然在默认情况下,虽然IE不会记住密码,每次都需要再次输入。
不过,IE却可以支持不提示用户输入登录帐号而直接打开网页, 此时IE将使用用户的当前Windows登录帐号传递给IIS验证身份。
要让IE打开一个Windows身份认证的网站不提示登录对话框,必须满足以下条件:
1. 必须在 IIS 的 Web 站点属性中启用 Windows 集成身份验证。
2. 客户端和Web服务器都必须在基于Microsoft Windows的同一个域内。
3. Internet Explorer 必须把所请求的 URL 视为 Intranet(本地)。
4. Internet Explorer 的 Intranet 区域的安全性设置必须设为 只在 Intranet 区域自动登录 。
5. 请求Web页的用户必须具有访问该Web页以及该Web页中引用的所有对象的适当的文件系统(NTFS)权限。
6. 用户必须用域帐号登录到Windows 。
在这几个条件中,如果网站是在一个Windows域中运行,除了第3条可能不满足外,其它条件应该都容易满足(第4条是默认值)。因此,要让IE不提示输入登录帐号,只要确保第3条满足就可以了。下面的图片演示了如何完成这个配置:(注意:配置方法也适合用域名访问的情况)
另外,除了在IE中设置Intranet外,还可以在访问网站时,用计算机名代替IP地址或者域名,那么IE始终认为是在访问Intranet内的网站,此时也不会弹出登录对话框。
在此,想再啰嗦三句:
1. IE在集成Windows身份认证时,虽然不提示登录对话框,但是不表示不安全,它会自动传递登录凭据。
2. 这种行为只有IE才能支持。(其它的浏览器只是会记住密码,在实现上其实是不一样的。)
3. 集成Windows身份认证,也只适合在Intranet的环境中使用。
在客户端代码中访问Windows身份认证的页面
在上篇中,演示了如何用代码访问一个使用Forms身份认证的网站中的受限页面,方法是使用CookieContainer对象接收服务端生的登录Cookie。然而,在Windows身份认证的网站中,身份验证的过程发生在IIS中,而且根本不使用Cookie保存登录状态,而是需要在请求时发送必要的身份验证信息。
在使用代码做为客户端访问Web服务器时,们仍然需要使用HttpWebRequest对象。为了能让HttpWebRequest在访问IIS时发送必要的身份验证信息,HttpWebRequest提供二个属性都可以完成这个功能:
// 获取或设置请求的身份验证信息。 // // 返回结果: // 包含与该请求关联的身份验证凭据的 System.Net.ICredentials。默认为 null。 public override ICredentials Credentials { get; set; } // 获取或设置一个 System.Boolean 值,该值控制默认凭据是否随请求一起发送。 // // 返回结果: // 如果使用默认凭据,则为 true;否则为 false。默认值为 false。 public override bool UseDefaultCredentials { get; set; }
下面是准备的完整的示例代码(注意代码中的注释):
static void Main(string[] args) { try { // 请把WindowsAuthWebSite1这个网站部署在IIS中, // 开启Windows认证方式,并禁止匿名用户访问。 // 然后修改下面的访问地址。 HttpWebRequest request = (HttpWebRequest)WebRequest.Create( http://localhost:33445/Default.aspx ); // 下面三行代码,启用任意一行都是可以的。 request.UseDefaultCredentials = true; //request.Credentials = CredentialCache.DefaultCredentials; //request.Credentials = CredentialCache.DefaultNetworkCredentials; // 如果上面的三行代码全被注释了,那么将会看到401的异常信息。 using( HttpWebResponse response = (HttpWebResponse)request.GetResponse() ) { using( StreamReader sr = new StreamReader(response.GetResponseStream()) ) { Console.WriteLine(sr.ReadToEnd()); } } } catch( WebException wex ) { Console.WriteLine( ===================================== ); Console.WriteLine( 异常发生了。 ); Console.WriteLine( ===================================== ); Console.WriteLine(wex.Message); } }
其实关键部分还是设置UseDefaultCredentials或者Credentials,代码中的三种方法是有效的。
这三种方法的差别:
1. Credentials = CredentialCache.DefaultCredentials; 表示在发送请求会带上当前用户的身份验证凭据。
2. UseDefaultCredentials = true; 此方法在内部会调用前面的方法,因此与前面的方法是一样的。
3. Credentials = CredentialCache.DefaultNetworkCredentials; 是在.NET 2.0中引用的新方法。
关于DefaultCredentials和DefaultNetworkCredentials的更多差别,请看整理的表格:
1. NetworkCredential实现了ICredentials接口,
2. SystemNetworkCredential继承自NetworkCredential。
在结束这篇之前,想应该感谢新蛋。
在新蛋的网络环境中,让学会了使用Windows身份认证。
除了感谢之外,现在还特别怀念 fl45 这个登录名......
点击此处下载示例代码