[WCF] 透過實作MessageInspector進行訊息的加/解密

以WCF實作的Web Service是不是也能像ASP.Net Web Service一樣,可以在訊息傳遞的過程中加上加密保護的功能呢? 答案當然是可以的....

 

[Web Service] 透過SOAP Extension進行訊息的加/解密系列文中已經簡單的介紹過了透過SOAP Extension來達到在傳送資料的過程中套上加密保護的ASP.Net Web Service與Client端的實作方式。

不過,隨著時代的進步,微軟也自.Net Framework 3.0推出之後,漸漸開始建議開發者們以WCF來實作Web Service。

那麼,以WCF實作的Web Service是不是也能像ASP.Net Web Service一樣,可以在訊息傳遞的過程中加上加密保護的功能呢? 答案當然是可以的。

讓我們來看看下面的範例(這個範例和[Web Service] 透過SOAP Extension進行訊息的加/解密系列文中的範例相同,只是服務端改以WCF來實作):

image

在未加上加解密功能之前,透過WPF開發的Client端一樣可以輕鬆的WCF實作的服務端取得資料。

上面這個範例的程式碼在此:

 

接下來,我們一樣要分別對WCF服務端和WPF實作的客戶端加上些東西,以讓資料在傳輸的過程中是有加密保護的。

與ASP.Net WebService不同之處是:在WCF中並不像ASP.Net WebService一樣有SOAP Extension讓我們使用,而是提供了IDispatchMessageInspectorIClientMessageInspector這兩個擴充介面分別供服務端與客戶端使用。

(這次的範例程式為了要讓服務端與客戶端可以共用相同的元素,因此我將可以共用的資料型態、加解密方法以及相關類別抽到共用的專案中以便參考。)

由於本範例中服務端與客戶端的加解密功能是一致的,所以我們可以先從訊息加解密訊息的部份下手:

MessageInspectorHelper.cs

using System.Configuration;
using System.IO;
using System.Linq;
using System.ServiceModel.Channels;
using System.Xml;
using System.Xml.Linq;

namespace MySimpleMessageUtility
{
    public static class MessageInspectorHelper
    {
        private static readonly string Key = ConfigurationManager.AppSettings[ "EncryptorKey" ];
        private static readonly string Iv = ConfigurationManager.AppSettings[ "EncryptorIV" ];

        private static readonly XNamespace SoapNamespace = "http://schemas.xmlsoap.org/soap/envelope/";

        private const string RootElementName = "MySoapMessage";

        public static object DecryptMessage( ref Message message )
        {
            XDocument xDocument = XDocument.Parse( message.ToString() );

            if( xDocument.Descendants( RootElementName ).Count() == 1 )
            {
                XElement mySoapMessageElement = xDocument.Descendants( RootElementName ).First();

                //取出Message屬性中的值,以供解密
                MySoapMessage mySoapMessage = new MySoapMessage
                {
                    Message = mySoapMessageElement.Element( "Message" ).Value ,
                };

                string descryptedSoapBody = Encryptor.DecryptAes( mySoapMessage.Message , Key , Iv );

                //取出Body節點內容,以供替換
                XElement soapBodyElement = xDocument.Descendants( SoapNamespace + "Body" ).First();

                //移除原來Body中的內容
                soapBodyElement.Elements().Remove();

                //取出解密後的真實資料內容
                XElement bodyForReplace = XElement.Parse( descryptedSoapBody );

                //將解密後的值塞回Body節點中
                soapBodyElement.Add( bodyForReplace );

                MemoryStream memoryStream = new MemoryStream();
                XmlWriter xmlWriter = XmlWriter.Create( memoryStream );

                memoryStream.Position = 0;

                xDocument.Save( xmlWriter );

                xmlWriter.Flush();
                xmlWriter.Close();

                //再次將memoryStream的位置歸0
                memoryStream.Position = 0;

                XmlDictionaryReader xmlDictionaryReader = XmlDictionaryReader.CreateTextReader( memoryStream , new XmlDictionaryReaderQuotas() );

                //產生新的Message以供替換
                Message replacedMessage = Message.CreateMessage( xmlDictionaryReader , int.MaxValue , message.Version );

                message = replacedMessage;
            }

            return null;
        }

        public static object EncryptMessage( ref Message message )
        {
            XDocument xDocument = XDocument.Parse( message.ToString() );

            XElement soapBodyElement = xDocument.Descendants( SoapNamespace + "Body" ).First();

            //將原來SoapBody的內容進行加密
            string encryptedString = Encryptor.EncryptAes( soapBodyElement.FirstNode.ToString() , Key , Iv );

            //將加密過的內容以MySoapMessage類別封裝
            MySoapMessage mySoapMessage = new MySoapMessage { Message = encryptedString };

            //移除SoapBody中原來的內容
            soapBodyElement.Elements().Remove();

            XElement bodyForReplace = SerializeHelper.Serialize( mySoapMessage ).Element( RootElementName );

            soapBodyElement.Add( bodyForReplace );

            MemoryStream memoryStream = new MemoryStream();

            XmlWriter xmlWriter = XmlWriter.Create( memoryStream );

            memoryStream.Position = 0;

            xDocument.Save( xmlWriter );

            xmlWriter.Flush();
            xmlWriter.Close();

            //再次將memoryStream的位置歸0
            memoryStream.Position = 0;

            XmlDictionaryReader xmlDictionaryReader = XmlDictionaryReader.CreateTextReader( memoryStream , new XmlDictionaryReaderQuotas() );

            //產生新的Message以供替換
            Message replacedMessage = Message.CreateMessage( xmlDictionaryReader , int.MaxValue , message.Version );

            message = replacedMessage;

            return null;
        }
    }
}

 

接著運用剛才完成的加解/密訊息功能來實作IDispatchMessageInspectorIClientMessageInspector兩個介面(這兩個介面中定義好的方法其實就與SOAP Extension中訊息的四個狀態非常類似,分別是服務端收到需求後的事件及送出回應前的事件;以及客戶端收到回應後的事件和送出需求前的事件):

 

MyMessageInspector.cs

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

namespace MySimpleMessageUtility
{
    public class MyMessageInspector : IDispatchMessageInspector , IClientMessageInspector
    {
        public object AfterReceiveRequest( ref Message request , System.ServiceModel.IClientChannel channel , System.ServiceModel.InstanceContext instanceContext )
        {
            return MessageInspectorHelper.DecryptMessage( ref request );
        }

        public void BeforeSendReply( ref Message reply , object correlationState )
        {
            MessageInspectorHelper.EncryptMessage( ref reply );
        }

        public void AfterReceiveReply( ref Message reply , object correlationState )
        {
            MessageInspectorHelper.DecryptMessage( ref reply );
        }

        public object BeforeSendRequest( ref Message request , System.ServiceModel.IClientChannel channel )
        {
            return MessageInspectorHelper.EncryptMessage( ref request );
        }

    }
}

 

WCF中的擴充功能得要藉助Behavior來進行設定,所以我們得再撰寫Behavior的相關程式碼。

以本次的範例來說,我希望將MessageInspector套用到我指定的EndPoint上(其實也只有開一個EndPoint啦~),所以我們就先從實作IEndpointBehavior介面的類別開始吧(就是透過它來針對服務端和客戶端分別指派要套用的MessageInspector的喔!!):

MyMessageProcessingBehavior.cs

using System.ServiceModel.Description;

namespace MySimpleMessageUtility
{
    public class MyMessageProcessingBehavior : IEndpointBehavior
    {
        public void AddBindingParameters( ServiceEndpoint endpoint , System.ServiceModel.Channels.BindingParameterCollection bindingParameters )
        {
        }

        public void ApplyClientBehavior( ServiceEndpoint endpoint , System.ServiceModel.Dispatcher.ClientRuntime clientRuntime )
        {
            clientRuntime.MessageInspectors.Add( new MyMessageInspector() );
        }

        public void ApplyDispatchBehavior( ServiceEndpoint endpoint , System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher )
        {
            endpointDispatcher.DispatchRuntime.MessageInspectors.Add( new MyMessageInspector() );
        }

        public void Validate( ServiceEndpoint endpoint )
        {
        }
    }
}

 

最後,為了讓我們可以直接透過修改服務端的web.config檔就可以將服務端套上我們指定的MessageInspector,所以我們還得多實作一個繼承BehaviorExtensionElement類別的類別(有點饒舌....):

 

MyMessageProcessingBehaviorExtension.cs

using System;
using System.ServiceModel.Configuration;

namespace MySimpleMessageUtility
{
    public class MyMessageProcessingBehaviorExtension : BehaviorExtensionElement
    {
        protected override object CreateBehavior()
        {
            return new MyMessageProcessingBehavior();
        }

        public override Type BehaviorType
        {
            get { return typeof( MyMessageProcessingBehavior ); }
        }
    }
}

 

如此一來,共用的元件庫部份就完成啦!!

最後的最後,我們只需要在服務端的web.config檔中的system.serviceModel區段裡加上些設定,就可以替服務端套上MessageInspector啦:

 

web.config

<system.serviceModel>
  <!--引用MyMessageProcessingBehaviorExtension,以供behaviors區段使用。-->
  <extensions>
    <behaviorExtensions>
      <add name="myMessageProcessingBehavior" type="MySimpleMessageUtility.MyMessageProcessingBehaviorExtension, MySimpleMessageUtility, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
    </behaviorExtensions>
  </extensions>
  <behaviors>
    <!--加入endpointBehaviors,並且指到extensions區段中指定的extension。-->
    <endpointBehaviors>
      <behavior name="MySimpleWcfService.MessageProcessingBehavior">
        <myMessageProcessingBehavior/>
      </behavior>
    </endpointBehaviors>
    <serviceBehaviors>
      <behavior name="MySimpleWcfService.SimpleServiceBehavior">
        <!-- To avoid disclosing metadata information, set the value below to false before deployment -->
        <serviceMetadata httpGetEnabled="true"/>
        <!-- To receive exception details in faults for debugging purposes, set the value below to true.  Set to false before deployment to avoid disclosing exception information -->
        <serviceDebug includeExceptionDetailInFaults="false"/>
      </behavior>
    </serviceBehaviors>
  </behaviors>
  <services>
    <service name="MySimpleWcfService.SimpleService" behaviorConfiguration="MySimpleWcfService.SimpleServiceBehavior">
      <!--在這邊指定EndPoint要套用的Behavior-->
      <endpoint address="" binding="basicHttpBinding" contract="MySimpleWcfService.ISimpleService" behaviorConfiguration="MySimpleWcfService.MessageProcessingBehavior">
        <identity>
          <dns value="localhost"/>
        </identity>
      </endpoint>
      <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
    </service>
  </services>
</system.serviceModel>

 

而客戶端呢?也很簡單,只要引用共用的元件庫後再加上一行程式碼,就可以輕鬆的套用啦!!

MainWindow.xaml.cs

_simpleServiceClient.Endpoint.Behaviors.Add( new MyMessageProcessingBehavior() );

 

最後(真的是最後了~),奉上完成的範例程式專案原始碼,請自行取用:

 

延伸資料: