毫不夸张地说,安全主体(Principal)是整个授权机制的核心。我们可以简单地将将安全主体定义成能够被成功实施授权的主体。一个安全主体具有两个基本的要素:基于某个用户的安全身份和该用户具有的权限。绝大部分的授权都是围绕着“角色”进行的,我们将一组相关的权限集和一个角色绑定,然后分配给某个用户。所以在基于角色授权环境下,我们可以简单地将安全主体表示成:身份 + 角色。在.NET基于安全的应用编程接口中,通过IPrincipal接口表示安全主体。
目录
一、IPrincipal
二、WindowsPrincipal
三、GenericPrincipal
四、 基于安全主体的授权
一、IPrincipal
用以表示安全主体的IPrincipal接口定义在System.Security.Principal命名空间下。IPrincipal的定义体现在如下的代码片断中,从中我们可以看出IPrincipal仅仅具有两个成员。只读属性Identity表示安全主体的身份,而IsInRole用以判断安全主体对应的用户是否被分配了给定的角色。
1: public interface IPrincipal
2: {
3: bool IsInRole(string role);
4: IIdentity Identity { get; }
5: }
上面我们具体介绍了IIdentity接口的两个实现,WindowsIdentity和GenericIdentity。实际上IPrincipal也具有相类似的实现类型:WindowsPrincipal和GenericPrincipal,它们均定义在System.Security.Principal命名空间下。
二、WindowsPrincipal
我们先来谈谈WindowsPrincipal。之前我们谈到一个安全主体具有身份与权限两个基本要素,在Windows安全体系下,某个用户具有的权限决定于它被添加到那些用户组(User Group)中。Windows默认为我们创建了一些用户组,比如Adminstrators和Guests等。你也根据需要创建自定义用户组。从本质上讲,Windows的用户组和我们之前谈到的角色并没有本质的区别,都是一组权限的载体。
WindowsPrincipal的定义如下。表示安全身份的只读属性Identity返回一个WindowsIdentity对象,该对象在WindowsPrincipal被创建的时候通过构造函数指定。所以在Windows安全体系,一个用户组具有多种不同的标识方式,比如相对标识符(RID:Relative Identifier)、安全标识符(SID:Security Identifier)和用户组名称,对于一些与定义的用户组甚至还可以通过System.Security.Principal.WindowsBuiltInRole枚举来表示,所以WindowsPrincipal具有若干重载的IsInRole方法。
1: public class WindowsPrincipal : IPrincipal
2: {
3: public WindowsPrincipal(WindowsIdentity ntIdentity);
4: public virtual bool IsInRole(int rid);
5: public virtual bool IsInRole(SecurityIdentifier sid);
6: public virtual bool IsInRole(WindowsBuiltInRole role);
7: public virtual bool IsInRole(string role);
8: public virtual IIdentity Identity { get; }
9: }
三、GenericPrincipal
而一个GenericPrincipal对象本质上就是对一个IIdentity对象和表示角色列表的字符创数组的封装而以。下面的代码片断体现了整个GenericPrincipal的定义。
1: public class GenericPrincipal : IPrincipal
2: {
3: public GenericPrincipal(IIdentity identity, string[] roles);
4: public virtual bool IsInRole(string role);
5: public virtual IIdentity Identity { get; }
6: }
四、基于安全主体的授权
一个通过接口IPrincipal表示的安全主体不仅仅可以表示被授权用户的身份(通过Identity属性),其本身就具有授权判断的能力(通过IsInRole方法)。如果我们在访问者成功实施认证后根据用户的权限设置构建一个安全主体对象,并将其存储在当前的上下文中,在需要的时候就可以改安全主体获取出来以完成对授权的实现。
实际上Windows授权机制的实现就是安全这样的原理实现的,而这个所谓的上下文就是当前线程的线程本地存储(TLS:Thread Local Storage)。而反映在编程上,你可以通过Thread类型的CurrentPrincipal属性来获取或者设置这个当前的安全主体。
1: public sealed class Thread
2: {
3: //其他成员
4: public static IPrincipal CurrentPrincipal { get; set; }
5: }
一旦为当前线程设置了安全主体,在需要确定当前用户是否有权限执行某项操作或者访问某个资源的时候,就可以通过上述的这个CurrentPrincipal属性将设置的安全主体获取出来,通过调用IsInRole方法判断当前用户是否具有相应的权限。下面的代码体现了用户需要具有Administrators角色(或者Windows用户组)才能执行被授权的操作,否则会抛出一个安全异常。
1: IPrincipal currentPrincipal = Thread.CurrentPrincipal;
2: if (currentPrincipal.IsInRole("Administrators"))
3: {
4: //执行被授权的操作
5: }
6: else
7: {
8: //抛出安全异常
9: }
我们通过编写具体授权逻辑的编程方式称为命令式编程(Imperative Programming)。如果一个针对某个方法的授权(当前用户是否有权限调用需要被授权的方法),我们还可以省却所有授权代码,采用一种声明式的编程方式(Declarative Programming)。声明式的授权需要使用到一个特殊的特性:PrincipalPermissionAttribute。
从如下代码片断给出的关于PrincipalPermissionAttribute类型的定义我们不难看出,这是一个与代码访问安全(CAS:Code Access Security)的特性(继承自CodeAccessSecurityAttribute)。如果在某个方法上应用了该特性,授权将被以检验代码访问安全的方式来执行。PrincipalPermissionAttribute的Authenticated属性用于指定目标方法是否一定需要在认证用户环境下执行。而Name和Role表示执行目标方法所允许的用户名和角色。
1: [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple=true]
2: public sealed class PrincipalPermissionAttribute : CodeAccessSecurityAttribute
3: {
4:
5: //其他成员
6: public PrincipalPermissionAttribute(SecurityAction action);
7:
8: public bool Authenticated { get; set; }
9: public string Name { get; set; }
10: public string Role { get; set; }
11: }
从应用在PrincipalPermissionAttribute上面的AttributeUsageAttribute定义我们可以看出,该特性指定应应到类型和方法级别,并且可以在同一个目标元素上应用多个PrincipalPermissionAttribute特性。如果在同一个方法上应用了不止一个PrincipalPermissionAttribute特性,那么只要定义在任何一个PrincipalPermissionAttribute上的授权策略通过检验,就任何目标方法被授权了。
在下面的程序中,我们创建了四个应用了PrincipalPermissionAttribute特性的测试方法(TestMethod1、TestMethod2、TestMethod3和TestMethod4)。其中TestMethod1和TestMethod2上设置了不同的用户名Foo和Bar,而TestMethod3和TestMethod4则设置了不同的角色,前者设置的单一的角色Adminstrators,后者则设置了两个角色Adminstrators和Guests。四个访均在Try/Catch中执行,在指定之前一个GenericPrincipal对象被创建并设置成当前线程的安全主体。该GenericPrincipal安全身份是一个用户名为Foo的GenericIdentity,并且具有唯一的角色Guests。通过最终的输出,我们可以看出系统自动为我们完成的授权正式采用了定义于应用在目标方法上的PrincipalPermissionAttribute特性中的授权策略。
1: static void Main(string[] args)
2: {
3: GenericIdentity identity = new GenericIdentity("Foo");
4: Thread.CurrentPrincipal = new GenericPrincipal(identity, new string[] { "Guests" });
5: Invoke(() => TestMethod1());
6: Invoke(() => TestMethod2());
7: Invoke(() => TestMethod3());
8: Invoke(() => TestMethod4());
9: }
10:
11: public static void Invoke(Action action)
12: {
13: try
14: {
15: action();
16: }
17: catch(Exception ex)
18: {
19: Console.WriteLine(ex.Message);
20: }
21: }
22:
23: [PrincipalPermission(SecurityAction.Demand, Name = "Foo")]
24: public static void TestMethod1()
25: {
26: Console.WriteLine("TestMethod1方法被成功执行。");
27: }
28: [PrincipalPermission(SecurityAction.Demand, Name = "Bar")]
29: public static void TestMethod2()
30: {
31: Console.WriteLine("TestMethod2方法被成功执行。");
32: }
33: [PrincipalPermission(SecurityAction.Demand, Role="Adminstrators")]
34: public static void TestMethod3()
35: {
36: Console.WriteLine("TestMethod3方法被成功执行。");
37: }
38: [PrincipalPermission(SecurityAction.Demand, Role = "Adminstrators")]
39: [PrincipalPermission(SecurityAction.Demand, Role = "Guests")]
40: public static void TestMethod4()
41: {
42: Console.WriteLine("TestMethod4方法被成功执行。");
43: }
输出结果:
1: TestMethod1方法被成功执行。
2: 对主体权限的请求失败。
3: 对主体权限的请求失败。
4: TestMethod4方法被成功执行。
虽然从应用在PrincipalPermissionAttribute的AttributeUsageAttribute特性定义上看,PrincipalPermissionAttribute是可同时应用在类和方法上的。但是,当我们采用这个特性以声明的方式进行WCF服务授权的时候,我们只能将PrincipalPermissionAttribute应用在服务操作方法上,而不能应用在服务类型上。
从两个重要的概念谈起:Identity与Principal[上篇]
从两个重要的概念谈起:Identity与Principal[下篇]
微信公众账号:大内老A
微博: www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号 蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。