在安全领域,认证和授权是两个重要的主题。认证是安全体系的第一道屏障,守护着整个应用或者服务的第一道大门。当访问者叩门请求进入的时候,认证体系通过验证对方提供凭证确定其真实身份。作为看门人的认证体系,只有在证实了访问者的真实身份的情况下才会为其打开城门,否则将之举之门外。
当访问者入门之后,并不意味着它可以为所欲为。为了让适合的人干适合的事,就需要授权机制为具体的人设置具体的权限,并根据这些权限设置决定试图调用的操作或者访问的资源对该访问者是否是安全的。对于一个安全保障体系来说,授权是目的。但是授权的执行是假定已经通过认证体系确定了访问者真实身份,因为用于进行授权采用的权限集是基于真个确定的身份的。在真正进入对WCF授权的具体介绍之前,我们有必要来了解一下这个“身份”的问题。
目录
一、IIdentity
二、WinodwsIdentity
三、GenericIdentity
四、X509Identity
五、服务安全上下文中的身份
在.NET的安全应用编程接口中,身份通过System.Security.Principal.IIdentity接口表示。从下面表示IIdentity接口定义的代码片断中,我们可以看到这个接口定义其实很简单,它具有如下三个只读属性:
public interface IIdentity
{
string AuthenticationType { get; }
bool IsAuthenticated { get; }
string Name { get; }
}
通过IIdentity表示的身份是基于某种认证类型的,不同类型的认证往往对应于不同的身份类型。以ASP.NET认证为例,如果我们采用Forms认证,那么认证后的身份通过一个FormsIdentity对象表示。而Windows Live Passport认证对应的具体身份类型则是PassportIdentity。在这里我们着重介绍一下如下三种身份类型:WindowsIdentity、GenericIdentity和X509Identity。
WindowsIdentity定义在System.Security.Principal命名空间下。顾名思义,WindowsIdentity用于表示一个基于Windows认证的身份。一个采用WindowsIdentity定义的Windos身份具有一系列的属性,它们主要定义在如下的代码片断中。
public class WindowsIdentity : IIdentity, ...
{
//其他成员
public virtual string Name { get; }
public string AuthenticationType { get; }
public virtual bool IsAuthenticated { get; }
public IdentityReferenceCollection Groups { get; }
public virtual bool IsAnonymous { get; }
public virtual bool IsGuest { get; }
public virtual bool IsSystem { get; }
}
对于用于表示认证类型的AuthenticationType属性来说,在工作组模式下返回NTLM。对于域模式,如果操作系统是Vista或者以后的版本,该属性返回Negotiate,表示采用SPNEGO认证协议。而对于之前的Windows版本,则该属性值为Kerberos。
Groups属性返回WindowsIdentity对应的Windows帐号所在的用户组(User Group),而IsGuest则用于判断Windows帐号是否存在于Guest用户组中。IsSystem属性则表示Windows帐号是否是一个系统帐号。
如果你对ASP.NET的安全有一定的了解,应该知道我们可以对IIS进行相应的配置使ASP.NET应用支持匿名用户。也就是说,用户无需提供具体的用户凭证,而是以匿名的方式登录到ASP.NET站点中。对于匿名登录,IIS实际上会采用一个预先指定的Windows帐号进行登录。而在这里,IsAnonymous属性就表示该WindowsIdentity对应的Windows帐号是否是匿名帐号。
对于匿名身份的问题,在这里还有一点值得补充一下。WindowsIdentity定义了如下一个静态的GetAnonymous方法用于返回一个表示匿名身份的WindowsIdentity对象。但是这仅仅是一个空的WindowsIdentity对象而以,并不对应着某个确定的Windows帐号。
public class WindowsIdentity : IIdentity, ...
{
//其他成员
public static WindowsIdentity GetAnonymous()
}
任何一个具体的Windows进程总是运行在一个确定的安全身份下。如果你手工启动一个.exe文件,被开启的进程会运行在基于当前登录帐号的身份下。如有你同时拥有多个Windows帐号,你可以通过“Run As”的方式选择一个不同于当前登录帐号的身份去运行某个.exe文件。而对于很多的Windows服务,它们大多运行在某个系统帐号下。比如我们熟悉的IIS(IIS6或者之后的版本)在默认的情况下就运行在Network Service这个系统帐号下面。当一个线程在这个进程中被创建并启动的时候,进程的安全身份会自动附加到线程上。WindowsIdentity为我们提供了如下一个GetCurrent静态方法返回基于当前线程/进程的WindowsIdentity。
public class WindowsIdentity : IIdentity, ...
{
//其他成员
public static WindowsIdentity GetCurrent();
}
虽然对于这些我们常用的认证类型,比如Windows认证、Forms认证和Windows Live Passport认证,都具有对应的安全身份类型。如果我们采用自定义的认证方式,是否意味着我们也需要定义一个实现了IIdentity接口的类型呢?实际上是不需要的,我们可以直接使用GenericIdentity这个类型。
正如名称所体现的一样,GenericIdentity为我们定义了一个一般性的安全身份。GenericIdentity的定义非常简单,仅仅实现了定义在IIdentity接口的三个只读属性而以。我们可以通过指定用户名或者用户名与认证类型来创建一个GenericIdentity对象。下面的代码片断体现了GenericIdentity的整个定义。
public class GenericIdentity : IIdentity
{
public GenericIdentity(string name);
public GenericIdentity(string name, string type);
public virtual string AuthenticationType { get; }
public virtual bool IsAuthenticated { get; }
public virtual string Name { get; }
}
由于GenericIdentity的IsAuthenticated属性是只读,也不能通过构造函数对其进行初始化,那么如何确定一个通过GenericIdentity对象表示的安全身份是否已经通过认证了呢?实际上,GenericIdentity采用很简单的逻辑来判断其自身是否经过认证:如果用户名不为空,IsAuthenticated返回True,否则返回False。下面给出的代码可以验证这一点。
var anonymousIdentity = new GenericIdentity("");
var authenticatedIdentity = new GenericIdentity("Foo");
Debug.Assert(anonymousIdentity.IsAuthenticated == false);
Debug.Assert(authenticatedIdentity.IsAuthenticated == true);
通过前面的介绍,我们知道了WCF具有三种典型的认证方式:Windows认证、用户名/密码认证和证书认证。认证的方式决定了安全身份的类型,对于Windows认证和用户名/密码认证,认证后的安全身份分别由一个WindowsIdentity和GenericIdentity表示。但是对于证书认证,则对应着另一种安全身份类型:X509Identity。
X509Identity定义在程序集System.IdentityModel中,对应的命名空间是System.IdentityModel.Claims。从下面给出的定义我们可以看出X509Identity仅仅是一个内部(Internal)类型。
internal class X509Identity : GenericIdentity, IDisposable
{
//其他成员
public X509Identity(X500DistinguishedName x500DistinguishedName);
public X509Identity(X509Certificate2 certificate);
public X509Identity Clone();
public void Dispose();
public override string Name { get; }
}
X509Identity直接继承自GenericIdentity。我们可以通过传入一个X509Certificate2对象或者以X500DistinguishedName对象表示的证书的标识名称来创建X509Identity。X509Identity重写了GenericIdentity的Name属性,最终作为名称的返回的是证书的主题名称和指纹的组合,<<主题名称>>; <<指纹>>(分号之后具有一个空格,比如:CN=Foo; 12BA3675C89BD7FE00E3F7E92A620749FB9E6D89)。X509Identity对象的AuthenticationType属性为“X509”。
当服务安全开始的情况,服务端在经过认证之后会创建一个上下文用以存储基于当前服务调用相关的安全相关的信息,其中就包含了代表被认证客户端的安全身份。这个上下文被称为服务安全上下文,通过类型ServiceSecurityContext表示。
public class ServiceSecurityContext
{
//其他成员
public static ServiceSecurityContext Current { get; }
public IIdentity PrimaryIdentity { get; }
public WindowsIdentity WindowsIdentity { get; }
public bool IsAnonymous { get; }
public static ServiceSecurityContext Anonymous { get; }
}
你可以通过两种方式获取当前的ServiceSecurityContext,一种是通过ServiceSecurityContext的静态只读属性Current,另一种则是通过当前OperationContext的ServiceSecurityContext属性。实际上通过这两种方式得到的是同一个ServiceSecurityContext。ServiceSecurityContext对象的同一性可以通过下面的代码来验证。
var securityContext1 = OperationContext.Current.ServiceSecurityContext;
var securityContext2 = ServiceSecurityContext.Current;
Debug.Assert(object.ReferenceEquals(securityContext1, securityContext2));
ServiceSecurityContext具有两个表示安全身份的属性PrimaryIdentity和WindowsIdentity,它们都代表当前客户端的身份。对于Windows认证,这两个属性返回同一个WindowsIdentity对象。不过需要注意的是,这是所说的Windows认证实际上包括如下三种情况:
而对于不属于上述三种情况下的非Windows凭证,当前ServiceSecurityContext的WindowsIdentity属性返回Null,而PrimaryIdentity属性则因客户端凭证类型和认证方式有所区别。具体来说,如果客户端凭证为用户名/密码凭证,并采用Membership和Custom认证模式,则在成功认证的情况下PrimaryIdentity的属性返回一个以用户名作为名称的GenericIdentity。如果客户端凭证为X.509证书凭证,但不采用Windows帐号映射机制,则PrimaryIdentity的属性返回的是一个X509Identity。
对于匿名客户端(客户端凭证类型为None),PrimaryIdentity返回的是一个空的GenericIdentity,IsAnonymous返回True。你通过静态属性Anonymous可以返回一个匿名ServiceSecurityContext。下面的表格体现了成功认证后当前ServiceSecurityContext的PrimaryIdentity与客户端凭证类型以及认证模式之间的关系。
毫不夸张地说,安全主体(Principal)是整个授权机制的核心。我们可以简单地将安全主体定义成能够被成功实施授权的主体。一个安全主体具有两个基本的要素:基于某个用户的安全身份和该用户具有的权限。绝大部分的授权都是围绕着“角色”进行的,我们将一组相关的权限集和一个角色绑定,然后分配给某个用户。所以在基于角色授权环境下,我们可以简单地将安全主体表示成:身份 + 角色。在.NET基于安全的应用编程接口中,通过IPrincipal接口表示安全主体。
目录
一、IPrincipal
二、WindowsPrincipal
三、GenericPrincipal
四、 基于安全主体的授权
用以表示安全主体的IPrincipal接口定义在System.Security.Principal命名空间下。IPrincipal的定义体现在如下的代码片断中,从中我们可以看出IPrincipal仅仅具有两个成员。只读属性Identity表示安全主体的身份,而IsInRole用以判断安全主体对应的用户是否被分配了给定的角色。
publicinterface IPrincipal
{
bool IsInRole(string role);
IIdentity Identity { get; }
}
上面我们具体介绍了IIdentity接口的两个实现,WindowsIdentity和GenericIdentity。实际上IPrincipal也具有相类似的实现类型:WindowsPrincipal和GenericPrincipal,它们均定义在System.Security.Principal命名空间下。
我们先来谈谈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方法。
publicclass WindowsPrincipal : IPrincipal
{
public WindowsPrincipal(WindowsIdentity ntIdentity);
publicvirtualbool IsInRole(int rid);
publicvirtualbool IsInRole(SecurityIdentifier sid);
publicvirtualbool IsInRole(WindowsBuiltInRole role);
publicvirtualbool IsInRole(string role);
publicvirtual IIdentity Identity { get; }
}
三、GenericPrincipal
而一个GenericPrincipal对象本质上就是对一个IIdentity对象和表示角色列表的字符创数组的封装而已。下面的代码片断体现了整个GenericPrincipal的定义。
publicclass GenericPrincipal : IPrincipal
{
public GenericPrincipal(IIdentity identity, string[] roles);
publicvirtualbool IsInRole(string role);
publicvirtual IIdentity Identity { get; }
}
四、基于安全主体的授权
一个通过接口IPrincipal表示的安全主体不仅仅可以表示被授权用户的身份(通过Identity属性),其本身就具有授权判断的能力(通过IsInRole方法)。如果我们在访问者成功实施认证后根据用户的权限设置构建一个安全主体对象,并将其存储在当前的上下文中,在需要的时候就可以从该安全主体获取出来以完成对授权的实现。
实际上Windows授权机制的实现就是按照这样的原理实现的,而这个所谓的上下文就是当前线程的线程本地存储(TLS:Thread Local Storage)。而反映在编程上,你可以通过Thread类型的CurrentPrincipal属性来获取或者设置这个当前的安全主体。
publicsealedclass Thread
{
//其他成员
publicstatic IPrincipal CurrentPrincipal { get; set; }
}
一旦为当前线程设置了安全主体,在需要确定当前用户是否有权限执行某项操作或者访问某个资源的时候,就可以通过上述的这个CurrentPrincipal属性将设置的安全主体获取出来,通过调用IsInRole方法判断当前用户是否具有相应的权限。下面的代码体现了用户需要具有Administrators角色(或者Windows用户组)才能执行被授权的操作,否则会抛出一个安全异常。
IPrincipal currentPrincipal = Thread.CurrentPrincipal;
if (currentPrincipal.IsInRole("Administrators"))
{
//执行被授权的操作
}
else
{
//抛出安全异常
}
我们通过编写具体授权逻辑的编方式称为命令式编程(Imperative Programming)。如果一个针对某个方法的授权(当前用户是否有权限调用需要被授权的方法),我们还可以省去所有授权代码,采用一种声明式的编程方式(Declarative Programming)。声明式的授权需要使用到一个特殊的特性:PrincipalPermissionAttribute。
从如下代码片断给出的关于PrincipalPermissionAttribute类型的定义我们不难看出,这是一个与代码访问安全(CAS:Code Access Security)的特性(继承自CodeAccessSecurityAttribute)。如果在某个方法上应用了该特性,授权将被以检验代码访问安全的方式来执行。PrincipalPermissionAttribute的Authenticated属性用于指定目标方法是否一定需要在认证用户环境下执行。而Name和Role表示执行目标方法所允许的用户名和角色。
从应用在PrincipalPermissionAttribute上面的AttributeUsageAttribute定义我们可以看出,该特性指定应应到类型和方法级别,并且可以在同一个目标元素上应用多个PrincipalPermissionAttribute特性。如果在同一个方法上应用了不止一个PrincipalPermissionAttribute特性,那么只要定义在任何一个PrincipalPermissionAttribute上的授权策略通过检验,就任何目标方法被授权了。
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple=true]
public sealed class PrincipalPermissionAttribute : CodeAccessSecurityAttribute
{
//其他成员
public PrincipalPermissionAttribute(SecurityAction action);
public bool Authenticated { get; set; }
public string Name { get; set; }
public string Role { get; set; }
}
在下面的程序中,我们创建了四个应用了PrincipalPermissionAttribute特性的测试方法(TestMethod1、TestMethod2、TestMethod3和TestMethod4)。其中TestMethod1和TestMethod2上设置了不同的用户名Foo和Bar,而TestMethod3和TestMethod4则设置了不同的角色,前者设置的单一的角色Adminstrators,后者则设置了两个角色Adminstrators和Guests。四个方法均在Try/Catch中执行,在执行之前,一个GenericPrincipal对象被创建并设置成当前线程的安全主体。该GenericPrincipal安全身份是一个用户名为Foo的GenericIdentity,并且具有唯一的角色Guests。通过最终的输出,我们可以看出系统自动为我们完成的授权正式采用了定义于应用在目标方法上的PrincipalPermissionAttribute特性中的授权策略。
staticvoid Main(string[] args)
{
GenericIdentity identity = new GenericIdentity("Foo");
Thread.CurrentPrincipal = new GenericPrincipal(identity, newstring[] { "Guests" });
Invoke(() => TestMethod1());
Invoke(() => TestMethod2());
Invoke(() => TestMethod3());
Invoke(() => TestMethod4());
}
public static void Invoke(Action action)
{
try
{
action();
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
}
[PrincipalPermission(SecurityAction.Demand, Name ="Foo")]
publicstaticvoid TestMethod1()
{
Console.WriteLine("TestMethod1方法被成功执行。");
}
[PrincipalPermission(SecurityAction.Demand, Name ="Bar")]
publicstaticvoid TestMethod2()
{
Console.WriteLine("TestMethod2方法被成功执行。");
}
[PrincipalPermission(SecurityAction.Demand, Role="Adminstrators")]
publicstaticvoid TestMethod3()
{
Console.WriteLine("TestMethod3方法被成功执行。");
}
[PrincipalPermission(SecurityAction.Demand, Role ="Adminstrators")]
[PrincipalPermission(SecurityAction.Demand, Role ="Guests")]
publicstaticvoid TestMethod4()
{
Console.WriteLine("TestMethod4方法被成功执行。");
}
输出结果:
TestMethod1方法被成功执行。
对主体权限的请求失败。
对主体权限的请求失败。
TestMethod4方法被成功执行。
虽然从应用在PrincipalPermissionAttribute的AttributeUsageAttribute特性定义上看,PrincipalPermissionAttribute是可同时应用在类和方法上的。但是,当我们采用这个特性以声明的方式进行WCF服务授权的时候,我们只能将PrincipalPermissionAttribute应用在服务操作方法上,而不能应用在服务类型上。