這次我們要真正的動手實作出一個可以將訊息進行加/解密的SOAP Extension功能。
前言
讓我們再複習一次Web Service生命周期示意圖 :
由上圖我們可以看出,一個由Client端送出的SOAP Request會經過以下流程:
1. Client端將資料序列化為XML後送出
2. Web Service端收到XML後將其反序列化回物件,並呼叫相對應的Web Method執行
3. Web Service將回傳值序列化為XML後送回給Client端
4. Client端收到Web Service回傳的XML後再將其反序列化回物件,進行後續動作
以前篇中的範例程式為例,當WPF Client端呼叫Web Service中的QueryMembersBySex方法取得性別為男生的資料時,會傳出的SOAP訊息內容如下:
<?xml version="1.0" encoding="utf-8" ?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<soap:Body>
<QueryMembersBySex xmlns="http://tempuri.org/">
<sex>true</sex>
</QueryMembersBySex>
</soap:Body>
</soap:Envelope>
很顯而易見的,只要有人有能力擷取到這段訊息或是Web Service端回傳的Response訊息,就可以不費吹灰之力的判讀出內容的涵意,或是仿造出相同格式的訊息。
如此一來,就算不公佈WSDL,也是有可能被其他第三方有心人士得知系統中提供的各個WebMethod所提供的訊息介面。
如果我們不想改變WSDL的結構,但是又想保護在網路中傳遞的訊息的話,我們可以在以下幾個階段改寫SOAP訊息的內容,將SOAP訊息加以加密,並以統一的格式收送:
如此就可以達到我們的目的。
而要在上述的幾個時機點對SOAP訊息動手腳這個重責大任,就透過SOAP Extension來完成了。
動手實作
接下來,我們要透過一個簡單的小範例,來說明怎麼實作出一個「只能接受自訂SOAP格式的Web Service與Client端」。
為了簡化開發的流程,我在方案中另外建立了一個Class Libray專用,以提供加密後的SOAP Message內容型別、加/解密功能以及最重要的SOAP Extension供Web Service與Client端共用。
在開始動手寫Code之前,請先將System.Web.Services加入專案的參考,以供SOAP Extension使用。
接著要在專案中加入以下幾個類別:
1. MySoapMessage – 用來定義SOAP訊息中訊息的格式(這邊為了簡化,整個類別中就只放一個字串,用來存放加密過的訊息)。
namespace MySimpleMessageUtility
{
public class MySoapMessage
{
public string Message { get; set; }
}
}
2. Encryptor – 用來處理訊息的加/解密功能(很基本的以AES演算法進行加解密的類別,不多作解釋)。
using System;
using System.Security.Cryptography;
using System.Text;
namespace MySimpleMessageUtility
{
public static class Encryptor
{
public static string EncryptAes( string text , string key , string iv )
{
string encryptedString;
try
{
RijndaelManaged rijndaelCipher = new RijndaelManaged();
rijndaelCipher.Mode = CipherMode.CBC;
rijndaelCipher.Padding = PaddingMode.PKCS7;
rijndaelCipher.KeySize = 128;
rijndaelCipher.BlockSize = 128;
byte[] pwdBytes = Encoding.UTF8.GetBytes( key );
byte[] keyBytes = new byte[ 16 ];
int len = pwdBytes.Length;
if( len > keyBytes.Length ) len = keyBytes.Length;
Array.Copy( pwdBytes , keyBytes , len );
rijndaelCipher.Key = keyBytes;
byte[] ivBytes1 = Encoding.UTF8.GetBytes( iv );
byte[] keyBytes1 = new byte[ 16 ];
int len1 = ivBytes1.Length;
if( len1 > keyBytes1.Length ) len1 = keyBytes1.Length;
Array.Copy( ivBytes1 , keyBytes1 , len1 );
rijndaelCipher.IV = ivBytes1;
ICryptoTransform transform = rijndaelCipher.CreateEncryptor();
byte[] plainText = Encoding.UTF8.GetBytes( text );
byte[] cipherBytes = transform.TransformFinalBlock( plainText , 0 , plainText.Length );
encryptedString = Convert.ToBase64String( cipherBytes );
}
catch( Exception ex )
{
throw ex;
}
return encryptedString;
}
public static string DecryptAes( string text , string key , string iv )
{
string decryptedString;
try
{
RijndaelManaged rijndaelCipher = new RijndaelManaged();
rijndaelCipher.Mode = CipherMode.CBC;
rijndaelCipher.Padding = PaddingMode.PKCS7;
rijndaelCipher.KeySize = 128;
rijndaelCipher.BlockSize = 128;
byte[] encryptedData = Convert.FromBase64String( text );
byte[] pwdBytes = Encoding.UTF8.GetBytes( key );
byte[] keyBytes = new byte[ 16 ];
int len = pwdBytes.Length;
if( len > keyBytes.Length ) len = keyBytes.Length;
Array.Copy( pwdBytes , keyBytes , len );
rijndaelCipher.Key = keyBytes;
byte[] ivBytes1 = Encoding.UTF8.GetBytes( iv );
byte[] keyBytes1 = new byte[ 16 ];
int len1 = ivBytes1.Length;
if( len1 > keyBytes1.Length ) len1 = keyBytes1.Length;
Array.Copy( ivBytes1 , keyBytes1 , len1 );
rijndaelCipher.IV = keyBytes1;
ICryptoTransform transform = rijndaelCipher.CreateDecryptor();
byte[] plainText = transform.TransformFinalBlock( encryptedData , 0 , encryptedData.Length );
decryptedString = Encoding.UTF8.GetString( plainText );
}
catch( Exception ex )
{
throw ex;
}
return decryptedString;
}
}
}
3. MyEncryptionExtension – SOAP Extesnion本體。
using System.Configuration;
using System.IO;
using System.Linq;
using System.Text;
using System.Web.Services.Protocols;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Serialization;
namespace MySimpleMessageUtility
{
public class MyEncryptionExtension : SoapExtension
{
private XNamespace _soapNamespace = "http://schemas.xmlsoap.org/soap/envelope/";
private Stream _oldStream;
private Stream _newStream;
private string _key = ConfigurationManager.AppSettings[ "EncryptorKey" ];
private string _iv = ConfigurationManager.AppSettings[ "EncryptorIV" ];
#region override SoapExtension methods
public override object GetInitializer( System.Type serviceType )
{
return null;
}
public override object GetInitializer( LogicalMethodInfo methodInfo , SoapExtensionAttribute attribute )
{
return null;
}
public override void Initialize( object initializer )
{
}
/// <summary>
/// 複寫Soap Extension中處理SOAP訊息的方法,以讓我們能對訊息動手腳
/// </summary>
/// <param name="message">原始的SOAP訊息</param>
public override void ProcessMessage( SoapMessage message )
{
switch( message.Stage )
{
case SoapMessageStage.BeforeSerialize:
break;
case SoapMessageStage.AfterSerialize:
EncryptMessage();
break;
case SoapMessageStage.BeforeDeserialize:
DecryptMessage();
message.Stream.SetLength( 0 );
_newStream.Position = 0;
Copy( _newStream , message.Stream );
message.Stream.Position = 0;
break;
case SoapMessageStage.AfterDeserialize:
break;
}
}
#endregion
#region Manually added methods
/// <summary>
/// 將SOAP訊息中的Stream內容進行加密,並且轉為透過自訂的型別傳輸
/// </summary>
private void EncryptMessage()
{
//將MemoryStream的位置歸零(很重要!!!)
_newStream.Position = 0;
//讀出Stream中的XML內容
XmlTextReader xmlTextReader = new XmlTextReader( _newStream );
XDocument xDocument = XDocument.Load( xmlTextReader , LoadOptions.None );
//取出SOAP訊息中的SoapBody節點
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();
//以序列化後的MySoapMessage資料取代原資料
soapBodyElement.Add( Serialize( mySoapMessage ).Element( "MySoapMessage" ) );
//宣告一個MemoryStream做為資料暫存容器
MemoryStream memoryStream = new MemoryStream();
//宣告一個以memoryStream為基底的StreamWriter,以便將處理過後的XML寫入
StreamWriter streamWriter = new StreamWriter( memoryStream );
//將處理過後的XML寫入streamWriter物件中
xDocument.Save( streamWriter , SaveOptions.DisableFormatting );
//將memoryStream的位置歸零,以便Copy MemoryStream時可以從頭到尾完全複製(很重要!!!)
memoryStream.Position = 0;
//以處理過後的MemoryStream取代原來SOAP訊息中的Stream
Copy( memoryStream , _oldStream );
}
/// <summary>
/// 將SOAP訊息中的Stream內容解密,並且轉回原來的XML格式
/// </summary>
private void DecryptMessage()
{
//由於原來的Stream無法修改,所以先將它複製到另一個自行建立的MemoryStream中方便我們動手腳
Copy( _oldStream , _newStream );
//複製完之後一樣要記得先把MemoryStream的位置歸零(還是很重要!!!)
_newStream.Position = 0;
//將MemoryStream的內容讀出為字串
string soapMessage = new StreamReader( _newStream ).ReadToEnd();
//將MemoryStream的內容轉為XDocument
XDocument xDocument = XDocument.Parse( soapMessage , LoadOptions.None );
//取出SOAP訊息中的SoapBody節點
XElement soapBodyElement = xDocument.Descendants( _soapNamespace + "Body" ).First();
//取出MySoapMessage節點
XElement messageElement = soapBodyElement.Descendants( "MySoapMessage" ).Descendants( "Message" ).First();
//將原來SoapBody的內容進行解密
string decryptedString = Encryptor.DecryptAes( messageElement.Value , _key , _iv );
//將解密過的內容反序列化回XElement,以便於取代原來SoapBody中的內容
XElement newContent = XElement.Parse( decryptedString , LoadOptions.None );
//移除SoapBody中原來的內容
soapBodyElement.Elements().Remove();
//以反序列化後的真實資料取代原資料
soapBodyElement.Add( newContent );
//將修改完成的XML存回_newStream中以便後續處理
_newStream = new MemoryStream( Encoding.UTF8.GetBytes( xDocument.ToString() ) );
}
/// <summary>
/// 序列化物件為XDocument的Method
/// </summary>
/// <typeparam name="T">要被序列化的物件的型別</typeparam>
/// <param name="source">要被序列化的物件</param>
/// <returns>轉換後的XDocuemnt物件</returns>
public XDocument Serialize<T>( T source )
{
XDocument xDocument = new XDocument();
XmlSerializer xmlSerializer = new XmlSerializer( typeof( T ) );
XmlWriter xmlWriter = xDocument.CreateWriter();
XmlSerializerNamespaces xmlSerializerNamespaces = new XmlSerializerNamespaces();
xmlSerializerNamespaces.Add( string.Empty , string.Empty );
xmlSerializer.Serialize( xmlWriter , source , xmlSerializerNamespaces );
xmlWriter.Close();
return xDocument;
}
/// <summary>
/// 複寫原來SoapExtension中的ChainStream方法,以便於讓我們偷天換日
/// </summary>
/// <param name="stream"></param>
/// <returns></returns>
public override Stream ChainStream( Stream stream )
{
_oldStream = stream;
_newStream = new MemoryStream();
return _newStream;
}
/// <summary>
/// 自訂一個用來複製Stream的方法(因SOAP訊息中的Stream會在不同的階段有讀寫的限制,故需要複製一份出來動手腳)
/// </summary>
/// <param name="from"></param>
/// <param name="to"></param>
void Copy( Stream from , Stream to )
{
TextReader reader = new StreamReader( from );
TextWriter writer = new StreamWriter( to );
writer.WriteLine( reader.ReadToEnd() );
writer.Flush();
}
#endregion
}
}
4. MyEncryptionExtensionAttribute – 與SOAP Extension搭配的屬性(若同時有多個SoapExtension要掛在同一個WebService中,也可以透過這個屬性來決定執行的先後順序)。
using System;
using System.Web.Services.Protocols;
namespace MySimpleMessageUtility
{
[AttributeUsage( AttributeTargets.Method )]
public class MyEncryptionExtensionAttribute : SoapExtensionAttribute
{
public override Type ExtensionType
{
get { return typeof( MyEncryptionExtension ); }
}
public override int Priority { get; set; }
}
}
完成以上的工作之後,再來就可以將Soap Extension分別掛載進Web Service端與WPF Client端了,在進行接下來的動作之前,請務必先確認Web Service專案與WPF Client專案都已加入MySimpleMessageUtility專案的參考。
將Web Service的Web Method加上Soap Extension的方法非常簡單,只需要在Web Method上加上要掛載的Soap Extesnion Attribute即可,範例如下:
using MySimpleMessageUtility;
using MySimpleWebService.Models;
using System.Collections.Generic;
using System.Linq;
using System.Web.Services;
namespace MySimpleWebService
{
[WebService( Namespace = "http://tempuri.org/" )]
[WebServiceBinding( ConformsTo = WsiProfiles.BasicProfile1_1 )]
[System.ComponentModel.ToolboxItem( false )]
public class SimpleService : WebService
{
private static List<Member> _allMembers;
public SimpleService()
{
if( _allMembers == null )
{
_allMembers = new List<Member>
{
new Member{ Id = 0 , Age = 35 , Name = "Ouch" , Sex = true },
new Member{ Id = 1 , Age = 27 , Name = "Sandy" , Sex = false },
new Member{ Id = 2 , Age = 30 , Name = "Wei" , Sex = true },
new Member{ Id = 3 , Age = 30 , Name = "Cross" , Sex = true },
new Member{ Id = 4 , Age = 10 , Name = "尼古拉" , Sex = true },
};
}
}
[WebMethod]
[MyEncryptionExtension()]
public List<Member> ListAllMembers()
{
return _allMembers;
}
[WebMethod]
[MyEncryptionExtension()]
public bool UpdateMember( int id , Member updatedMember )
{
Member member = _allMembers.FirstOrDefault( m => m.Id == id );
if( member == null )
{
return false;
}
member.Age = updatedMember.Age;
member.Name = updatedMember.Name;
member.Sex = updatedMember.Sex;
return true;
}
[WebMethod]
[MyEncryptionExtension()]
public List<Member> QueryMembersBySex( bool sex )
{
return _allMembers.Where( m => m.Sex == sex ).ToList();
}
}
}
而Client端要掛上Soap Extension更簡單,只要在app.config/web.config裡的system.web區段中加入以下設定就行了:
<system.web>
<webServices>
<soapExtensionTypes>
<!--type中需放入Soap Extension的類別名稱(包含命名空間)與元件名稱,中間以逗號分隔。 而priority則用來指定Soap Extension執行的先後順序。-->
<add type="MySimpleMessageUtility.MyEncryptionExtension,MySimpleMessageUtility" priority="1"/>
</soapExtensionTypes>
</webServices>
</system.web>
還有很重要的一點,既然說我們是要透過AES演算法對訊息的內容進行加/解密,那麼WebService和Client端裡加/解密所需要使用到的Key和IV就不能漏掉啦!!這邊就在兩個專案的config檔的appSettings區段中分別加入以下設定:
<appSettings>
<add key="EncryptorKey" value="Ouch1978" />
<add key="EncryptorIV" value="0123456789ABCDEF" />
</appSettings>
這些步驟都完成之後,就可以試著執行專案,看看是不是可以正常的運作囉!!~
若以除錯模式針對WPF Client端送出的訊息,會發現送出的訊息變成如下的格式,且還是可以從Web Service端取回資料喔!!
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<soap:Body>
<MySoapMessage>
<Message>AvrNTQ+Zz/rJZ6rWkiB1O6QF5oLawW+yeALGVzMXuHmXegtVJI5LO9pecXCkWYCbx64duwEd7IGmObYv+yQeuNMAdYgesqncwWNkknLNTcCyAD63Xg3RrFtJP3gr4y32</Message>
</MySoapMessage>
</soap:Body>
</soap:Envelope>
額外小叮嚀:
透過本文所介紹的方式,就可以做到某種程度的資料保護效果,只要Client端沒有實作一樣的Soap Extension來傳送資料,那Web Service端可是不會買帳的喔!!(也就是說,WSDL中提供的訊息介面就變成參考用,如果直接呼叫可是會碰壁的啊~~)
而除了針對訊息進行加/解密之外,也可以透過Soap Extension來進行對訊息的壓縮或是記錄等等工作,有興趣的朋友們不妨研究看看喔!!
本文專案原始碼如下,請自行取用: