首页 > 代码库 > 重温WCF之消息拦截与篡改(八)

重温WCF之消息拦截与篡改(八)

我们知道,在WCF中,客户端对服务操作方法的每一次调用,都可以被看作是一条消息,而且,可能我们还会有一个疑问:如何知道客户端与服务器通讯过程中,期间发送和接收的SOAP是什么样子。当然,也有人是通过借助其他工具来抓取数据包来查看。那,有没有办法让程序自己输出相应的SOAP信息呢?

要对SOAP消息进行拦截和修改,我们需要实现两个接口,它们都位于System.ServiceModel.Dispatcher (程序集System.ServiceModel)。下面分别价绍。

接口一:IClientMessageInspector

从名字中我们可以猜测,它是用来拦截客户消息的,而看看它的方法,你就更加肯定当初的猜测了。

  • BeforeSendRequest:向服务器发送请求前拦截或修改消息(事前控制)
  • AfterReceiveReply:接收到服务器的回复消息后,在调用返回之前拦截或修改消息(事后诸葛亮)

接口二:IDispatchMessageInspector

刚才那个接口是针对客户端的,而这个是针对服务器端的。

  • AfterReceiveRequest:接收客户端请求后,在进入操作处理代码之前拦截或修改消息(欺上)
  • BeforeSendReply:服务器向客户端发送回复消息之前拦截和修改消息(瞒下)。

虽然实现了这两个接口,但你会有新的疑问,怎么用?把它们放到哪儿才能拦截消息?因此,下一步就是要实现IEndpointBehavior按口(System.ServiceModel.Description命名空间,程序集System.ServiceModel),它有四个方法,而我们只需要处理两个就够了

 

新建一个类库应用,然后添加System.ServiceModel程序集的引用

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using System.ServiceModel;
using System.ServiceModel.Dispatcher;
using System.ServiceModel.Description;
using System.ServiceModel.Channels;

namespace MyLib
{
    /// <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;
        }
    }

}

这一步,我们先建立服务器端。

记得要引用我们刚才写的类库。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using System.Runtime;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Description;

namespace WCFServer
{
    class Program
    {
        static void Main(string[] args)
        {
       using (ServiceHost host = new ServiceHost(typeof (Service)))
            {
                WSHttpBinding bingding = new WSHttpBinding();

                host.AddServiceEndpoint(typeof (IService), bingding, "http://127.0.0.1:8888/service1");


                foreach (var endpoint in host.Description.Endpoints)
                {
                    endpoint.EndpointBehaviors.Add(new MyLib.MyEndPointBehavior());
                }

                ServiceMetadataBehavior behavior = new ServiceMetadataBehavior();
                behavior.HttpGetEnabled = true;
                behavior.HttpGetUrl = new Uri("http://127.0.0.1:8888/service"); //httpGetUrl客户端引用的地址
                host.Description.Behaviors.Add(behavior);
                host.Opened += delegate
                {
                    Console.WriteLine("服务已启动");
                    Console.ReadKey();
                };
                host.Open();
            }
        }
    }

    [ServiceContract(Namespace = "MyNamespace")]
    public interface IService
    {
        [OperationContract]
        int AddInt(int a, int b);
        [OperationContract]
        Student GetStudent();
        [OperationContract]
        CalResultResponse ComputingNumbers(CalcultRequest inMsg);
    }

    [ServiceBehavior(IncludeExceptionDetailInFaults = true)]
    public class MyService : IService
    {
        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("运算操作只允许加、减、乘、除。");
                    break;
            }
            return rmsg;
        }
    }

    [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;
    }
}

接下来,实现客户端。

a、引用刚才写的类库MyLib;

b、引用WCF服务。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace WCFClient
{
    class Program
    {
        static void Main(string[] args)
        {
            WS.ServiceClient client = new WS.ServiceClient();
            // 记得在客户端也要插入IEndPointBehavior
            client.Endpoint.EndpointBehaviors.Add(new MyLib.MyEndPointBehavior());
            try
            {
                // 1、调用带元数据参数和返回值的操作
                Console.WriteLine("\n20和35相加的结果是:{0}", client.AddInt(20, 35));
                // 2、调用带有数据协定的操作
                WS.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();
        }
    }
}

结果:

知道了如何拦截消息,那么修改消息就不难了。

现在我们把前面写的类库MyLib。

将消息拦截器MyMessageInspector作如下修改:

    /// <summary>
    ///  消息拦截器
    /// </summary>
    public class MyMessageInspector:IClientMessageInspector,IDispatchMessageInspector
    {
        void IClientMessageInspector.AfterReceiveReply(ref Message reply, object correlationState)
        {
            //Console.WriteLine("客户端接收到的回复:\n{0}", reply.ToString());
            return;
        }

        object IClientMessageInspector.BeforeSendRequest(ref Message request, IClientChannel channel)
        {
            //Console.WriteLine("客户端发送请求前的SOAP消息:\n{0}", request.ToString());
            // 插入验证信息
            MessageHeader hdUserName = MessageHeader.CreateHeader("u", "fuck", "admin");
            MessageHeader hdPassWord = MessageHeader.CreateHeader("p", "fuck", "123");
            request.Headers.Add(hdUserName);
            request.Headers.Add(hdPassWord);
            return null;
        }

        object IDispatchMessageInspector.AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
        {
            //Console.WriteLine("服务器端:接收到的请求:\n{0}", request.ToString());
            // 栓查验证信息
            string un = request.Headers.GetHeader<string>("u", "fuck");
            string ps = request.Headers.GetHeader<string>("p", "fuck");
            if (un == "admin" && ps == "abcd")
            {
                Console.WriteLine("用户名和密码正确。");
            }
            else
            {
                throw new Exception("验证失败,滚吧!");
            }
            return null;
        }

        void IDispatchMessageInspector.BeforeSendReply(ref Message reply, object correlationState)
        {
            //Console.WriteLine("服务器即将作出以下回复:\n{0}", reply.ToString());
            return;
        }
    }

注意:添加对System.Runtime.Serialization的引用。

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


现在重新生成一下项目,再试试。

前面我们说过,如果安装证书进行身份验证会相当TMD麻烦,而可以通过修改SOAP消息头来验证,但是,上次的做法会有一个麻烦,那就是每次调用操作协定都要手动修改一次,这一次,我们直接在终结点级别进行修改和验证,就省去了许多功夫。