通过添加HTTP Header实现上下文数据在WCF的自动传递

多年之前,我写了一篇通过WCF扩展实现上下文信息从客户端自动传递到服务端的文章,其实现机制很简单:将上下文信息存放到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<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均以字符串”__”作为前缀。

三、创建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: <?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="http://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=http://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

七、看看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
微博:www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

原文链接

时间: 2025-01-19 15:01:26

通过添加HTTP Header实现上下文数据在WCF的自动传递的相关文章

InnoDB 中文参考手册 --- 5 添加与移除 InnoDB 数据和日志文件

参考|参考手册|数据|中文 InnoDB 中文参考手册 --- 犬犬(心帆)翻译 5 添加与移除 InnoDB 数据和日志文件为了添加一个数据文件到表空间中,首先要关闭 MySQL 数据库,编辑 my.cnf 文件,在 innodb_data_file_path 中添加一个新文件,然后再重新启动服务. 如果,最后一个文件以关键字 autoextend 来描述,那么编辑 my.cnf 的过程如下所示.必须检查最后一个文件的尺寸,并使它向下接近于 1024 * 1024 bytes (= 1 MB)

jquery ajax实现input输入框输入,点击添加无跳转提交数据

问题描述 jquery ajax实现input输入框输入,点击添加无跳转提交数据 jquery ajax实现input输入框输入数据,点击添加按钮无跳转提交数据 解决方案 看一下ajax的例子http://jun1986.iteye.com/blog/1399242 解决方案二: 这个网上例子很多,自己搜看看 解决方案三: 一点不会jq,js跟ajax 解决方案四: 把代码拿出来看看啊,看看你是怎么写的 解决方案五: 肯定是js里面的ajax提交代码有错误

java-怎样在easyui中的datagrid的列中添加combobox并动态获取数据到combobox中?

问题描述 怎样在easyui中的datagrid的列中添加combobox并动态获取数据到combobox中? <table id="data" class="easyui-datagrid" style="width:700px;height:250px" url="getAllServer.do" data-options="pageSize:5 ,pageList: [3,5,10]" tool

oracle-用C#.net给代码添加右键屏蔽菜单,把数据添加到Oracle然后发布成网页的时候没有右键菜单

问题描述 用C#.net给代码添加右键屏蔽菜单,把数据添加到Oracle然后发布成网页的时候没有右键菜单 具体要求是在项目里面添加屏蔽代码,然后把编译的数据发送到Oracle里面去,再用Oracle里面的plsqldev.exe发布成网页让他们没有右键菜单 这个应该怎么完成??? 这个是图片 解决方案 屏蔽右键菜单是通过js控制的,在你的asp.net页面加上代码.http://www.cnblogs.com/top5/archive/2012/03/24/2415993.html 解决方案二:

winfrom datagridview 控件中 如何在修改、添加和删除后刷新数据?

问题描述 winfrom datagridview 控件中 如何在修改.添加和删除后刷新数据? winfrom datagridview 如何在修改.添加和删除之后如何刷新? 解决方案 http://blog.163.com/huang_ying_lu/blog/static/2699983201052971836350/ 解决方案二: datagridview.Remove这是自动删除一行,应该还有其他属性. 解决方案三: 你是在form上直接进行datagridview修改,删除,添加操作吗

项目中树形结构的添加与立即删除该数据问题

    立即添加是可以的,但是想把刚添加的那条数据删除就不行了.得不到数据的id值:   处理方法:我写了一个sql语句,在添加之后,把数据中最大的id值取出来,添加在节点上,这样就可以保证立即添加的数据,就可以立即删除了.     $.ajax({ type: 'POST', url: '/yxt-admin/admin/address/insert', data:{ name:name, pid:treeNode.id, codeValue:$('#code_value').textbox(

sql数据库 当一张表的内容添加一条时 写一个触发器 使其自动再生成一张

问题描述 sql数据库 当一张表的内容添加一条时 写一个触发器 使其自动再生成一张 1C sql数据库 当一张表的内容添加一条时 写一个触发器 使其自动再生成一张表 但自动生成的表的名字需要是添加的这个表的一个字段的内容 比如说表中添加了一个成员的名字 需要自动生成的表的名字就是这个人名 解决方案 需求可以实现,但没有这么去做的,这个表里有10000条数据时,你生成1万张表???? 建议修改设计,改成其它的方案来实现 解决方案二: Sql Server数据库新增触发器里面可以通过select (

ist取数据-从List中取数据MP3文件并自动播放

问题描述 从List中取数据MP3文件并自动播放 我做一个MP3项目,没有列表的,只有一个播放界面.点进去就开始播放,现在我想要实现我点进去就随机播放我存在List集合中的一首歌曲!我已经存好了 在List中.具体要怎么实现. 解决方案 var currentItem=list.OrderBy(x=>Guid.NewGuid()).First(); 随机排序后取第一个即可 解决方案二: var currentItem=list[new Random().Next(0, list.Count)];

dreamweaver 邮件-为什么在Dreamweaver中添加邮件链接,页面预览中点链接会自动跳转到QQ邮箱?

问题描述 为什么在Dreamweaver中添加邮件链接,页面预览中点链接会自动跳转到QQ邮箱? 代码是联系我们 解决方案 你可以用下面的代码代替: 联系我们 解决方案二: (a href="mailto:zss_emeil[at]126.com")(/a) 把括号变成<>