WCF-09-message intercept, modify
基本概念
我们知道,在WCF中,客户端对服务操作方法的每一次调用,都可以被看作是一条消息。
可能我们还会有一个疑问:如何知道客户端与服务器通讯过程中,期间发送和接收的SOAP是什么样子。
当然,也有人是通过借助其他工具来抓取数据包来查看。那,有没有办法让程序自己输出相应的SOAP信息呢?当然有,正式本文讨论主题。
说到消息拦截,这个你肯定可以理解,如果你不懂,你可以想一想电话窃听程序,我在你的手机上植入一种木马,可以截取你和MM的通话内容,其实这就是消息拦截。
WCF相关的API比较难寻找,我当初也找了N久,现在,我直接把思路和方法告诉各位,也免得大家太辛苦。
要对SOAP消息进行拦截和修改,我们需要实现两个接口,它们都位于 System.ServiceModel.Dispatcher
(程序集System.ServiceModel)。下面分别价绍。
接口一:IClientMessageInspector
从名字中我们可以猜测,它是用来拦截客户消息的,而看看它的方法,你就更加肯定当初的猜测了。
BeforeSendRequest:向服务器发送请求前拦截或修改消息(事前控制)
AfterReceiveReply:接收到服务器的回复消息后,在调用返回之前拦截或修改消息(事后诸葛亮)
接口二:IDispatchMessageInspector
刚才那个接口是针对客户端的,而这个是针对服务器端的。
AfterReceiveRequest:接收客户端请求后,在进入操作处理代码之前拦截或修改消息(欺上)
BeforeSendReply:服务器向客户端发送回复消息之前拦截和修改消息(瞒下)。
虽然实现了这两个接口,但你会有新的疑问,怎么用?把它们放到哪儿才能拦截消息?
因此,下一步就是要实现 IEndpointBehavior
接口(System.ServiceModel.Description命名空间,程序集System.ServiceModel),它有四个方法,而我们只需要处理两个就够了。
下面是 MSDN 的翻译版本说明:
使用 ApplyClientBehavior 方法可以在客户端应用程序中修改、检查或插入对终结点中的扩展。
使用 ApplyDispatchBehavior 方法可以在服务应用程序中修改、检查或插入对终结点范围执行的扩展。
说白了就是一个在客户拦截和修改消息,另一个在服务器端拦截和修改消息。
在实现这两个方法时,和前面我们实现的 IClientMessageInspector 和 IDispatchMessageInspector 联系起来就OK了。
做完了 IEndpointBehavior 的事情后,把它插入到服务终结点中就行了,无论是服务器端还是客户端,这一步都必须的,因为我们实现的拦截器是包括两个端的,
因此,较好的做法是把这些类写到一个独立的类库(dll)中,这样一来,服务器端和客户端都可以引用它。详见后面的示例。
Simple Demo
一、创建一个DLL
引入 System.ServiceModel
- MessageLib.cs
namespace MessageLib
{
///
/// 消息拦截器
///
public class MyMessageInspector : IClientMessageInspector, IDispatchMessageInspector
{
void IClientMessageInspector.AfterReceiveReply(ref Message reply, object correlationState)
{
Console.WriteLine("客户端接收到的回复:\n{0}", reply.ToString());
}
object IClientMessageInspector.BeforeSendRequest(ref Message request, IClientChannel channel)
{
Console.WriteLine("客户端发送请求前的SOAP消息:\n{0}", request.ToString());
return null;
}
object IDispatchMessageInspector.AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
{
Console.WriteLine("服务器端:接收到的请求:\n{0}", request.ToString());
return null;
}
void IDispatchMessageInspector.BeforeSendReply(ref Message reply, object correlationState)
{
Console.WriteLine("服务器即将作出以下回复:\n{0}", reply.ToString());
}
}
///
/// 插入到终结点的Behavior
///
public class MyEndPointBehavior : IEndpointBehavior
{
public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
{
// 不需要
return;
}
public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
// 植入“偷听器”客户端
clientRuntime.ClientMessageInspectors.Add(new MyMessageInspector());
}
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
{
// 植入“偷听器” 服务器端
endpointDispatcher.DispatchRuntime.MessageInspectors.Add(new MyMessageInspector());
}
public void Validate(ServiceEndpoint endpoint)
{
// 不需要
return;
}
}
}
二、Server
引入一创建的DLL。
- MessageModel.cs
namespace Server.Model
{
[DataContract]
public class Student
{
[DataMember]
public string StudentName;
[DataMember]
public int StudentAge;
}
[MessageContract]
public class CalcultRequest
{
[MessageHeader]
public string Operation;
[MessageBodyMember]
public int NumberA;
[MessageBodyMember]
public int NumberB;
}
[MessageContract]
public class CalResultResponse
{
[MessageBodyMember]
public int ComputedResult;
}
}
- MessageService.cs
namespace Server.Service
{
[ServiceContract(Namespace = "MyNamespace")]
public interface IMessageService
{
[OperationContract]
int AddInt(int a, int b);
[OperationContract]
Student GetStudent();
[OperationContract]
CalResultResponse ComputingNumbers(CalcultRequest inMsg);
}
[ServiceBehavior(IncludeExceptionDetailInFaults = true)]
public class MessageService : IMessageService
{
public int AddInt(int a, int b)
{
return a + b;
}
public Student GetStudent()
{
Student stu = new Student();
stu.StudentName = "小明";
stu.StudentAge = 22;
return stu;
}
public CalResultResponse ComputingNumbers(CalcultRequest inMsg)
{
CalResultResponse rmsg = new CalResultResponse();
switch (inMsg.Operation)
{
case "加":
rmsg.ComputedResult = inMsg.NumberA + inMsg.NumberB;
break;
case "减":
rmsg.ComputedResult = inMsg.NumberA - inMsg.NumberB;
break;
case "乘":
rmsg.ComputedResult = inMsg.NumberA * inMsg.NumberB;
break;
case "除":
rmsg.ComputedResult = inMsg.NumberA / inMsg.NumberB;
break;
default:
throw new ArgumentException("运算操作只允许加、减、乘、除。");
}
return rmsg;
}
}
}
- Main()
static void Main(string[] args)
{
// 服务器基址
Uri baseAddress = new Uri("http://127.0.0.1:18080/services");
// 声明服务器主机
using (ServiceHost host = new ServiceHost(typeof(MessageService), baseAddress))
{
// 添加绑定和终结点
WSHttpBinding binding = new WSHttpBinding();
host.AddServiceEndpoint(typeof(IMessageService), binding, "/test");
// 添加服务描述
host.Description.Behaviors.Add(new ServiceMetadataBehavior { HttpGetEnabled = true });
// 把自定义的IEndPointBehavior插入到终结点中
foreach (var endpont in host.Description.Endpoints)
{
endpont.EndpointBehaviors.Add(new MessageLib.MyEndPointBehavior());
}
try
{
// 打开服务
host.Open();
Console.WriteLine("服务已启动。");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
Console.ReadKey();
}
}
三、Client
引入一创建的DLL、Service 服务。
- Main()
static void Main(string[] args)
{
MS.MessageServiceClient client = new MS.MessageServiceClient();
// 记得在客户端也要插入IEndPointBehavior
client.Endpoint.EndpointBehaviors.Add(new MessageLib.MyEndPointBehavior());
try
{
// 1、调用带元数据参数和返回值的操作
Console.WriteLine("\n20和35相加的结果是:{0}", client.AddInt(20, 35));
// 2、调用带有数据协定的操作
MS.Student student = client.GetStudent();
Console.WriteLine("\n学生信息---------------------------");
Console.WriteLine("姓名:{0}\n年龄:{1}", student.StudentName, student.StudentAge);
// 3、调用带消息协定的操作
Console.WriteLine("\n15乘以70的结果是:{0}", client.ComputingNumbers("乘", 15, 70));
}
catch (Exception ex)
{
Console.WriteLine("异常:{0}", ex.Message);
}
client.Close();
Console.ReadKey();
}
Main()方法中测试了三种类型。打印的LOG较多。此处选取1、调用带元数据参数和返回值的操作如下:
(其他如有兴趣,请自行测试)
客户端发送请求前的SOAP消息:
MyNamespace/IMessageService/AddInt
urn:uuid:6bbd15c8-dd25-463d-b5d5-4a0460213435
http://www.w3.org/2005/08/addressing/anonymous
20
35
客户端接收到的回复:
MyNamespace/IMessageService/AddIntR
esponse
urn:uuid:6bbd15c8-dd25-463d-b5d5-4a0460213435
2017-04-25T01:36:45.001Z
2017-04-25T01:41:45.001Z
0
24
UvBWBKyPBtJKBssD5Q+nPg==
Y5C0z6cpxgr4ZSSbaq/VsQ==
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
55
20和35相加的结果是:55
Modify
其实当你可以看到内容时,修改他还会远吗?
一、修改 DLL
注意:在DLL中引入System.Runtime.Serialization
创建消息头时,第一个参数是名字,如上面的“u”,第二个参数是命名空间,这个可以自己来定义,比如上面的“valid”,第三个参数就是消息头的内容。
///
/// 消息拦截器
///
public class MyMessageInspector : IClientMessageInspector, IDispatchMessageInspector
{
void IClientMessageInspector.AfterReceiveReply(ref Message reply, object correlationState)
{
//Console.WriteLine("客户端接收到的回复:\n{0}", reply.ToString());
}
object IClientMessageInspector.BeforeSendRequest(ref Message request, IClientChannel channel)
{
//Console.WriteLine("客户端发送请求前的SOAP消息:\n{0}", request.ToString());
// 插入验证信息
MessageHeader username = MessageHeader.CreateHeader("u", "valid", "admin");
MessageHeader password = MessageHeader.CreateHeader("p", "valid", "123");
request.Headers.Add(username);
request.Headers.Add(password);
return null;
}
object IDispatchMessageInspector.AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
{
//Console.WriteLine("服务器端:接收到的请求:\n{0}", request.ToString());
// 检查验证信息
string username = request.Headers.GetHeader("u", "valid");
string password = request.Headers.GetHeader("p", "valid");
if ("admin".Equals(username) && "123".Equals(password))
{
Console.WriteLine("用户名和密码正确。");
}
else
{
throw new Exception("验证失败,请重新输入!");
}
return null;
}
void IDispatchMessageInspector.BeforeSendReply(ref Message reply, object correlationState)
{
//Console.WriteLine("服务器即将作出以下回复:\n{0}", reply.ToString());
}
}
二、调用
重新编译调用。
- Client
20和35相加的结果是:55
学生信息---------------------------
姓名:小明
年龄:22
15乘以70的结果是:1050
- Server
每一次调用,都会进行身份验证。
服务已启动。
用户名和密码正确。
用户名和密码正确。
用户名和密码正确。