以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來實作):
在未加上加解密功能之前,透過WPF開發的Client端一樣可以輕鬆的WCF實作的服務端取得資料。
上面這個範例的程式碼在此:
接下來,我們一樣要分別對WCF服務端和WPF實作的客戶端加上些東西,以讓資料在傳輸的過程中是有加密保護的。
與ASP.Net WebService不同之處是:在WCF中並不像ASP.Net WebService一樣有SOAP Extension讓我們使用,而是提供了IDispatchMessageInspector與IClientMessageInspector這兩個擴充介面分別供服務端與客戶端使用。
(這次的範例程式為了要讓服務端與客戶端可以共用相同的元素,因此我將可以共用的資料型態、加解密方法以及相關類別抽到共用的專案中以便參考。)
由於本範例中服務端與客戶端的加解密功能是一致的,所以我們可以先從訊息加解密訊息的部份下手:
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;
}
}
}
接著運用剛才完成的加解/密訊息功能來實作IDispatchMessageInspector與IClientMessageInspector兩個介面(這兩個介面中定義好的方法其實就與SOAP Extension中訊息的四個狀態非常類似,分別是服務端收到需求後的事件及送出回應前的事件;以及客戶端收到回應後的事件和送出需求前的事件):
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的喔!!):
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類別的類別(有點饒舌....):
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啦:
<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>
而客戶端呢?也很簡單,只要引用共用的元件庫後再加上一行程式碼,就可以輕鬆的套用啦!!
_simpleServiceClient.Endpoint.Behaviors.Add( new MyMessageProcessingBehavior() );
最後(真的是最後了~),奉上完成的範例程式專案原始碼,請自行取用:
延伸資料: