.NET框架中基于角色的安全性
 
2009-02-03 作者:sPhinX 来源:yesky
 

概述

在过去的相当长一段时间内,计算机及信息犯罪的比例正在逐渐升高。美国联邦调查局的计算机安全组织在2001年的研究调查中发现85%企业的企业安全受到侵害。在对这些企业进行调查之后提出的财物损失报告中指出,合计损失为3亿7千7百万美元,比起2000年的2亿6千5百万美金增加了42%。由此可清楚的看出,计算机及信息犯罪的发生次数越来越频繁,其所造成的损失也越来越大,另外,犯罪的手段也越来越丰富,令企业安全人员防不胜防。因此企业必须有所行动来保护有价值的信息资产。自然而然的,安全性在现在的程序开发中越来越成为一个不可忽视的问题。

传统的安全模型将安全性建立在用户以及用户组的机制上来提供隔离和访问控制。这就意味着用户要么可以运行全部代码,要么都不能运行。而这正是现在大多数操作系统采取的安全模型,即使现在看来这种机制也是很有效的,但是深入思考之后我们可以发现这种机制存在的假设是所有的代码都具有相同的信任程度。当所有的代码都是来自你或者你的系统管理员,那么这种假设是行之有效的。但是现在大多数计算机都连上了Internet之后,这种"都行"或者"都不行"的方式就不那么好了。.NET框架提供了全面的安全系统,足以应付现在已有的大多数安全性问题。在.NET框架中提供了与传统模型相似的,但却是由开发人员自定义的安全模型,称为基于角色的安全性(Role-Based Security)。基于角色的安全性最重要的概念就是授权(Principals)和标识(Identity)。

基于角色的安全性

简单的说,程序安全性的目的就是防止不怀好意的人或者程序不能做管理员和开发人员不允许做的事情。在前面提到的传统的安全机制着眼于控制用户的权限,通过验证用户的身份标识来限制用户的操作,从而可以控制特定的用户对资源的访问,在过去很长的一段时间里,Windows和UNIX这两个最成功的操作系统都采取了这种安全机制。在讨论.NET的安全编程之前,我们将先来看看.NET平台提供的安全模型,只有深入理解安全模型,我们才能更加有效的利用平台给我们提供的更好的安全性保证(以下讨论的操作系统以Windows 2000及Windows XP为主)。

.NET的安全模型在系统的安全模型的上层,并且与一些服务器程序的安全特性结合的很好(当然,目前这些产品还仅限于微软自己的产品,比如SQL Server和Internet Information Services (IIS))。正因为.NET与操作系统的层次不同,所以.NET程序的安全性就取决于这样几个因素:.NET安全性是如何配置的,程序组件是怎样编写的,以及一些由Windows,网络设置或者其他程序设定的安全特性。

下面这幅图说明了.NET安全模型是如何在Windows的安全子系统上工作的。管理员使用管理控制台snap-ins设置用户帐号并制定安全策略。同时,管理员也负责管理.NET安全配置。当用户登陆操作系统并运行.NET托管程序,CLR将验证用户并允许程序进行某些动作,接着将这些操作传递给操作系统的安全监视程序。

不过有一个需要特别注意的问题,就是无论你怎样使用.NET的安全,资源仍然是处于操作系统级的保护之下。对于受特殊保护的资源,.NET平台的安全权限是无能为力的(其实这一点也正好符合.NET与操作系统的层次关系)。

下面我们就来详细的看看基于角色的安全性中的几个概念,以及这几个概念在.NET中是如何运用的。

验证(authentication)指的是确定用户身份的过程,而授权(authorization)指的是经过上面的过程之后给予用户访问特定资源的权限,说明白一点,验证就是知道"你是谁",而授权则是"让你可以做什么"。.NET为实现这两个过程提供了Principal和Identity对象,其中,基于角色的安全性基础建立在Principal对象之上,该对象封装了当前用户的信息,既包含用户身份,也包含他所扮演的角色;用户身份用Identity对象来指明,Identity对象中不仅包含指定的用户身份信息(用户名称或账号),还包括了"如何验证这一身份"的方法。

Identity对象

Identity对象是实现了IIdentity接口的类的实例。IIdentity接口包括三个只读属性:

string AuthenticationType {get;} 获取所使用的身份验证的类型
 bool IsAuthenticated {get;} 获取布尔值,该值指出登陆用户是否经过验证
 string Name {get;} 获取当前用户的名称

.NET中实现了接口的有以下四个类:

1、GenericIdentity 用来表示一般性的用户,可以用于自定义登陆验证的情况。

2、WindowsIdentity 用来表示登陆Windows系统成功的普通Windows用户。

3、FormsIdentity 用来表示ASP.NET应用程序中使用Forms身份验证的用户。

4、PassportIdentity 用来表示在使用Passport的应用程序中的用户。不过要注意必须要安装了Passport SDK才能使用这个类。

因为在当前的具体开发中使用得最多的是前三个,而FormsIdentity类将在后文专门讲到,所以下面我们将详细讨论前两个类

GenericIdentity类

GenericIdentity类其实相当简单,它并不与任何特定的验证协议相关联。因此,它往往被用在采用了自定义登陆机制的场合。比如一个程序可以自己提示用户输入用户名和密码,然后到自定义的用户数据库中去查询。如果用户名和密码有效,那么程序就会创建一个基于数据库中的匹配记录的principal和(对应的)identity对象。

GenericIdentity类除了三个IIdentity接口定义的属性之外没有更多的东西了。不过,GenericIdentity类提供了两个构造函数。一个构造函数接受一个字符串参数,该参数指定的是用户名;另一个构造函数接受两个参数:第一个是用户名字符串,第二个是给定的验证类型字符串。

public GenericIdentity(string name);
 public GenericIdentity(string name, string type);

现在我们不过多地讲述使用GenericIdentity类的细节问题,在后面我们将会看到在一个实际的程序当中是如何使用GenericIdentity对象的。

WindowsIdentity类

作为实现了IIdentity接口的派生类,WindowsIdentity类主要用于表示登陆Windows成功的用户。下面我们依次来看看WindowsIdentity类的构造函数,属性和方法。

在构造函数中会用到IntPtr类型的参数,我们先来看看这种数据类型,IntPtr类型通常用来表示与平台相关的数据类型,比如一个内存指针或者是一个句柄。在我们使用的情况下,IntPtr参数通常用来代表一个Win32句柄,而这个句柄指向的是一个32位的用户的帐号标记(account token)。这个标记一般是通过调用非托管的Win32 API获得的。

public WindowsIdentity(IntPtr userToken);
 public WindowsIdentity(IntPtr userToken, string authType);
 public WindowsIdentity(IntPtr userToken, string authType, WindowsAccountType acctType);
 public WindowsIdentity(IntPtr userToken, string authType, WindowsAccountType acctType, bool isAuthenticated);

每一个构造函数都带有相同的IntPtr参数,后面跟着一些带有其他信息的参数:验证类型,Windows帐号类型以及验证状态。要注意WindowsAccountType参数必须要使用下列枚举值之一:Anonymous,Guest,Normal,System。

理所当然的,WindowsIdentity类也具有IIdentity接口的三个只读属性: AuthenticationType,IsAuthenticated和Name。另外,WindowsIdentity类还有自身特有的属性:IsAnonymous,IsGuest和IsSystem,有了这三个属性可以更好的确定用户帐号。

接着再来看看WindowsIdentity类的方法。除了继承于Object类的方法之外,WindowsIdentity类还有这样三个方法:GetAnonymous,GetCurrent和Impersonate。

1、GetAnonymous方法返回的是一个表示匿名用户的WindowsIdentity对象。

2、GetCurrent方法返回的是一个表示当前用户的WindowsIdentity对象。

3、Impersonate方法可以让你的代码临时模拟出一个用户。

GetAnonymous和GetCurrent方法都返回一个WindowsIdentity对象,使用也很简单,我们需要注意的是Impersonate方法,该方法有两个版本:实例版本(instance version)和静态版本(static version)。实例版本的方法不带参数,返回一个基于被调用WindowsIdentity对象的WindowsImpersonationContext对象(WindowsImpersonationContext类表示模拟操作之前的 Windows 用户);静态版本则需要一个IntPtr参数。这种模拟操作对于服务器程序来说是很有用的,它可以降低客户端访问服务器所用用户帐号的权限,从而在一定程度上提高了安全性。下面是上述方法的具体语法:

public static WindowsIdentity GetAnonymous();
每一个构造函数都带有相同的IntPtr参数,后面跟着一些带有其他信息的参数:验证类型,Windows帐号类型以及验证状态。要注意WindowsAccountType参数必须要使用下列枚举值之一:Anonymous,Guest,Normal,System。

理所当然的,WindowsIdentity类也具有IIdentity接口的三个只读属性: AuthenticationType,IsAuthenticated和Name。另外,WindowsIdentity类还有自身特有的属性:IsAnonymous,IsGuest和IsSystem,有了这三个属性可以更好的确定用户帐号。

接着再来看看WindowsIdentity类的方法。除了继承于Object类的方法之外,WindowsIdentity类还有这样三个方法:GetAnonymous,GetCurrent和Impersonate。

1、GetAnonymous方法返回的是一个表示匿名用户的WindowsIdentity对象。

2、GetCurrent方法返回的是一个表示当前用户的WindowsIdentity对象。

3、Impersonate方法可以让你的代码临时模拟出一个用户。

GetAnonymous和GetCurrent方法都返回一个WindowsIdentity对象,使用也很简单,我们需要注意的是Impersonate方法,该方法有两个版本:实例版本(instance version)和静态版本(static version)。实例版本的方法不带参数,返回一个基于被调用WindowsIdentity对象的WindowsImpersonationContext对象(WindowsImpersonationContext类表示模拟操作之前的 Windows 用户);静态版本则需要一个IntPtr参数。这种模拟操作对于服务器程序来说是很有用的,它可以降低客户端访问服务器所用用户帐号的权限,从而在一定程度上提高了安全性。下面是上述方法的具体语法:

public static WindowsIdentity GetAnonymous();
 public static WindowsIdentity GetCurrent();
 public virtual WindowsImpersonationContext Impersonate();
 public static WindowsImpersonationContext Impersonate(IntPtr userToken);

Principal对象

Principal对象是实现了IPrincipal接口的类的实例,这些对象用来表示用户,并且包括了用户的身份信息。System.Security.Principal命名空间包括了几种类型的Principal类,这些类中封装了程序代码运行的的安全环境(security context)。我们在后面将会看到对用户名和角色进行检查以确定根据用户身份和角色资格是否可以让用户执行某些特定操作的示例代码。

对于每一个线程来说都与一个principal对象相关联。这个principal对象包括了表示运行当前线程的用户的identity对象。我们可以利用Thread类的静态属性CurrentPrincipal来获得这个principal对象。

下面我们来看看IPrincipal接口,该接口只有一个Identity公共属性和IsInRole公共方法:

1、Identity属性指向一个与principal 对象关联的IIdentity对象。

2、IsInRole方法需要一个字符串参数,该字符串是一个角色的名称,并且返回布尔值,指出principal对象是否属于指定的角色。

由于实际开发的需要,我们更多接触到的是WindowsPrincipal类,下面将详细讨论WindowsPrincipal类,相对而言,GenericPrincipal类就要简略一些了。

GenericPrincipal类

GenericPrincipal类用来表示一个通过自定义验证的用户,通常与GenericIdentity类一起使用。下面是一段简单的程序,说明了这两个类如何使用:

//创建一个GenericIdentity对象
IIdentity myGenericIdentity = new GenericIdentity(strUserName, "MyAuthenticationType");

//创建一个GenericPrincipal对象
String[] roles = null;
GenericPrincipal myGenericPrincipal = new GenericPrincipal(myGenericIdentity, roles);

//将创建的GenericPrincipal对象附加到当前线程上
Thread.CurrentPrincipal = myGenericPrincipal;

注意在上面的例子中,我们可以把MyAuthenticationType的验证类型换成熟知的Kerberos身份验证或者NTLM身份验证。

下面是验证的过程:

//取得当前线程的principal对象
IPrincipal principal = Thread.CurrentPrincipal;

if (!principal.Identity.Name.Equals("TrustedUser"))
{
throw new SecurityException(
strUserName + " NOT PERMITTED to proceed.\n");
}
Console.WriteLine(
strUserName + " is PERMITTED to proceed.\n");

WindowsPrincipal类

WindowsPrincipal类作为我们在开发中最常遇到的实现了IPrincipal接口的类,构造函数相当简单:
public WindowsPrincipal(WindowsIdentity ntIdentity);

下面的代码说明了如何创建一个WindowsPrincipal对象:

WindowsIdentity wi = WindowsIdentity.GetCurrent();
WindowsPrincipal wp = new WindowsPrincipal(wi);

WindowsPrincipal类中需要注意的是下面这三个重载的IsInRole方法:

public virtual bool IsInRole(int);

第1个重载函数接受一个整型参数,该参数表示用户组对应的RID(RID也就是与域相关联的下级凭证(domain-relative subauthority)ID)。RID值定义在Platform SDK的头文件Winnt.h中,Winnt.h中包括一些常见的用户和组,比如DOMAIN_USER_RID_ADMIN、 DOMAIN_USER_RID_GUEST、DOMAIN_GROUP_RID_ADMINS、DOMAIN_GROUP_RID_USERS和DOMAIN_GROUP_RID_GUESTS等等,可以在...\Microsoft Visual Studio .NET\Vc7\PlatformSDK\Include文件夹中找到该文件。

public virtual bool IsInRole(string);

第2个重载函数接受一个字符串参数,该参数表示一个用户组名称,比如MYCOMPUTER\Developer(MachineName\GroupName)表示了机器名为MYCOMPUTER的计算机上的Developer用户组。不过对于系统内置的用户组就不能这样表示了,比如Administrators,不能表示为MYCOMPUTER\Administrators,而应该像BUILTIN\Administrators这样,不过这样总觉得有点多余,不够自然。于是我们可以使用下面的重载函数。
public virtual bool IsInRole(WindowsBuiltInRole);

第3个重载函数接受一个WindowsBuiltInRole枚举类型参数,下面就是WindowsBuiltInRole枚举中定义的值:

1、AccountOperator- 管理计算机上或域中的用户帐号。

2、Administrator- 可以任意访问计算机或域

3、BackupOperator- 可以在文件系统上执行备份和恢复操作。

4、Guest- 和User类似,不过有更多的限制。

5、PowerUser- 和Administrator地位相近,不过有一些限制。

6、PrintOperator- 执行打印操作。

7、Replicator- 在域中执行文件复制。

8、SystemOperator- 管理计算机。

9、User- 用户不能执行危害系统或者影响整个系统的操作。

Permissions对象

作为.NET安全性两个重要的分支,基于角色的安全性和代码访问安全都离不开一个重要的概念--权限(permissions)。在基于角色的安全性中,PrincipalPermission类用来检查调用线程的用户身份;而在代码访问安全中,从CodeAccessPermission派生的类则用来检查执行当前方法的所有线程各自的权限。

权限对象通过已有的安全策略来说明操作是否被允许或拒绝。对于代码访问安全权限(不过这不适用于用户权限),.NET CLR提供了堆栈遍历机制来确定所有的调用堆栈帧是否具有应有的权限。需要注意的一点是,如果permission对象为null,那么我们可以将它和PermissionState.None视为等同的,说明没有提供任何权限。权限常用于下面这些场合:

1、定义执行代码所需要的权限。

2、系统的安全策略可以承认或拒绝代码请求的权限。

3、代码通过Demand方法来保证(要求)它调用的代码具有所需的权限。

4、代码还可以使用Assert,Deny或PermitOnly方法来跳过安全堆栈检查机制。

另外,我们还可以使用成组的权限,在PermissionSet类中我们就可以使用各种不同权限组集合。

下面我们还是先来看看最常用到的PrincipalPermission类,至于另外一个常用的CodeAccessPermission类,稍后将会在代码访问安全的内容中详细介绍。

作为PrincipalPermission类实现的三个接口之一,IPermission接口在PrincipalPermission类中有了举足轻重的作用。IPermission接口提供了以下方法:

1、Copy 创建并返回当前权限的相同副本。

2、Demand 如果调用堆栈上的内容不满足权限对象所指定的权限内容,则会在运行时引发SecurityException。该方法可以让当前代码不会被其他的恶意代码所利用。

3、Intersect 创建并返回一个权限,该权限是当前权限和指定权限的交集。

4、IsSubsetOf 确定当前权限是否为指定权限的子集。

5、Union 创建一个权限,该权限是当前权限与指定权限的并集。

在上面列出的方法中,Demand方法是最常用也是最重要的方法。Demand方法会对当前方法的所有调用者进行检查以确定它们是否有足够的权限访问以指定的方式访问特定的资源(通常的检查方式是从调用堆栈上最新的调用方法开始通过执行完全的堆栈遍历来满足安全条件,但堆栈遍历不是必须的,PrincipalPermission.Demand就没有进行堆栈遍历),如果检查失败的话,Demand方法会抛出SecurityException异常,只有没有任何异常抛出的情况下,Demand方法才成功返回。

下图显示的是IPermission接口的继承层次:

接着我们来讨论PrincipalPermission类,这里就需要我们前面所谈到principal对象了,在PrincipalPermission类的实例执行Demand方法时,实际上是在判断当前的principal对象是否匹配给定PrincipalPermission对象,如果不匹配的话,就抛出SecurityException。另外,Demand方法还可以用来强行让principal对象的identity验证通过,以便能不抛出异常,进行正常的操作。

下面的代码片断简要地说明了如何使用principalpermission对象:

String user1 = "Abbott";
String role1 = "StraightMan";
PrincipalPermission PrincipalPerm1 =
new PrincipalPermission(user1, role1);
String user2 = "Costello";
String role2 = "FunnyMan";
PrincipalPermission pp =
new PrincipalPermission(user2, role2);
PrincipalPerm1.Union(pp).Demand();

上面的代码说明了如何使用合并两个principalpermission对象,在合并之后,如果用户是StraightMan角色的Abbott或是FunnyMan角色的Costello,那么Demand将会成功返回。

以上就是基于角色的安全性中所需要用到的基础类和一些示例代码,希望大家能够从上面的内容中粗窥.NET安全性的门堂,能更好的应用.NET安全性来开发安全的程序。


火龙果软件/UML软件工程组织致力于提高您的软件工程实践能力,我们不断地吸取业界的宝贵经验,向您提供经过数百家企业验证的有效的工程技术实践经验,同时关注最新的理论进展,帮助您“领跑您所在行业的软件世界”。
资源网站: UML软件工程组织