[WCF許可權控制]通過擴充套件自行實現服務授權[提供原始碼下載]

行者武松發表於2017-10-26

其實針對安全主體的授權實現的原理很簡單,原則上講,只要你能在服務操作執行之前能夠根據本認證的使用者正確設定當前的安全主體就可以了。如果你瞭解WCF的整個執行時框架結構,你會馬上想到用於授權的安全主體初始化可以通過自定義CallContextInitializer來實現。[原始碼從這裡下載]

目錄:
CallContextInitializer簡介
步驟一、自定義CallContextInitializer
步驟二、建立服務行為
步驟三、使用服務行為進行授權

CallContextInitializer簡介

對於WCF的整個執行時框架來說,CallContextInitializer是一個重要的物件。一個執行時服務操作(DispatchOperation)具有一個CallContextInitializer列表。而每一個CallContextInitializer實現ICallContextInitializer介面。如下面的程式碼片斷所示,ICallContextInitializer具有兩個方法BeforeInvoke和AfterInvoke。它們分別在操作方法之前前後進行呼叫上下文的初始化和清理操作。那麼我麼就可以自定義CallContextInitializer,在BeforeInvoke中初始化當前的安全主體。

   1: public interface ICallContextInitializer
   2: {
   3:     void AfterInvoke(object correlationState);
   4:     object BeforeInvoke(InstanceContext instanceContext, IClientChannel channel, Message message);
   5: }

步驟一、自定義CallContextInitializer

我們授權自定義一個抽象的CallContextInitializer,起名為AuthorizationCallContextInitializerBase。下面的程式碼片斷給出了AuthorizationCallContextInitializerBase的整個定義。AuthorizationCallContextInitializerBase具有一個抽象的方法GetPrincipal用於根據當前的安全上下文資訊建立安全主體。該方法會在BeforeInvoke方法被呼叫,返回值被設定成當前執行緒的安全主體。為了讓服務操作執行之後當前執行緒的上下文恢復到執行前的狀態,在BeforeInvoke方法中當前的安全主體被儲存下來,並傳遞給AfterInvoke方法中恢復當前執行緒的原來的安全主體。

   1: public abstract class AuthorizationCallContextInitializerBase: ICallContextInitializer
   2: {
   3:     public void AfterInvoke(object correlationState)
   4:     {
   5:         IPrincipal principal = correlationState as IPrincipal;
   6:         if (null != principal)
   7:         {
   8:             Thread.CurrentPrincipal = principal;
   9:         }
  10:     }
  11:     public object BeforeInvoke(InstanceContext instanceContext, IClientChannel channel, Message message)
  12:     {
  13:         var originalPrincipal = Thread.CurrentPrincipal;
  14:         Thread.CurrentPrincipal = this.GetPrincipal(ServiceSecurityContext.Current);
  15:         return originalPrincipal;
  16:     }
  17:     protected abstract IPrincipal GetPrincipal(ServiceSecurityContext serviceSecurityContext);
  18: }

基於兩種安全主體許可權模式,我們建立了兩個具體的CallContextInitializer。第一個為基於Windows使用者組的WindowsAuthorizationCallContextInitializer。WindowsAuthorizationCallContextInitializer定義如下,它繼承了AuthorizationCallContextInitializerBase,在實現的抽象方法GetPrincipal中根據當前ServiceSecurityContext的WindowsIdentity屬性建立WindowsPrincipal。

   1: public class WindowsAuthorizationCallContextInitializer:AuthorizationCallContextInitializerBase
   2: {
   3:     protected override IPrincipal GetPrincipal(ServiceSecurityContext serviceSecurityContext)
   4:     {
   5:         WindowsIdentity identity = serviceSecurityContext.WindowsIdentity;
   6:         if (null == identity)
   7:         {
   8:             identity =WindowsIdentity.GetAnonymous();
   9:         }
  10:         return new WindowsPrincipal(identity);
  11:     }
  12: }

而基於ASP.NET Roles安全主體許可權模式的安全主體初始化實現在如下所示的AspRoleAuthorizationCallContextInitializer類中。AspRoleAuthorizationCallContextInitializer具有一個RoleProvider屬性,表示用於獲取當前使用者角色列表的RoleProvider,該屬性在建構函式中被初始化。在實現的GetPrincipal抽象方法中,藉助於RoleProvider獲取基於當前使用者的所有角色,並建立GenericPrincipal

   1: public class AspRoleAuthorizationCallContextInitializer : AuthorizationCallContextInitializerBase
   2: {
   3:     public RoleProvider RoleProvider { get; private set; }
   4:     public AspRoleAuthorizationCallContextInitializer(RoleProvider roleProvider)
   5:     {
   6:         this.RoleProvider = roleProvider;
   7:     }
   8:     protected override IPrincipal GetPrincipal(ServiceSecurityContext serviceSecurityContext)
   9:     {
  10:         var userName = serviceSecurityContext.PrimaryIdentity.Name;
  11:         var identity = new GenericIdentity(userName);
  12:         var roles = this.RoleProvider.GetRolesForUser(userName);
  13:         return new GenericPrincipal(identity, roles);
  14:     }
  15: }

步驟二、建立服務行為

現在,使用者進行安全主體初始化的兩個具體的CallContextInitializer已經建立完成,現在需要做的工作就是將其應用到WCF的執行時框架體系之中。為此,我們建立瞭如下一個服務行為ServiceAuthorizationBehaviorAttribute。ServiceAuthorizationBehaviorAttribute是一個自定義特性,並實現了IServiceBehavior介面。它具有兩個兩個屬性:PrincipalPermissionMode和CallContextInitializer。前者在建構函式中指定,我們根據該引數決定具體建立的CallContextInitializer型別,是WindowsAuthorizationCallContextInitializer還是AspRoleAuthorizationCallContextInitializer。而建構函式中具有一個可選的引數roleProviderName表示採用的RoleProvider配置名稱。

   1: [AttributeUsage( AttributeTargets.Class)]
   2: public class ServiceAuthorizationBehaviorAttribute: Attribute, IServiceBehavior
   3: {
   4:     public PrincipalPermissionMode PrincipalPermissionMode { get; private set; }
   5:     public ICallContextInitializer CallContextInitializer { get; private set; }
   6:  
   7:     public ServiceAuthorizationBehaviorAttribute(PrincipalPermissionMode principalPermissionMode, string roleProviderName = "")
   8:     {
   9:         switch (principalPermissionMode)
  10:         {
  11:             case PrincipalPermissionMode.UseWindowsGroups:
  12:                 {
  13:                     this.CallContextInitializer = new WindowsAuthorizationCallContextInitializer();
  14:                     break;
  15:                 }
  16:             case PrincipalPermissionMode.UseAspNetRoles:
  17:                 {
  18:                     if (string.IsNullOrEmpty(roleProviderName))
  19:                     {
  20:                         this.CallContextInitializer = new AspRoleAuthorizationCallContextInitializer(Roles.Provider);
  21:                     }
  22:                     else
  23:                     {
  24:                         this.CallContextInitializer = new AspRoleAuthorizationCallContextInitializer(Roles.Providers[roleProviderName]);
  25:                     }
  26:                     break;
  27:                 }
  28:             case PrincipalPermissionMode.Custom:
  29:                 {
  30:                     throw new ArgumentException("只有UseWindowsGroups和UseAspNetRoles模式被支援!");
  31:                 }
  32:         }
  33:     }
  34:  
  35:     public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters) { }
  36:  
  37:     public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
  38:     {
  39:         if (null == this.CallContextInitializer)
  40:         {
  41:             return;
  42:         }
  43:  
  44:         foreach (ChannelDispatcher channelDispatcher in serviceHostBase.ChannelDispatchers)
  45:         {
  46:             foreach (EndpointDispatcher endpoint in channelDispatcher.Endpoints)
  47:             {
  48:                 foreach (DispatchOperation operation in endpoint.DispatchRuntime.Operations)
  49:                 {
  50:                     operation.CallContextInitializers.Add(this.CallContextInitializer);
  51:                 }
  52:             }
  53:         }
  54:     }
  55:     public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) { }
  56: }

CallContextInitializer的註冊實現在ApplyDispatchBehavior方法中,邏輯很簡單:遍歷所有通道分發器(ChannelDispatcher),每個通道分發器的所有終結點分發器(EndpointDispatcher),以及每個終結點分發器對應的分發執行時(DispatchRuntime)的所有執行時操作(DispatchOperation)。最後將初始化的CallContextInitializer新增到操作的CallContextInitializer列表中。

步驟三、使用服務行為進行授權

由於上面定義的服務行為ServiceAuthorizationBehaviorAttribute是一個自定義特性,所以我們可以直接將其應用到服務型別上。我們直接採用《基於Windows使用者組的授權方式[下篇]》的例子。如下所示,在服務型別CalculatorService上應用了ServiceAuthorizationBehaviorAttribute特性,並採用了UseWindowsGroups安全主體許可權模式。

   1: [ServiceAuthorizationBehavior(PrincipalPermissionMode.UseWindowsGroups)]
   2: public class CalculatorService : ICalculator
   3: {
   4:     [PrincipalPermission(SecurityAction.Demand, Role = "Administrators")]
   5:     public double Add(double x, double y)
   6:     {
   7:         return x + y;
   8:     }
   9: }

為了證明我們自定義的服務行為也能和ServiceAuthorizationBehavior一樣實現正確的授權,我們需要將ServiceAuthorizationBehavior的授權功能關閉。為此我們修正了服務端的配置,將ServiceAuthorizationBehavior的PrincipalPermissionMode設定為None。

   1: <?xml version="1.0"?>
   2: <configuration>
   3:   <system.serviceModel>
   4:     <services>
   5:       <service name="Artech.WcfServices.Services.CalculatorService" behaviorConfiguration="disableAuthorization">
   6:         <endpoint address="http://127.0.0.1/calculatorservice" binding="ws2007HttpBinding" contract="Artech.WcfServices.Contracts.ICalculator"/>
   7:       </service>
   8:     </services>
   9:     <behaviors>
  10:       <serviceBehaviors>
  11:         <behavior  name="disableAuthorization">
  12:           <serviceAuthorization principalPermissionMode="None"/>
  13:         </behavior>
  14:       </serviceBehaviors>
  15:     </behaviors>
  16:   </system.serviceModel>
  17: </configuration>

而客戶端的服務呼叫程式中,依然是分別以Foo和Bar(Foo具有管理員許可權)的名義進行兩次服服務呼叫。由於兩個Windows帳號許可權的不同,同樣只有第一個服務呼叫能夠成功,這反映在最終的執行結果中。客戶端程式:

   1: ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>("calculatorService");
   2: NetworkCredential credential = channelFactory.Credentials.Windows.ClientCredential;
   3: credential.UserName = "Foo";
   4: credential.Password = "Password";
   5: ICalculator calculator = channelFactory.CreateChannel();
   6: Invoke(calculator);
   7:  
   8: channelFactory = new ChannelFactory<ICalculator>("calculatorService");
   9: credential = channelFactory.Credentials.Windows.ClientCredential;
  10: credential.UserName = "Bar";
  11: credential.Password = "Password";
  12: calculator = channelFactory.CreateChannel();
  13: Invoke(calculator);

輸出結果:

   1: 服務呼叫成功...
   2: 服務呼叫失敗...
作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的訊息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。


相關文章