裝飾者模式(Decorator Pattern)實例:壓縮加密一次完成
5 |
手邊有個自訂傳輸管道的加密需求,預期資料量可能高達數MB,為提升效能,先壓縮再加密是不錯的做法,既可減少加密時間及成本,又能節省頻寬,一舉兩得。
過去用C#寫過DES加密,也寫過GZip壓縮,把兩個結合起來不是難事。如果不要想太多,取得待處理資料(byte[]),用GZipStream壓縮可得壓縮後資料(byte[]),再以壓縮後資料當成輸入參數交給CryptoStream進行DES加密,就得到壓縮並加密的結果(byte[])。
事實上,有更輕巧簡潔的做法-.NET的Stream支援設計模式(Design Pattern)裡的裝飾者模式(Decorator Pattern,也有人翻成修飾模式),裝飾者模式的精神在於「不使用繼承,而以組裝方式動態地為物件加入新功能」。常見的做法是設計一個裝飾類別,以被裝飾物件A做為建構式參數,而裝飾類別提供與被裝飾對象相同的方法,讓呼叫端使用跟原來相同的介面呼叫,得到裝飾類別加工後的結果。以GZip壓縮使用的GZipStream為例,假設原本有個FileStream fs,呼叫fs.Write()可以將byte[]寫入檔案。若要加入壓縮功能,我們可以建立一個GZipStream gzip = new GZipStream(fs, CompressionMode.Compress),寫入時改呼叫gzip.Write(),資料使會先經GZip壓縮再寫入檔案。如果要做到動態決定要不要壓縮,則可將變數stream宣告成抽象型別Stream(FileStream、GZipStream共同繼承的繼承來源),一開始stream = new FileStream(…),若決定壓縮,就讓stream = new GZipStream(stream, CompressionMode.Compress),寫入資料時則一律呼叫stream.Write(),便能實現執行期間動態切換是否壓縮,展現裝飾者模式的優勢。
解釋完概念,以下是完整程式範例:
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace Demo
{
class Program
{
//以金鑰字串進行雜湊運算產生DES所需的Key及IV byte[]
public static Tuple<byte[], byte[]> GetKeyIV(string keyString)
{
MD5 md5 = MD5.Create();
var hash = md5.ComputeHash(Encoding.UTF8.GetBytes(keyString));
byte[] key = new byte[8];
byte[] iv = new byte[8];
System.Buffer.BlockCopy(hash, 0, key, 0, 8);
System.Buffer.BlockCopy(hash, 8, iv, 0, 8);
return new Tuple<byte[],byte[]>(key, iv);
}
public static byte[] Encrypt(string key, byte[] data, bool compress = false)
{
using (MemoryStream ms = new MemoryStream())
{
DESCryptoServiceProvider des = new DESCryptoServiceProvider();
var keyIv = GetKeyIV(key);
//先建立加密用的CryptoStream
using (CryptoStream crypto = new CryptoStream(ms,
des.CreateEncryptor(keyIv.Item1, keyIv.Item2), CryptoStreamMode.Write))
{
//依compress參數將Stream gzip設成GzipStream
//若不需壓縮,gzip直接等於CryptoStream crypto
using (Stream gzip =
compress ?
(Stream)new GZipStream(crypto, CompressionMode.Compress) :
(Stream)crypto)
{
//不管是CryptoStream或GZipStream
//一視同仁寫入byte[]就對了
gzip.Write(data, 0, data.Length);
}
}
return ms.ToArray();
}
}
public static byte[] Decrypt(string key, byte[] crypted, bool decompress = false)
{
//用MemoryStream將傳入的byte[]包裝成Stream物件
using (MemoryStream ms = new MemoryStream(crypted))
{
DESCryptoServiceProvider des = new DESCryptoServiceProvider();
var keyIv = GetKeyIV(key);
//解密兼解壓時,CryptoStream一樣在外層
using (CryptoStream crypto = new CryptoStream(ms,
des.CreateDecryptor(keyIv.Item1, keyIv.Item2), CryptoStreamMode.Read))
{
using (Stream gzip =
decompress ?
(Stream)new GZipStream(crypto, CompressionMode.Decompress) :
(Stream)crypto)
{
//另外宣告一個MemoryStream用來存結果
using (MemoryStream msDec = new MemoryStream())
{
byte[] buff = new byte[8192];
int len = 0;
//由於不知結果資料長度,持續讀取
//由Read()傳回長度偵測是否已到結尾
while ((len = gzip.Read(buff, 0, buff.Length)) > 0)
{
msDec.Write(buff, 0, len);
}
return msDec.ToArray();
}
}
}
}
}
static void Main(string[] args)
{
WebClient wc = new WebClient();
byte[] testData = wc.DownloadData(
"http://cdn.kendostatic.com/2014.1.318/js/kendo.all.min.js");
var encoding = Encoding.UTF8;
int displayLen = Math.Min(testData.Length, 128);
Console.WriteLine("來源長度={0:n0}", testData.Length);
Console.WriteLine("內容預覽: {0}...",
encoding.GetString(testData, 0, displayLen));
Console.WriteLine();
var encKey = "SECRET";
byte[] encrypted = Encrypt(encKey, testData);
Console.WriteLine("加密後長度={0:n0}", encrypted.Length);
Console.WriteLine("內容預覽: {0}...",
encoding.GetString(encrypted, 0, displayLen));
Console.WriteLine();
byte[] decrypted = Decrypt(encKey, encrypted);
string srcB64 = Convert.ToBase64String(testData);
Console.WriteLine("解密還原驗證: {0}",
srcB64 == Convert.ToBase64String(decrypted));
byte[] compressEncrypted = Encrypt(encKey, testData, true);
Console.WriteLine("壓縮加密後長度={0:n0}", compressEncrypted.Length);
Console.WriteLine("內容預覽: {0}...",
encoding.GetString(compressEncrypted, 0, displayLen));
Console.WriteLine();
byte[] decompressDecrypted = Decrypt(encKey, compressEncrypted, true);
Console.WriteLine("解密還原驗證: {0}",
srcB64 == Convert.ToBase64String(decompressDecrypted));
Console.Read();
}
}
}
以Kendo UI Libaray為測試對象,原本長度為1,493,858,加密後大小為1,493,864,啟用壓縮再加密後大小為442,504,解密、解壓縮還原後與原始內容比對一致,測試成功。
Comments
# by kuanpak
有沒有試過先壓縮後加密? 感覺壓縮率應該會更高.
# by Jeffrey
to kuanpak, 同意,文章採用的做法也是先壓縮再加密,加密後的內容形同亂碼,壓縮率不會太好。
# by 別問我是誰
大大分享的範例真是"完整" 在網頁看了看還是不能體會,實際貼到 VS 上跑一次,在上面看程式碼也比較習慣。 謝謝了
# by Vanson
很喜歡 decorator pattern 例如 iOS UIAlertView 和 Android AlertView 等等
# by 胡忠晞
如果是 Decorator Pattern 應該要實作 WebClient 吧! 這篇講解 Decorator Pattern 比較清楚。 http://www.dotblogs.com.tw/pin0513/archive/2010/01/04/12779.aspx