閱讀389 返回首頁    go 技術社區[雲棲]


通過添加HTTP Header實現上下文數據在WCF的自動傳遞

多年之前,我寫了一篇通過WCF擴展實現上下文信息從客戶端自動傳遞到服務端的文章,其實現機製很簡單:將上下文信息存放到SOAP Header進行傳遞。那麼對於非SOAP消息的RESTful服務就不使用了。為了解決這個問題,我們可以將存放上下文信息的地方從SOAP Header替換成HTTP Header。這篇為你消息講述具體的實現[源代碼從這裏下載]。

目錄
一、 Ambient Context
二、ApplicationContext
三、創建ContextSender將上下文附加到請求消息的HTTP Header
四、創建ContextReceiver從請求消息中接收上下文
五、創建自定義終結點行為
六、如何使用ContextPropagationBehavior
七、看看HTTP請求消息的結構

在一個多層結構的應用中,我們需要傳遞一些上下文的信息在各層之間傳遞,比如:為了進行Audit,需要傳遞一些當前當前user profile的一些信息。在一些分布式的環境中也可能遇到context信息從client到server的傳遞。如何實現這種形式的Context信息的傳遞呢?我們有兩種方案:

  • 將Context作為參數傳遞:將context作為API的一部分,context的提供者在調用context接收者的API的時候顯式地設置這些Context信息,context的接收者則直接通過參數將context取出。這雖然能夠解決問題,但決不是一個好的解決方案,因為API應該隻和具體的業務邏輯有關,而context 一般是與非業務邏輯服務的,比如Audit、Logging等等。此外,將context納入API作為其一部分,將降低API的穩定性, 比如,今天隻需要當前user所在組織的信息,明天可能需求獲取當前客戶端的IP地址,你的API可以會經常變動,這顯然是不允許的。
  • 創建Ambient Context來保存這些context信息:Ambient Context可以在不同的層次之間、甚至是分布式環境中每個節點之間共享或者傳遞。比如在ASP.NET 應用中,我們通過SessionSate來存儲當前Session的信息;通過HttpContext來存儲當前Http request的信息。在非Web應用中,我們通過CallContext將context信息存儲在TLS(Thread Local Storage)中,當前線程下執行的所有代碼都可以訪問並設置這些context數據。

介於上麵所述,我創建一個名為ApplicationContext的Ambient Context容器,Application Context實際上是一個dictionary對象,通過key-value pair進行context元素的設置,通過key獲取相對應的context元素。Application Context通過CallContext實現,定義很簡單:

   1: public class ApplicationContext: Dictionary<string, string>
   2: {
   3:     public const string KeyOfApplicationContext = "__ApplicationContext";
   4:     private ApplicationContext()
   5:     { }
   6:     public static ApplicationContext Current
   7:     {
   8:         get
   9:         {
  10:             if (HttpContext.Current != null)
  11:             {
  12:                 if (HttpContext.Current.Session[KeyOfApplicationContext] == null)
  13:                 {
  14:                     HttpContext.Current.Session[KeyOfApplicationContext] = new ApplicationContext();
  15:                 }
  16:                 return (ApplicationContext)HttpContext.Current.Session[KeyOfApplicationContext];
  17:             }
  18:  
  19:             if (CallContext.GetData(KeyOfApplicationContext) == null)
  20:             {
  21:                 CallContext.SetData(KeyOfApplicationContext, new ApplicationContext());
  22:             }
  23:             return (ApplicationContext)CallContext.GetData(KeyOfApplicationContext);
  24:         }
  25:         set
  26:         {
  27:             CallContext.SetData("__ApplicationContext", value);
  28:         }
  29:     }
  30:  
  31:     public string Username
  32:     {
  33:         get{return this.GetContextValue("__UserName");}
  34:         set{this["__UserName"] = value;}
  35:     }
  36:     public string Department
  37:     {
  38:         get { return this.GetContextValue("__Department"); }
  39:         set { this["__Department"] = value; }
  40:     }
  41:     private string GetContextValue(string key)
  42:     {
  43:         if (this.ContainsKey(key))
  44:         {
  45:             return (string)this[key];
  46:         }
  47:         return string.Empty;
  48:     }
  49: }

ApplicationContext本質上是個字典,靜態屬性Current用於設置和獲取當前ApplicationContext。具體來說,根據應用類型的不同,我們分別將當前ApplicationContext存放在SessionState和CallContext中。而UserName和Department是為了編程方便而實現的兩個原生的上下文元素。需要注意的是:字典元素的Key均以字符串”__”作為前綴。

實現上下文從客戶端到服務端的自動傳遞需要解決兩個問題:客戶端將當前上下文附加到請求消息中,服務端則從請求消息獲取上下文信息並作為當前的上下文。對於前者,我創建了一個自定義的ClientMessageInspector:ContextSender。在BeforeSendRequest方法中,我們將所有上下文元素置於請求消息的HTTP Header之中。

   1: public class ContextSender: IClientMessageInspector
   2: {
   3:     public void AfterReceiveReply(ref Message reply, object correlationState) { }
   4:     public object BeforeSendRequest(ref Message request, IClientChannel channel)
   5:     {
   6:         HttpRequestMessageProperty requestProperty;
   7:         if (!request.Properties.Keys.Contains(HttpRequestMessageProperty.Name))
   8:         {
   9:             requestProperty = new HttpRequestMessageProperty();
  10:         }
  11:         else
  12:         { 
  13:             requestProperty = (HttpRequestMessageProperty)request.Properties[HttpRequestMessageProperty.Name];
  14:         }
  15:         foreach(var context in ApplicationContext.Current)
  16:         {
  17:             requestProperty.Headers.Add(context.Key, context.Value.ToString());
  18:         }
  19:         return null;
  20:     }
  21: }

對於服務端,請求消息的接收,以及對當前上下文的設定,實現在一個自定義CallContextInitializer中。該自定義CallContextInitializer起名為ContextReceiver,定義如下。而上下文的獲取和設置實現在BeforeInvoke方法中,確保在服務操作在執行的時候當前上下文信息已經存在。在這裏通過判斷Header名稱是否具有”__”前綴確實是否是基於上下文HTTP Header。

   1: public class ContextReceiver: ICallContextInitializer
   2: {
   3:     public void AfterInvoke(object correlationState) { }
   4:     public object BeforeInvoke(InstanceContext instanceContext, IClientChannel channel, Message message)
   5:     {
   6:         HttpRequestMessageProperty requestProperty = (HttpRequestMessageProperty)message.Properties[HttpRequestMessageProperty.Name];
   7:         foreach (string key in requestProperty.Headers.Keys)
   8:         { 
   9:             if(key.StartsWith("__"))
  10:             {
  11:                 ApplicationContext.Current[key] = requestProperty.Headers[key];
  12:             }
  13:         }            
  14:         return null;
  15:     }
  16: }

為了將上麵創建的兩個自定義對象,ContextSender和ContextReceiver,最終應用到WCF的消息處理運行時框架中,我們創建了如下所示的自定義的終結點行為:ContextPropagationBehavior。而ContextSender和ContextReceiver的應用分別實現在方法ApplyClientBehavior和ApplyDispatchBehavior方法中。

   1: public class ContextPropagationBehavior: IEndpointBehavior
   2: {
   3:     public void AddBindingParameters(ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters) { }
   4:     public void ApplyClientBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime)
   5:     {
   6:         clientRuntime.MessageInspectors.Add(new ContextSender());
   7:     }
   8:     public void ApplyDispatchBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher)
   9:     {
  10:         foreach (var operation in endpointDispatcher.DispatchRuntime.Operations)
  11:         {
  12:             operation.CallContextInitializers.Add(new ContextReceiver());
  13:         }
  14:     }
  15:     public void Validate(ServiceEndpoint endpoint) { }
  16: }

為了使ContextPropagationBehavior能夠需要通過配置的方式進行使用,我們定義它對應的BehaviorExtensionElement:ContextPropagationBehaviorElement。

   1: public class ContextPropagationBehaviorElement : BehaviorExtensionElement
   2: {
   3:     public override Type BehaviorType
   4:     {
   5:         get { return typeof(ContextPropagationBehavior); }
   6:     }
   7:  
   8:     protected override object CreateBehavior()
   9:     {
  10:         return new ContextPropagationBehavior();
  11:     }
  12: }
六、如何使用ContextPropagationBehavior

為了演示ContextPropagationBehavior的使用和證明該終結點行為真的具有上下文自動傳播的公用,我們創建一個簡單的WCF應用。下麵是服務契約的定義IContextTest,服務操作用於返回服務端當前的ApplicationContext。

   1: [ServiceContract]
   2: public interface IContextTest
   3: {
   4:     [OperationContract]
   5:     [WebGet]
   6:     ApplicationContext GetContext();
   7: }

而服務類型很簡單。

   1: public class ContextTestService : IContextTest
   2: {
   3:     public ApplicationContext GetContext()
   4:     {
   5:         return ApplicationContext.Current;
   6:     }
   7: }

假設我們采用自我寄宿的方式,我們創建的自定義終結點行為通過如下的配置應用到服務的終結點上。而從配置上我們也可以看到,我們並沒有采用基於SOAP的消息交換,而是采用JSON的消息編碼方式。

   1: <?xml version="1.0"?>
   2: <configuration>
   3:   <system.serviceModel>
   4:     <behaviors>
   5:       <endpointBehaviors>
   6:         <behavior name="contextPropagation">
   7:           <contextPropagation/>
   8:           <webHttp defaultOutgoingResponseFormat="Json" />
   9:         </behavior>
  10:       </endpointBehaviors>
  11:     </behaviors>
  12:     <extensions>
  13:       <behaviorExtensions>
  14:         <add name="contextPropagation" type="Artech.ContextPropagation.ContextPropagationBehaviorElement, 
  15: Artech.ContextPropagation.Lib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
  16:       </behaviorExtensions>
  17:     </extensions>
  18:     <services>
  19:       <service name="Service.ContextTestService">
  20:         <endpoint address="https://127.0.0.1/testService" behaviorConfiguration="contextPropagation"
  21:           binding="webHttpBinding" bindingConfiguration="" contract="Service.Interface.IContextTest" />
  22:       </service>
  23:     </services>
  24:   </system.serviceModel>
  25: </configuration>

下麵對客戶端進行服務調用的配置。

   1: <?xml version="1.0"?>
   2: <configuration>
   3:     <system.serviceModel>
   4:         <behaviors>
   5:             <endpointBehaviors>
   6:                 <behavior name="contextPropagation">
   7:                     <contextPropagation />
   8:                     <webHttp defaultOutgoingResponseFormat="Json" />
   9:                 </behavior>
  10:             </endpointBehaviors>
  11:         </behaviors>
  12:         <client>
  13:             <endpoint address=https://127.0.0.1/testservice
  14:                 behaviorConfiguration="contextPropagation" binding="webHttpBinding"
  15:                 contract="Service.Interface.IContextTest" name="contextTestService" />
  16:         </client>
  17:         <extensions>
  18:             <behaviorExtensions>
  19:                 <add name="contextPropagation" type="Artech.ContextPropagation.ContextPropagationBehaviorElement, Artech.ContextPropagation.Lib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
  20:             </behaviorExtensions>
  21:         </extensions>       
  22:     </system.serviceModel>
  23: </configuration>

客戶端使用如下的程序調用服務操作GetConext。在調用之前設置了當前上下文的UserName和Department,最終將從服務端獲取的ApplicationContext的所有元素打印出來,以驗證是否和客戶端的上下文是否一致。

   1: ApplicationContext.Current.Username = "Zhan San";
   2: ApplicationContext.Current.Department = "IT";
   3: using (ChannelFactory<IContextTest> channelFactory = new ChannelFactory<IContextTest>("contextTestService"))
   4: {
   5:     IContextTest proxy = channelFactory.CreateChannel();
   6:     ApplicationContext context = proxy.GetContext();
   7:     foreach (var item in context)
   8:     {
   9:         Console.WriteLine("{0,-20}:{1}", item.Key, item.Value);
  10:     }
  11: }

輸出結果充分地證明了客戶端設置的上下文被成功地傳播到了服務端。

   1: __UserName          :Zhan San
   2: __Department        :IT

為了更加清楚地證實客戶端設置的當前上下文是否存在於請求消息中,我們可以通過Fildder查看整個HTTP請求消息(你需要將IP地址127.0.0.1替換成你的主機名)。整個HTTP請求消息如下所示,從中我們可以清楚地看到兩個上下文項存在於HTTP Header列表中。

   1: GET https://jinnan-pc/testservice/GetContext HTTP/1.1
   2: Content-Type: application/xml; charset=utf-8
   3: __UserName: Zhan San
   4: __Department: IT
   5: Host: jinnan-pc
   6: Accept-Encoding: gzip, deflate
   7: Connection: Keep-Alive

最後需要指出一點的是:和SOAP Header的實現方式不同,這種方式采用明文的形式存儲,所以不要將敏感信息放在上下文中傳遞。


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

最後更新:2017-10-26 16:34:18

  上一篇:go  [WCF安全係列]通過綁定元素看各種綁定對消息保護的實現
  下一篇:go  [WCF權限控製]從兩個重要的概念談起:Identity與Principal[上篇]