[WCF權限控制]從兩個重要的概念談起:Identity與Principal

作者: Artech  來源: 博客園  發布時間: 2011-09-22 14:25  閱讀: 7281 次  推薦: 0   原文鏈接   [收藏]  

  在安全領域,認證和授權是兩個重要的主題。認證是安全體系的第一道屏障,守護著整個應用或者服務的第一道大門。當訪問者叩門請求進入的時候,認證體系通過驗證對方提供憑證確定其真實身份。作為看門人的認證體系,只有在證實了訪問者的真實身份的情況下才會為其打開城門,否則將之舉之門外。

  當訪問者入門之后,并不意味著它可以為所欲為。為了讓適合的人干適合的事,就需要授權機制為具體的人設置具體的權限,并根據這些權限設置決定試圖調用的操作或者訪問的資源對該訪問者是否是安全的。對于一個安全保障體系來說,授權是目的。但是授權的執行是假定已經通過認證體系確定了訪問者真實身份,因為用于進行授權采用的權限集是基于真個確定的身份的。在真正進入對WCF授權的具體介紹之前,我們有必要來了解一下這個“身份”的問題。

目錄
一、IIdentity
二、WinodwsIdentity
三、GenericIdentity
四、X509Identity
五、服務安全上下文中的身份

  一、IIdentity

  在.NET的安全應用編程接口中,身份通過System.Security.Principal.IIdentity接口表示。從下面表示IIdentity接口定義的代碼片斷中,我們可以看到這個接口定義其實很簡單,它具有如下三個只讀屬性:

  • Name:身份所代表的用戶的名稱;
  • IsAuthenticated:身份所代表的用戶是否經過認證;
  • AuthenticationType:身份認證所采用的類型。
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

  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();
}

  三、 GenericIdentity

  雖然對于這些我們常用的認證類型,比如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);

  四、X509Identity

  通過前面的介紹,我們知道了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憑證;
  • 客戶端憑證為用戶名/密碼憑證,并采用Windows認證模式;
  • 客戶端憑證為X.509證書憑證,并允許與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

  用以表示安全主體的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

  我們先來談談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應用在服務操作方法上,而不能應用在服務類型上。

0
0
 
標簽:WCF
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()