继上篇总结了CAS,这篇来总结一下与用户与角色相关的安全话题。每个用户都可以访问系统中他们各自的账户,这体现了一个用户的概念。一个系统还应按类型区分它的用户,一个用户类型即为一个角色。系统中的用户属于零个、一个或多个角色。在应用程序中授予用户何种级别的信任主要取决于用户所扮演的角色,系统需要在用户执行某个关键操作前验证其角色。
Windows用户方面的安全策略
在Windows中也实现了用户与角色的概念,但Windows中的角色是通过用户组来实现的。每个用户组有一个角色与之对应,将用户加入某一用户组后,该用户即获得了该用户组对应角色所拥有的权限。一个用户可以属于一个或多个用户组。
在一个Windows NT系统中每个运行的进程都有一个与其关联的表示当前用户身份的被称为主体的东西(主体标识了用户及其所属角色)。一个线程执行在其进程的主体的安全上下文中(也可以用重载过的Process.Start()方法创建一个与其父进程不在同一安全上下文的新进程)。例外情况是系统启动时创建的三个登录会话:系统会话、本地会话以及网络会话没有关联的用户主体。这些会话为一些自启动的服务所准备。
Windows为其中的各种资源关联一系列的访问规则。当一个线程访问一个Windows资源时,Windows会验证与该线程关联的主体是否符合访问规则的要求。
.NET用户方面安全介绍
.NET对Windows API函数进行了封装,使我们可以在编程时使用Windows安全系统。
.NET Framework中提供了IIdentity与IPrincipal接口分别表示身份与主体的概念。
IIdentity接口用于身份认证,其定义如下:
interface System.Security.Principal.IIdentity
{
string AuthenticationType{get;}
bool IsAuthenticationd{get;}
string Name{get;}
}
IPrincipal接口用于权限管理,其定义
interface System.Security.Principal.IPrincipal
{
IIdentity IIdentity{get;}
bool IsInRole(string role);
}
System.Security.Principal.WindowsIdentity与System.Security.Principal.WindowsPrincipal分别实现了IIdentity与IPrincipal接口,可以用这两个类来操作Windows安全。
示例:以下代码获得当前线程运行上下文的用户信息。
using System; using System.Security.Principal; class Program { static void Main() { IIdentity id = WindowsIdentity.GetCurrent(); Console.WriteLine( "Name : " + id.Name ); Console.WriteLine( "Authenticated? : " + id.IsAuthenticated ); Console.WriteLine( "Authentication Type : " +id.AuthenticationType ); } }
.NET Framework中还有对IIdentity与IPrincipal接口的其它实现;
- System.Web.Security.FormsIdentity类:用于ASP.NET中的表单验证。
- System.Web.Security.PassportIdentity类:用于Passport安全机制。
Windows中使用安全标识符(SID)标识用户与用户组。类似与GUID在一定时间与空间内SID是唯一的。
.NET Framework提供了三个类,其实例代表一个SID。
- System.Security.Principal.IdentityReference
- System.Security.Principal.NTAccount
- System.Security.Principal.SecurityIdentifier
其中NTAccout类以一种人类可读的方式表示一个SID,而SecurityIdentifier则用于将一个SID传给Windows。
这些类的应用先看如下示例:该示例方法用于验证当前的SID是否属于某个特定的Windows组。
代码说明:
WindowsIdentity的User属性表示当前Windows用户的SID。
System.Security.Principal.WellKnownSidType枚举值代表了大部分系统默认提供的用户组。
SecurityIdentifier的实例方法ISWellKnown(WellKnownSidType)用于验证当前SID是否属于某个特定的Windows组。
GetCurrent()函数的一个重载版本可以接受一个bool参数。当此参数为true时,仅当线程模拟另一个用户(模拟用户概念见下文)时,该方法才返回用户的身份,当该布尔量为false时,则只有在线程没有模拟其他用户时,该方法才返回用户的身份。
using System.Security.Principal; class Program { static void Main() { WindowsIdentity id = WindowsIdentity.GetCurrent(); SecurityIdentifier sid = id.User; NTAccount ntacc = sid.Translate( typeof( NTAccount ) ) as NTAccount; System.Console.WriteLine( "SID: " + sid.Value); System.Console.WriteLine( "NTAccount: " + ntacc.Value); if ( sid.IsWellKnown( WellKnownSidType.AccountAdministratorSid ) ) System.Console.WriteLine("这个管理员账户"); } }
在线程中模拟用户
默认情况下,一个Windows线程运行于它所在的进程上下文中。我们可以使用Win32函数WindowsIdentity.Impersonate()为一个线程的上下文关联一个用户,称作线程在模拟用户。过程是使用LogonUser()方法以一个用户登录,获取一个Identity,并将该Identity传入Impersonate()方法,开始模拟用户。
示例代码如下:
using System.Runtime.InteropServices; using System.Security.Principal; class Program{ [DllImport("Advapi32.Dll")] static extern bool LogonUser( string sUserName, string sDomain, string sUserPassword, uint dwLogonType, uint dwLogonProvider, out System.IntPtr token); [DllImport("Kernel32.Dll")] static extern void CloseHandle( System.IntPtr token ); static void Main(){ WindowsIdentity id1 = WindowsIdentity.GetCurrent(); System.Console.WriteLine( "模拟用户前:" + id1.Name); System.IntPtr pToken; if( LogonUser( "guest" , // login string.Empty, // Windows domain "guest_pwd" , // password 2, // LOGON32_LOGON_INTERACTIVE 0, // LOGON32_PROVIDER_DEFAUT out pToken) ) { WindowsIdentity.Impersonate( pToken ); WindowsIdentity id2 = WindowsIdentity.GetCurrent(); System.Console.WriteLine( "模拟用户后: " + id2.Name ); // Here, the underlying Windows thread ... // ... has the 'guest' identity. CloseHandle( pToken ); } } }
资源访问控制
Windows访问控制介绍
一个Windows资源(一个文件,一个同步对象或一个注册表项)都在物理上包含了一些与之对应的访问权限的信息,Windows可以根据这些信息推断出哪些用户对该资源拥有何种访问权限。这些信息包含在一个与资源关联的结构体 - 安全描述符(SD)中。针对某些类型的资源,Windows允许SD有继承性。这个功能使执行一次操作就可以设置有多个子目录的权限。
一个SD含有一个代表资源创建者或拥有者的SID及一个自由访问控制列表(Discretionary Access Control List, DACL)。DACL是一个有序的访问控制项(Access Control Elements, ACE)列表,而ACE是一个将SID与一个将SID与一个访问权限列表关联的结构。DACL中的ACE有以下两种类型:
-
授予相应SID访问权限的ACE。
-
拒绝授予其SID访问权限的ACE。
当一个线程试图获得一个资源的某些访问权限时,Windows将根据线程的SID以及资源的DACL作出相应的判断。在判断过程中,ACE的访问顺序将由它们在DACL中的存放顺序决定。每个ACE会根据它所包含的SID允许或拒绝该线程对资源的访问。一旦所有请求的访问权限都通过检查,这些权限就会立即被授予线程。而一旦其中某个访问权限被拒绝,那么所有请求的访问权限都会被拒绝。而且ACE在DACL中的存放顺序是有关的,因此在请求访问权限的时候Windows并不需要判断所有的ACE。
一个Windows资源的每个SD还包含另一个称作系统访问控制列表(System Access Control List, SACL)的ACE列表,Windows使用该列表审核对某个资源的访问。与DACL中的ACE一样,SACL中的ACE也是将一列访问权限与每个SID关联。其不同之处在于SACL中的ACE包含了两份二进制信息,其作用如下:
-
如果该ACE关联的SID允许获取某项访问权,那么授予该权限这个事件发生时是否要被记录。
-
如果该ACE关联的SID被拒绝授予某项访问权限,那么拒绝该权限这个事件发生时是否要被记录。
由上可知,SACL中ACE的存放顺序是无关紧要的。
System.Security.AccessControl中定义了一些类型来使用SD,下面分两部分讨论:
在.NET代码中使用特殊的SD
特殊的SD指可以专门用于操作Windows中特殊资源的SD的类,这个SD类型体系包括三部分:
此类型体系中的类有:
- System.Security.AccessControl.ObjectSecurity
- System.Security.AccessControl.DirectoryObjectSecurity
- System.DirectoryServices.ActiveDirectorySecuirity
- System.Security.AccessControl.CommonObjectSecurity
- Microsoft.Iis.Metabase.MetaKeySecurity
- System.Security.AccessControl.NativeObjectSecurity
- System.Security.AccessControl.EventWaitHandleSecurity
- System.Security.AccessControl.FileSystemSecurity
- System.Security.AccessControl.DirectorySecurity
- System.Security.AccessControl.FileSecurity
- System.Security.AccessControl.MutexSecurity
- System.Security.AccessControl.RegistrySecurity
- System.Security.AccessControl.SemaphoreSecurity
以上这些类接受代表ACE类型的参数以填充其中的DACL与SACL。注意ACE有代表DACL与SACL之分(下文介绍)。
这些类型的对象用于填充SD的DACL或SACL,所以这些代表ACE的类被分为表示DACL的访问规则,以及代表用于SACL审核的ACE的类。
- System.Security.AccessControl.AuthorizationRule
- System.Security.AccessControl.AccessRule
- Microsoft.Iis.Metabase.MetaKeyAccessRule
- System.Security.AccessControl.EventWaitHandleAccessRule
- System.Security.AccessControl.FileSystemAccessRule
- System.Security.AccessControl.MutexAccessRule
- System.Security.AccessControl.ObjectAccessRule
- System.DirectoryServices.ActiveDirectoryAccessRule
- System.DirectoryServices. [*]AccessRule
- System.Security.AccessControl.RegistryAccessRule
- System.Security.AccessControl.SemaphoreAccessRule
- System.Security.AccessControl.AuditRule
- Microsoft.Iis.Metabase.MetaKeyAuditRule
- System.Security.AccessControl.EventWaitHandleAuditRule
- System.Security.AccessControl.FileSystemAuditRule
- System.Security.AccessControl.MutexAuditRule
- System.Security.AccessControl.ObjectAuditRule
- System.DirectoryServices.ActiveDirectoryAuditRule
- System.Security.AccessControl.RegistryAuditRule
- System.Security.AccessControl.SemaphoreAuditRule
- Microsoft.Iis.Metabase.MetaKeyRights
- System.Security.AccessControl.EventWaitHandleRights
- System.Security.AccessControl.FileSystemRights
- System.Security.AccessControl.MutexRights
- System.Security.AccessControl.ObjectRights
- System.Security.AccessControl.SemaphoreRights
如:FileSystemRights枚举值包含AppendData等,而MutexRights枚举量含有值TakeOwnership。
另外,正如下面示例中将看到的,.NET Framework中直接代表相关Windows资源的各种不同类型(如System.Threading.Mutex、System.IO.File…)都有一个接受ACL的构造函数以及一个用于设置和获取该类型实例的ACL的方法Set/GetAccessControl()。
示例:使用DACL创建一份文件。
using System.Security.AccessControl; using System.Security.Principal; using System.IO; class Program { static void Main() { //创建DACL FileSecurity dacl = new FileSecurity(); //创建ACE FileSystemAccessRule ace = new FileSystemAccessRule( WindowsIdentity.GetCurrent().Name, FileSystemRights.AppendData | FileSystemRights.ReadData, AccessControlType.Allow); //使用ACE填充DACL dacl.AddAccessRule( ace ); //使用DACL创建一个文件 System.IO.FileStream fileStream = new System.IO.FileStream( @"file.bin" , FileMode.Create , FileSystemRights.Write , FileShare.None, 4096 , FileOptions.None, dacl ); fileStream.Write( new byte[] { 0, 1, 2, 3 }, 0, 4 ); fileStream.Close(); } }
经上述操作后可以在文件属性页的安全选项卡产看文件的访问权限。
在.NET代码中使用通用的SD
这些方式是操作SD类型的通用方式(与底层的Windows资源的类型无关),.NET提供了如下类型:
- System.Security.AccessControl.GenericSecurityDescriptor
- System.Security.AccessControl.CommonSecurityDescriptor
- System.Security.AccessControl.RawSecurityDescriptor
- System.Security.AccessControl.GenericAcl
- System.Security.AccessControl.CommonAcl
- System.Security.AccessControl.DescretionaryAcl
- System.Security.AccessControl.SystemAcl
- System.Security.AccessControl.RawAcl
- System.Security.AccessControl.GenericAce
- System.Security.AccessControl.CustomAce
- System.Security.AccessControl.KnownAce
- System.Security.AccessControl.CompoundAce
- System.Security.AccessControl.QualifiedAce
- System.Security.AccessControl.CommonAce
- System.Security.AccessControl.ObjectAce
示例:创建一个SD,向其DACL中添加ACE并将其转化为一个代表Windows资源的特殊SD(此处为互斥体)。
using System; using System.Security.AccessControl; using System.Security.Principal; class Program { static void Main() { //创建一个新的SD CommonSecurityDescriptor csd = new CommonSecurityDescriptor(false, false, string.Empty); DiscretionaryAcl dacl = csd.DiscretionaryAcl; //向DACL中添加ACE dacl.AddAccess( AccessControlType.Allow, // 枚举Allow、Deny. WindowsIdentity.GetCurrent().Owner, (int)MutexRights.TakeOwnership | (int)MutexRights.Synchronize, InheritanceFlags.None, // 禁用ACE继承 PropagationFlags.None); string sSDDL = csd.GetSddlForm( AccessControlSections.Owner ); MutexSecurity mutexSec = new MutexSecurity(); mutexSec.SetSecurityDescriptorSddlForm( sSDDL ); AuthorizationRuleCollection aces = mutexSec.GetAccessRules(true, true, typeof(NTAccount)); foreach ( AuthorizationRule ace in aces ) { if (ace is MutexAccessRule) { MutexAccessRule mutexAce = (MutexAccessRule)ace; Console.WriteLine( "-->SID : " + mutexAce.IdentityReference.Value ); Console.WriteLine( "访问权限类型:" + mutexAce.AccessControlType.ToString()); if (0xffffffff == (uint) mutexAce.MutexRights) Console.WriteLine( "拥有所有权限" ); else Console.WriteLine( "权限: " + mutexAce.MutexRights.ToString()); } } } }
另:CommonAcl.Purge(SecurityIdentifier)可以在ACL中删除某个SID所拥有的ACE项。
.NET与角色
类似于一个Windows线程在一个Windows安全上下文中执行,一个托管线程也可以根据开发者的选择在一个安全上下文中执行,这样既可以在Windows中利用用户/角色安全机制,还可以在诸如ASP.NET这样的环境中使用。实现这个功能的关键是Thread类提供的IPrincipal CurrentPrincipal{get;set; }属性。
一个主体可以通过3种不同的方式与一个线程关联。
-
使用Thread.CurrentPrincipal属性显示地将一个主体与一个托管线程关联。
-
为应用程序域定义一个主体策略。当托管线程执行到该域中的代码时,域中的主体策略最终会把主体与线程关联起来(除非该线程已经被显示地关联上了一个主体)。
-
可以为某个应用程序域中创建的或渗入进来的所有线程赋予一个特定的主体,前提是这些线程没有主体与之关联。为了实现这个目标,只需使用void AppDomain.SetThreadPrincipal(IPrincipal)方法。
以上这三个操作需要执行SecurityPermissionFlag.ControlPrincipal元权限。
定义应用程序域的主体策略
下面示例将当前应用程序域的主体策略设置为WindowsPrincipal。(这意味着当一个托管线程执行该域中的代码时,如果尚未有一个主体显示地与它关联,那么底层的Windows安全上下文将自动与其关联。)
using System; using System.Security.Principal; class Program{ static void Main(){ AppDomain.CurrentDomain.SetPrincipalPolicy( PrincipalPolicy.WindowsPrincipal); IPrincipal pr = System.Threading.Thread.CurrentPrincipal; IIdentity id = pr.Identity; Console.WriteLine( "Name : " + id.Name ); Console.WriteLine( "Athenticated? : " + id.IsAuthenticated ); Console.WriteLine( "Authentification type : "+id.AuthenticationType); } }
程序输出如下结果:
Name : Dev\Administrator
Athenticated? : True
Authentification type : NTLM
在一个应用程序域中可能定义的其他主体策略有:
PrincipalPolicy.NoPrincipal,表示没有主体与线程关联。(此种情况下,Thread.Current.Principal默认为null)。
PrincipalPolicy.Unauthenticated表示一个未通过身份认证的主体与线程关联。(这时,CLR将Thread.CurrentPrincipal属性与一个未通过身份验证的GenericPrincipal实例关联。)
后者是所有应用程序域默认采取的主体策略。
检查用户是否属于某个特定角色
有三种途径验证与托管线程关联的主体的角色:
代码示例如下:
using System.Security.Principal; class Program{ static void Main(){ IPrincipal pr = System.Threading.Thread.CurrentPrincipal; if( pr.IsInRole( @"BUILTIN\Administrators" ) ){ //此处主体为管理员身份 } else System.Console.WriteLine("You must be an administrator to run this program!"); } }
1). 使用该命名空间下的PrincipalPermissions类,示例如下:
static void Main(){
try{
PrincipalPermission prPerm = new PrincipalPermission(null, @"BUILTIN\Administrators" );
prPerm.Demand();
// 此处主体为管理员主体
}
catch(System.Security.SecurityException){
System.Console.WriteLine("You must be an administrator to run this program!");
}
}
这种方法的优点在于,其为角色管理与权限管理提供了一种较为一致的方式;另外它允许在一个操作中验证多个角色。示例:
PrincipalPermission prPermAdmin = new PrincipalPermission(null, @"BUILTIN\Administrators" );
PrincipalPermission prPermUser = new PrincipalPermission(null, @"BUILTIN\Users" );
System.Security.IPermission prPerm =prPermAdmin.Union(prPermUser);
prPerm.Demand();
2). 有上篇文章可知道,上述类有一个对应的Attributte - PrincipalPermissionAtrribute。该类的使用如下:
[PrincipalPermission( SecurityAction.Demand, Role= @"BUILTIN\Administrators")] static void Main() { // 此处是管理员主体 }
同样,如果想允许多个角色访问该方法,可以多次使用attribute。当用户是其中至少一个角色的成员时,他就被允许访问该方法。这种声明角色权限的attribute也可以被应用在类上,这时只有相关角色的人可以创建此类型的实例,这也是不能在构造函数上使用角色attribute的一种替代方式。
另外多说一点,角色attribute不仅可以声明角色的权限,它还可以赋予单个用户的权限。如下面这样:
[PrincipalPermission( SecurityAction.Demand, Name= @"MStar")]
或同时约束两者:
[PrincipalPermission( SecurityAction.Demand, Name= @"MStar", Role= @"BUILTIN\Administrators")]
但这种写法不建议,因为硬编码用户名的程序是脆弱的。
在Windows环境下使用这样的写法就足够了,因为当前用户已经通过了身份验证(登录Windows的过程),但是在Web环境中,需要强制验证后再判断用户角色,所以应该如下形式的attribute:
[PrincipalPermission( SecurityAction.Demand, Authenticated = true, Role= @"BUILTIN\Administrators")]
经过两篇文章把代码访问权限与角色访问权限总结的差不多了,最后做为补充总结一下.NET中的安全性工具与安全方面的异常管理。
安全性工具
此处总结了权限与程序集管理工具,证书相关工具放到专门介绍证书的部分。
.NET Framework SDK提供的许可和程序集管理工具
程序名称 |
作用 |
Caspol.exe |
表示代码访问安全策略工具。允许查看和修改安全设置 |
Signcode.exe |
文件标记工具,允许对可执行文件进行数字标记 |
Storeadm.exe |
用于独立存储区管理的工具,限制对文件系统的代码访问 |
Permview.exe |
显示程序集请求的访问权限 |
Peverify.exe |
检查可执行文件是否对类型安全编码进行了运行时检测 |
Secutil.exe |
从证书中提取公共密钥,并以能重用的格式放在源代码中 |
Sn.exe |
用强名创建程序集,即给命名空间和版本信息添加数字标记 |
使用SecurityException类处理异常
.NET Framework中的SystemException在2.0版本进行了很大了的扩展,可以提供更详细、安全环境中所遇到的各种异常信息。
下面表格总结了SystemException类的新属性内容
属性 |
描述 |
Action |
获取导致异常发生的安全行为 |
Demanded |
返回导致错误发生的权限、权限组或者权限组集合 |
DenySetInstance |
返回导致安全操作失败的被拒绝权限、权限组或者权限组集合 |
FailedAssemblyInfo |
返回失败的程序集的信息 |
FirstPermissionThatFailed |
返回失败的权限组或者权限组集合中的第一个权限 |
GrantedSet |
返回导致安全行为失败的一组权限 |
Method |
返回连接到异常的方法的信息 |
PermissionState |
返回抛出异常的权限状态 |
PermissionType |
返回抛出异常的权限类型 |
PermitOnlySetInstance |
如果安全行为失败,就返回permit-only堆栈结构中的一个权限组或者权限集合 |
RefusedSet |
返回被程序集拒绝的权限 |
Url |
返回导致异常的程序集的URL |
Zone |
返回导致异常的程序集的环境 |
参考书籍:C#和.NET2.0实战