博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
通过添加HTTP Header实现上下文数据在WCF的自动传递
阅读量:6986 次
发布时间:2019-06-27

本文共 9547 字,大约阅读时间需要 31 分钟。

多年之前,我写了一篇的文章,其实现机制很简单:将上下文信息存放到SOAP Header进行传递。那么对于非SOAP消息的RESTful服务就不使用了。为了解决这个问题,我们可以将存放上下文信息的地方从SOAP Header替换成HTTP Header。这篇为你消息讲述具体的实现[源代码从下载]。

目录

一、 Ambient Context
二、ApplicationContext
三、创建ContextSender将上下文附加到请求消息的HTTP Header
四、创建ContextReceiver从请求消息中接收上下文
五、创建自定义终结点行为
六、如何使用ContextPropagationBehavior
七、看看HTTP请求消息的结构

一、 Ambient Context

在一个多层结构的应用中,我们需要传递一些上下文的信息在各层之间传递,比如:为了进行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

介于上面所述,我创建一个名为ApplicationContext的Ambient Context容器,Application Context实际上是一个dictionary对象,通过key-value pair进行context元素的设置,通过key获取相对应的context元素。Application Context通过CallContext实现,定义很简单:

1: public class ApplicationContext: Dictionary
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均以字符串”__”作为前缀。

三、创建ContextSender将上下文附加到请求消息的HTTP Header

实现上下文从客户端到服务端的自动传递需要解决两个问题:客户端将当前上下文附加到请求消息中,服务端则从请求消息获取上下文信息并作为当前的上下文。对于前者,我创建了一个自定义的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: }

四、创建ContextReceiver从请求消息中接收上下文

对于服务端,请求消息的接收,以及对当前上下文的设定,实现在一个自定义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: 
2: 
3:   
4:     
5:       
6:         
7:           
8:           
9:         
10:       
11:     
12:     
13:       
14:         
15: Artech.ContextPropagation.Lib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
16:       
17:     
18:     
19:       
20:         
21:           binding="webHttpBinding" bindingConfiguration="" contract="Service.Interface.IContextTest" />
22:       
23:     
24:   
25: 

下面对客户端进行服务调用的配置。

1: 
2: 
3:     
4:         
5:             
6:                 
7:                     
8:                     
9:                 
10:             
11:         
12:         
13:             
14:                 behaviorConfiguration="contextPropagation" binding="webHttpBinding"
15:                 contract="Service.Interface.IContextTest" name="contextTestService" />
16:         
17:         
18:             
19:                 
20:             
21:         
22:     
23: 

客户端使用如下的程序调用服务操作GetConext。在调用之前设置了当前上下文的UserName和Department,最终将从服务端获取的ApplicationContext的所有元素打印出来,以验证是否和客户端的上下文是否一致。

1: ApplicationContext.Current.Username = "Zhan San";
2: ApplicationContext.Current.Department = "IT";
3: using (ChannelFactory
channelFactory = new ChannelFactory
("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

七、看看HTTP请求消息的结构

为了更加清楚地证实客户端设置的当前上下文是否存在于请求消息中,我们可以通过Fildder查看整个HTTP请求消息(你需要将IP地址127.0.0.1替换成你的主机名)。整个HTTP请求消息如下所示,从中我们可以清楚地看到两个上下文项存在于HTTP Header列表中。

1: GET http://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
微博:
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号
蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
你可能感兴趣的文章
Unity查找子物体的方式-怎么查找GameObject
查看>>
大数据学习系列之三 ----- HBase Java Api 图文详解
查看>>
cookie和session
查看>>
关于前端复用的构思
查看>>
微信小程序连接本地接口(转)
查看>>
小白的正则表达式学习之旅-02
查看>>
学习C语言必须知道的理论知识(第三章-数据类型的分类)
查看>>
hdu 素数环
查看>>
H3C CAS 介绍 & 基本概念
查看>>
xxx
查看>>
openSUSE 安装 Caffe
查看>>
你可能没注意的CSS单位
查看>>
咱计算机专业的人,能不能不那么特别地彰显对语文的无知?——再谈面向对象......
查看>>
foreach Transform 同时chils.setParent引起的bug
查看>>
AES加密--适用于RC2、RC4和Blowfish
查看>>
如何强制删除一个apk
查看>>
SHA算法摘要处理
查看>>
[HEOI2012]采花 BZOJ2743
查看>>
Codevs 3305 水果姐逛水果街Ⅱ 倍增LCA
查看>>
【智力题】程序员面试经典
查看>>