PunCha

导航

用issnode+IIS来托管NodeJs Server之三:加入Windows集成验证功能

这几篇文章是一个系列的:

  1.  用issnode+IIS来托管NodeJs Server之一:安装篇
  2.  用issnode+IIS来托管NodeJs Server之二:移植
  3.  用issnode+IIS来托管NodeJs Server之三:加入Windows集成验证功能
  4.  用issnode+IIS来托管NodeJs Server之四:支持Debug


今天花了一天时间,解决了NTLM身份认证的问题,现在把整个过程写下来:

1. 首先要启用网站的Windows认证,方法是:在IIS里面,选择你的站点iisCER,然后点击Authetication进行配置,你应该能看到Windows Authentication的组件,先Enable,然后再Advanced Settings里面勾选Enable Kernel-mode authentication。不然的话,无法正常认证,这个也不知道是为什么。接着,再禁用Anonymous authentication。假如你没有看到Windows Authentication组件,就需要重启下电脑。




这样一来,再访问nodeJs站点就会提示认证了(我测试下来,如果你在域里面,IE可以自动完成认证,而不必输入用户名密码,Firefox和Chrome貌似也行)。

对于配置IIS,可以参考这篇博文,作者写的很好,对我配置IIS和后面的LDAP代码帮助蛮大的:http://www.cnblogs.com/fish-li/archive/2012/05/07/2486840.html


2. IIS默认不会把身份认证的信息传给Nodejs的,所以需要手动配置一下,具体参见:https://github.com/tjanczuk/iisnode/issues/87。其实最终修改的是web.config文件(你直接添加就行了):

<iisnode promoteServerVars="AUTH_USER,AUTH_TYPE" />

3. 经过以上2步,nodeJs端就可以获得用户名了:
exports.sendReport = function (req, res) {
    var productId = req.query.productId;
    var productName = req.query.productName;
    var title = 'Send CER report';
    var page = 'page_send_report';
    var page_params = {
        title: title, header: productName,
        productId: productId, productName: productName,
        domain_account: req.headers["x-iisnode-auth_user"]
    };
用户的每一个Web请求,都会带有一个x-iisnode-auth_user头信息,里面就是用户的域账号,例如ads\fengx。用"\"分隔一下就能拿到域和用户名了。

5. 对于我来说,仅仅拿到用户名是不够的,我需要知道用户的Email地址,这个可以通过ADSI 和 LDAP协议拿到。相关代码:
using System;
using System.Diagnostics;
using System.DirectoryServices;

namespace GetAdEmailAddress
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(getEmailAddress(@"ads\someone"));
        }

        static string getEmailAddress(string domainAccount)
        {
            String[] domain_username = domainAccount.Split('\\');
            Debug.Assert(domain_username.Length == 2);

            String domain = domain_username[0];
            String user = domain_username[1];

            DirectorySearcher search = null;

            if (true)
            {
                search = new DirectorySearcher("(&(objectClass=user)(anr=" + user + "))");
            }
            else
            {
                // If we are not in a domain, we need authenticate outselves before access LDAP
                //
                DirectoryEntry entry = new DirectoryEntry(@"LDAP://ads.autodesk.com", "fengx", "test1234", AuthenticationTypes.Secure);
                search = new DirectorySearcher(entry, "(&(objectClass=user)(anr=" + user + "))");
            }

            search.PropertiesToLoad.Add("mail");
            SearchResult result = search.FindOne();
            return (result.Properties["mail"][0].ToString());

        }
    }
}
上面的代码需要引用System.DirectoryServices.dll。上面代码里面,我假如了一个if (true),假如运行这个程序的机器是在域里面的,那么直接查询就可以了,否则的话,就需要用用户名密码验证下。但是这样就有一个问题,我们的Server不在域里面!而且把用户名密码暴露在C#程序里面很危险!咋办!

5. 我们整理一下思路,首先我们的代码是iisnode模块执行的,所以我们的代码和iisnode模块在同一个进程空间(或者是父子进程的关系),而iisnode是IIS的一个模块,当然和IIS在一个进程空间。那么假如IIS已经对用户进行了身份验证(即拥有了用户的credentials),那么假如由nodeJs spawn执行上面那个获取email信息的小程序,行不行呢?哈哈,竟然可以! 相关代码如下:
app.get("/test", function (req, res) {

    var exec = require('child_process').exec;
    var getEmailAddress = exec('GetAdEmailAddress.exe', 
        {
            cwd: "./app",
            env: process.env,
        },
        function (error, stdout, stderr) {
            if (error)
                res.send(400, "failed to launch");
            else
                res.send(200,stdout);
        });
});

我没有是用spawn,而是用exec来创建进程, 原因只是exec获取stdout很方便,spawn很麻烦,但是对于stdout比较多的话,一定要用spawn,否则exec的stdout缓冲区会溢出。

6. 到此为止,大功告成!贴一下,最终的Web.Config文件(那个impersonate最好是true,但是其实false也没问题):

<configuration>
    <system.webServer>

        <handlers>
            <add name="iisnode" path="app.js" verb="*" modules="iisnode" />
        </handlers>

        <rewrite>
            <rules>
                <rule name="mysite">
                    <match url="/*" />
                    <action type="Rewrite" url="app.js" />
                </rule>
            </rules>
        </rewrite>

        <defaultDocument>
            <files>
                <add value="app.js" />
            </files>
        </defaultDocument>

        <iisnode promoteServerVars="AUTH_USER,AUTH_TYPE" />
    </system.webServer>
    <system.web>
        <identity impersonate="false" />
    </system.web>
</configuration>

7. 我开发过程中,还碰到一个严重的问题就是,我代码改了,IIS重启了,但是返回的竟然是我没有改代码前的页面,没办法,只有杀node.exe进程,让iisnode重启node.exe,这个具体是什么原因不知道,应该是iisnode的一个bug。

这篇文章是介绍IIS各种认证的,还没来得及看,先收藏下:http://www.cnblogs.com/chnking/archive/2007/11/20/965553.html


-------------------------------------------------

1. 假如开启windows认证有问题的话,可以看看这3篇博文:

http://support.microsoft.com/kb/215383/zh-cn

http://statsoft.blog.163.com/blog/static/276531322010414593971

http://blogs.iis.net/nitashav/archive/2010/03/12/iis6-0-ui-vs-iis7-x-ui-series-integrated-windows-authentication.aspx

相关代码执行:
C:\Windows\System32\inetsrv>appcmd list config /section:windowsAuthentication  
C:\Windows\System32\inetsrv>appcmd set config /section:windowsAuthentication /enabled:true  

-------------------------------------------------

附博文:http://www.cnblogs.com/fish-li/archive/2012/04/15/2450571.html,免得被博主删了。。。

上篇博客我谈到了一些关于ASP.NET Forms身份认证方面的话题,这次的博客将主要介绍ASP.NET Windows身份认证。

Forms身份认证虽然使用广泛,不过,如果是在 Windows Active Directory 的环境中使用ASP.NET,那么使用Windows身份认证也会比较方便。方便性表现为:我们不用再设计登录页面,不用编写登录验证逻辑。而且使用Windows身份认证会有更好的安全保障。

认识ASP.NET Windows身份认证

要使用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的更多差别,请看我整理的表格:

Credentials属性 申明类型 实例类型 .NET支持版本
DefaultCredentials ICredentials SystemNetworkCredential 从1.0开始
DefaultNetworkCredentials NetworkCredential SystemNetworkCredential 从2.0开始

三个类型的继承关系:
1. NetworkCredential实现了ICredentials接口,
2. SystemNetworkCredential继承自NetworkCredential。

在结束这篇博客之前,我想我应该感谢新蛋。
在新蛋的网络环境中,让我学会了使用Windows身份认证。
除了感谢之外,我现在还特别怀念 fl45 这个登录名......

点击此处下载示例代码


posted on 2013-06-08 16:50  PunCha  阅读(368)  评论(0编辑  收藏  举报