基本概念

消息拦截与篡改

我们知道,在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
{
    /// <summary>  
    ///  消息拦截器  
    /// </summary>  
    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());
        }
    }

    /// <summary>  
    /// 插入到终结点的Behavior  
    /// </summary>  
    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消息:
<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:s="http://www.w
3.org/2003/05/soap-envelope">
  <s:Header>
    <a:Action s:mustUnderstand="1">MyNamespace/IMessageService/AddInt</a:Action>

    <a:MessageID>urn:uuid:6bbd15c8-dd25-463d-b5d5-4a0460213435</a:MessageID>
    <a:ReplyTo>
      <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
    </a:ReplyTo>
  </s:Header>
  <s:Body>
    <AddInt xmlns="MyNamespace">
      <a>20</a>
      <b>35</b>
    </AddInt>
  </s:Body>
</s:Envelope>
客户端接收到的回复:
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://ww
w.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oas
is-200401-wss-wssecurity-utility-1.0.xsd">
  <s:Header>
    <a:Action s:mustUnderstand="1" u:Id="_2">MyNamespace/IMessageService/AddIntR
esponse</a:Action>
    <a:RelatesTo u:Id="_3">urn:uuid:6bbd15c8-dd25-463d-b5d5-4a0460213435</a:Rela
tesTo>
    <o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/200
4/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
      <u:Timestamp u:Id="uuid-6a634229-ab79-46c4-8d91-4b164993b2fa-17">
        <u:Created>2017-04-25T01:36:45.001Z</u:Created>
        <u:Expires>2017-04-25T01:41:45.001Z</u:Expires>
      </u:Timestamp>
      <c:DerivedKeyToken u:Id="uuid-6a634229-ab79-46c4-8d91-4b164993b2fa-15" xml
ns:c="http://schemas.xmlsoap.org/ws/2005/02/sc">
        <o:SecurityTokenReference>
          <o:Reference URI="urn:uuid:082d5f66-ea4c-4f40-82d1-e3c12b7239e0" Value
Type="http://schemas.xmlsoap.org/ws/2005/02/sc/sct" />
        </o:SecurityTokenReference>
        <c:Offset>0</c:Offset>
        <c:Length>24</c:Length>
        <c:Nonce>UvBWBKyPBtJKBssD5Q+nPg==</c:Nonce>
      </c:DerivedKeyToken>
      <c:DerivedKeyToken u:Id="uuid-6a634229-ab79-46c4-8d91-4b164993b2fa-16" xml
ns:c="http://schemas.xmlsoap.org/ws/2005/02/sc">
        <o:SecurityTokenReference>
          <o:Reference URI="urn:uuid:082d5f66-ea4c-4f40-82d1-e3c12b7239e0" Value
Type="http://schemas.xmlsoap.org/ws/2005/02/sc/sct" />
        </o:SecurityTokenReference>
        <c:Nonce>Y5C0z6cpxgr4ZSSbaq/VsQ==</c:Nonce>
      </c:DerivedKeyToken>
      <e:ReferenceList xmlns:e="http://www.w3.org/2001/04/xmlenc#">
        <e:DataReference URI="#_1" />
        <e:DataReference URI="#_4" />
      </e:ReferenceList>
      <e:EncryptedData Id="_4" Type="http://www.w3.org/2001/04/xmlenc#Element" x
mlns:e="http://www.w3.org/2001/04/xmlenc#">
        <e:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes256-c
bc" />
        <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
          <o:SecurityTokenReference>
            <o:Reference ValueType="http://schemas.xmlsoap.org/ws/2005/02/sc/dk"
 URI="#uuid-6a634229-ab79-46c4-8d91-4b164993b2fa-16" />
          </o:SecurityTokenReference>
        </KeyInfo>
        <e:CipherData>
          <e:CipherValue>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</e:CipherValue>
        </e:CipherData>
      </e:EncryptedData>
    </o:Security>
  </s:Header>
  <s:Body u:Id="_0">
    <AddIntResponse xmlns="MyNamespace">
      <AddIntResult>55</AddIntResult>
    </AddIntResponse>
  </s:Body>
</s:Envelope>

20和35相加的结果是:55

Modify

其实当你可以看到内容时,修改他还会远吗?

一、修改 DLL

注意:在DLL中引入System.Runtime.Serialization

创建消息头时,第一个参数是名字,如上面的“u”,第二个参数是命名空间,这个可以自己来定义,比如上面的“valid”,第三个参数就是消息头的内容。

/// <summary>  
///  消息拦截器  
/// </summary>  
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<string>("u", "valid");
        string password = request.Headers.GetHeader<string>("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

每一次调用,都会进行身份验证。

服务已启动。
用户名和密码正确。
用户名和密码正确。
用户名和密码正确。