[转载]细说ASP.NET Windows身份认证

2019-12-14 03:09栏目:bob体育平台
TAG:

如何获取当前系统用户对文件/文件夹的操作权限?

细说ASP.NET Windows身份认证

阅读目录

  • 开始
  • 认识ASP.NET Windows身份认证
  • 访问 Active Directory
  • 在ASP.NET中访问Active Directory
  • 使用Active Directory验证用户身份
  • 安全上下文与用户模拟
  • 在IIS中配置Windows身份认证
  • 关于浏览器的登录对话框问题
  • 在客户端代码中访问Windows身份认证的页面

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

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

回到顶部

 1.获取安全信息DirectorySecurity

DirectorySecurity fileAcl = Directory.GetAccessControl(folder);

通过Directory.GetAccessControl获取文件夹的权限/安全信息

详细介绍,可参考MSDN官方文档)

对文件/文件夹权限的详细操作,可参考一篇博客C#文件夹权限操作

认识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这二个与用户身份相关的对象。

回到顶部

2. 获取文件夹访问权限列表FileSystemAccessRule

var rules = fileAcl.GetAccessRules(true, true, typeof(System.Security.Principal.NTAccount)).OfType<FileSystemAccessRule>().ToList();

GetAccessRules()方法返回的是AuthorizationRule集合,此处只需要获取文件权限。

FileSystemAccessRule.aspx)继承自AuthorizationRule,并新增俩个属性

  • AccessControlType -- 枚举 Allow/Deny
  • FileSystemRights -- 对文件的访问权限详细信息(读/写等),可见下面列表: 

图片 1图片 2

 1   /// <summary>定义要创建访问和审核规则时使用的访问权限。</summary>
 2   [Flags]
 3   public enum FileSystemRights
 4   {
 5     ReadData = 1,
 6     ListDirectory = ReadData, // 0x00000001
 7     WriteData = 2,
 8     CreateFiles = WriteData, // 0x00000002
 9     AppendData = 4,
10     CreateDirectories = AppendData, // 0x00000004
11     ReadExtendedAttributes = 8,
12     WriteExtendedAttributes = 16, // 0x00000010
13     ExecuteFile = 32, // 0x00000020
14     Traverse = ExecuteFile, // 0x00000020
15     DeleteSubdirectoriesAndFiles = 64, // 0x00000040
16     ReadAttributes = 128, // 0x00000080
17     WriteAttributes = 256, // 0x00000100
18     Delete = 65536, // 0x00010000
19     ReadPermissions = 131072, // 0x00020000
20     ChangePermissions = 262144, // 0x00040000
21     TakeOwnership = 524288, // 0x00080000
22     Synchronize = 1048576, // 0x00100000
23     FullControl = Synchronize | TakeOwnership | ChangePermissions | ReadPermissions | Delete | WriteAttributes | ReadAttributes | DeleteSubdirectoriesAndFiles | Traverse | WriteExtendedAttributes | ReadExtendedAttributes | CreateDirectories | CreateFiles | ListDirectory, // 0x001F01FF
24     Read = ReadPermissions | ReadAttributes | ReadExtendedAttributes | ListDirectory, // 0x00020089
25     ReadAndExecute = Read | Traverse, // 0x000200A9
26     Write = WriteAttributes | WriteExtendedAttributes | CreateDirectories | CreateFiles, // 0x00000116
27     Modify = Write | ReadAndExecute | Delete, // 0x000301BF
28   }

View Code

 因为AuthorizationRule中,IdentityReference对应权限的用户/用户组标识,格式为:"MYDOMAINMyAccount"

所以,如通过当前系统用户名与IdentityReference匹配,即可获取FileSystemAccessRule权限。如何获取用户名,见下一段落

访问 Active Directory

我们通常使用LDAP协议来访问Active Directory, 在.net framework中提供了DirectoryEntry和DirectorySearcher这二个类型让我们可以方便地从托管代码中访问 Active Directory 域服务。

如果我们要在"test.corp”这个域中搜索某个用户信息,我们可以使用下面的语句构造一个DirectoryEntry对象:

DirectoryEntry entry = new DirectoryEntry("LDAP://test.corp");

在这段代码中,我采用硬编码的方式把域名写进了代码。 我们如何知道当前电脑所使用的是哪个域名呢? 答案是:查看“我的电脑”的属性对话框:

图片 3

注意:这个域名不一定与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());
    }
}

结果如下:

图片 4

在前面的代码,我在搜索Active Directory时,只搜索了"name,givenName,samaccountname,mail"这4个属性。 然而,LDAP还支持更多的属性,我们可以使用下面的代码查看更多的用户信息:图片 5;)

        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
";

回到顶部

3. 获取当前系统用户名/用户组

通过 System.Environment.UserDomainName 和 System.Environment.UserName 取得当前用户名

对当前系统用户名/用户组的其它操作,可参考

  • C# 管理 Windows 本地用户组
  • C# 获取 Windows 用户组成员

因此,将Path.Combine(Environment.UserDomainName, Environment.UserName)与IdentityReference.Value比较,获取当前用户对文件夹的权限信息

详细实现如下:

 1     /// <summary>
 2     /// 检查当前用户是否拥有此文件夹的操作权限
 3     /// </summary>
 4     /// <param name="folder"></param>
 5     /// <returns></returns>
 6     public static bool HasOperationPermission(string folder)
 7     {
 8         var currentUserIdentity = Path.Combine(Environment.UserDomainName, Environment.UserName);
 9 
10         DirectorySecurity fileAcl = Directory.GetAccessControl(folder);
11         var userAccessRules = fileAcl.GetAccessRules(true, true, typeof(System.Security.Principal.NTAccount)).OfType<FileSystemAccessRule>().Where(i=>i.IdentityReference.Value==currentUserIdentity).ToList();
12 
13         return userAccessRules.Any(i => i.AccessControlType == AccessControlType.Deny);
14     }

 

在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的格式为:UserDomainNameLoginName
    // 我们只需要后面的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;
}

那么,我们可以使用这样的代码来获取所需的用户信息:图片 6;)

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的格式为:UserDomainNameLoginName
        // 我们只需要后面的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>

程序运行的效果如下:

图片 7

另外,还可以从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);
    }
}

程序运行的效果如下:

图片 8

回到顶部

版权声明:本文由bob体育app发布于bob体育平台,转载请注明出处:[转载]细说ASP.NET Windows身份认证