[Web Service] 透過SOAP Extension進行訊息的加/解密 – (中) 實作SOAP Extension

  • 6189
  • 0
  • C#
  • 2013-07-12

這次我們要真正的動手實作出一個可以將訊息進行加/解密的SOAP Extension功能。

 

前言

讓我們再複習一次Web Service生命周期示意圖 :

image_thumb9

 

由上圖我們可以看出,一個由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訊息加以加密,並以統一的格式收送:

image

如此就可以達到我們的目的。

 

而要在上述的幾個時機點對SOAP訊息動手腳這個重責大任,就透過SOAP Extension來完成了。

 

動手實作

接下來,我們要透過一個簡單的小範例,來說明怎麼實作出一個「只能接受自訂SOAP格式的Web Service與Client端」。

為了簡化開發的流程,我在方案中另外建立了一個Class Libray專用,以提供加密後的SOAP Message內容型別、加/解密功能以及最重要的SOAP Extension供Web Service與Client端共用。

image

在開始動手寫Code之前,請先將System.Web.Services加入專案的參考,以供SOAP Extension使用。

image

 

接著要在專案中加入以下幾個類別:

1. MySoapMessage – 用來定義SOAP訊息中訊息的格式(這邊為了簡化,整個類別中就只放一個字串,用來存放加密過的訊息)。

MySimpleMessageUtility.cs

namespace MySimpleMessageUtility
{
    public class MySoapMessage
    {
        public string Message { get; set; }
    }
}

2. Encryptor – 用來處理訊息的加/解密功能(很基本的以AES演算法進行加解密的類別,不多作解釋)。

Encryptor.cs

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本體。

MyEncryptionExtension.cs

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中,也可以透過這個屬性來決定執行的先後順序)。

MySimpleMessageUtility.cs

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即可,範例如下:

MySimpleWebService.cs

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區段中加入以下設定就行了:

app.config

<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區段中分別加入以下設定:

app.config/web.config

<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來進行對訊息的壓縮或是記錄等等工作,有興趣的朋友們不妨研究看看喔!!

 

本文專案原始碼如下,請自行取用: