2013年10月20日 星期日

DI patterns (DI 模式)

前言:
這篇文章主要講的是DI常見的方式,共四種模式這邊首先介紹最為常見的建構子注入。其實DI這個技巧我們常常都會使用到,甚至就算你不知道也會用它,雖然是什麼相當偉大的技巧但是卻是很實用也很重要的技巧。DI(Dependency Injection, 依賴注入),講的是針對抽象來撰寫程式而非實體。底下會有範例來展示這項技巧。另外,這篇文章主要是依照Dependency Injection in .NET這本書的內容來撰寫的,但有部分觀點屬於個人。

內文:
Constructor Injection (建構子注入):
這個模式應用很廣泛也是很容易實現的一個DI模式,在使用這個模式時會需要類別提供一個公開(public)並且需要一個實體作為參數的的建構子,通常的情況下這個類別只會有這麼一個建構子的存在,另外,請儘量避免overload建構子,保持只有單一一個建構子的情況。
//實現 Constructor Injection 的類別
public class MyFirstDIClass
{
   private readonly AnimalRepository repository;// DI 欄位是readonly可以在類別初始化後避免被修改。
   public MyFirstDIClass(AnimalRepository repository)
   {
      if(repository==null)// 確保注入的實體是存在的
      {
         throw new ArgumentNullException("repository");
      }
      this.repository=repository;// 注入依賴的實體
   }
}
通常所要求輸入的參數會是一個抽象類別或是一個介面(abstract class or interface),之所以使用這兩種Type是因為建構子可以不用去管真正實現的類別究竟為何,只要這個類別繼承或是實作指定的Type即可,因為對於這個注入的實體也只會使用到指定Type所提供的公開方法而已。
在使用這個方法時有個需要注意的地方,要儘量避免在建構子中撰寫其他的邏輯。建構子裡面最好就是只有建構這個類別時所需要的邏輯即可,所有初始化這個類別以外的邏輯都不應該出現在這裡,簡單來講就是維持建構子的單純。
一般來說 Constructor Injection 這個方式的優缺點為
優點:
1)能確保依賴確實的注入到類別中
2)容易實現
缺點:
1)在一些架構下很難去實現(ex. 像是在ASP.NET Web Form 這個 Framework 中就很難去實現這個依賴注入的方法,有些架構就是天生不適合使用DI)

小結:
Constructor Injection這個方法在面對依賴注入是必要的情況下很好使用。而從建構子注入依賴也可以滿足大多數的使用情況,儘量使用這個方法做為首選依賴注入的方法。

Method Injection(方法注入)
---

當遇到Dependency會依據每一次的呼叫而有所不同時,就很適合使用這種依賴注入的方式。

public class UserInfoService
{
public IEnumerable<AdvertisementEntity> ShowAdvertisement(IUserGradeService userGradeService)
{
//// 依據傳入的會員等級不同,顯示不同的優惠
return userGradeService.GetAdvertisements();
}
}
有兩種比較常見的使用時機

* 像是 add-in 一類,主程式提供統一介面注入,實作該介面的實體就會被主程式使用。

* 像是一個領域模型(Domain Entity)(常見於[領域驅動開發](https://en.wikipedia.org/wiki/Domain-driven_design)) 混合資料與商業邏輯。

優點:

1. 允許呼叫者依據情境提供相應的實體
2. 讓依賴得以注入以資料為中心的物件,而非使用組合(composition)的方式

缺點:

1. 使用限制比較多
2. 會讓依賴暴露於 API 層級,向外部的呼叫者透露較多訊息

--全文尚未完結--

2013年4月19日 星期五

using Strong Name Tool to encryption message

前言:

一般來說在.net中實現加密時我們會使用RSACryptoServiceProvider這個類別來實現產生公私鑰進行加解密的行為,而且一個簡單的ToXMLString()方法就能把公鑰(或甚至是私鑰,但實務上很不建議)給吐出來。不過除了使用這個類別來產生鑰匙外我們其實還可以利用強式名稱工具來幫我們產生鑰匙,好處是不但可以作為加解密的鑰匙來源,沒有金鑰容器名稱以及存取層級(User Layer, Machine Layer)的管理issue簡單用,還可以順便用它來簽署組件,一檔多用資源徹底利用。以下就是利用強式名稱工具來實作的方式。

內文:

強式名稱工具(Strong Name Tool)是安裝Visual Studio之後就會附上的開發用工具我們可以開啟"Visual Studio x64 Win64命令提示字元",鍵入"sn.exe /?"來觀看可以使用的參數,這裡我們會用到的就是最簡單的 -k 參數而且就是使用default值。我們就利用這個工具產生一組全新的snk檔案放在D槽吧~




我們已經可以在D槽的跟目錄下面看到產生好的Strong Name Key File了~

要注意的是使用預設的話key size長度就是1024 bit這長度與能拿來加密的文字長度會有直接關係使用預設長度的話一次能夠加密的文字長度為128bits,不過要小心在.net中會有padding的issue造成實際上可以加密的文字長度會更短(註一),這個問題的解決方式我們會在稍後的範例中看到。

準備好之後我們就來開啟一個專案試試看這snk檔要怎麼幫我們加密資料吧~

我們這邊就使用最單純的Console專案來進行測試即可~

ps.這邊記得把D槽根目錄中的snk檔案放在專案的bin資料夾底下。

ps2.為了保持目的單純不小心類別就被搞大了...

using System;
using System.Collections;
using System.IO;
using System.Security.Cryptography;

class Program
{
   private static string _strongNameKeyFile = "Test.snk";
   public static string GetPublicKey
   {
      get;
      private set;
   }
   private static string GetPrivateKey
   {
      get;
      set;
   }

   public static string Data(string plaintext)
   {
      using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
      {
         rsa.ImportCspBlob(File.ReadAllBytes(_strongNameKeyFile));
         GetPublicKey = rsa.ToXmlString(false);
         GetPrivateKey = rsa.ToXMLString(true);

         // base-64 encoded.
         return Convert.ToBase64String(EncryptData(plaintext));
      }
   }

   // 若資料量很大的話(資料量小以預設來說就是資料量小於117 bits,那可以直接呼叫Encrypt()來進行加密及可)
   // 就需要分段加密不然一定會炸掉,吐一個Bad Length的例外訊息。
   private static byte[] EncryptData(string rawdata)
   {
      // 憑證的金鑰預設就是1024 bit
      // buffer扣去11 bits是為了padding使用,請參考註一的連結。
      const int encryptionBufferSize = (1024 / 8) - 11;
      const int DecryptionBufferSize = 1024 / 8;

      RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
      // 這邊利用公鑰來加密,因此解密的一方需要取得密鑰才能解密,這樣可以確保有私鑰的人才有辦法看到資訊。
      rsa.FromXmlString(GetPublicKey);

      // 進行加密時都需要把字串轉成byte陣列來操作,這邊需要注意轉換的問題!
      byte[] dataEncoded = System.Text.Encoding.UTF8.GetBytes(rawdata);
      using (MemoryStream ms = new MemoryStream())
      {
         byte[] buffer;
         int pos = 0;
         int copyLength = encryptionBufferSize;
         while (true)
         {
            if (pos + copyLength > dataEncoded.Length)
               copyLength = dataEncoded.Length - pos;
            buffer = new byte[copyLength];
            Array.Copy(dataEncoded, pos, buffer, 0, copyLength);
            pos += copyLength;

            // 這邊不使用OEAP padding,使用 PKCS#1 v1.5 padding(填補法的差異請參考註一)
            // 另外也要注意雖然加密的資料需要扣除 padding 量,但是寫入的長度依然不變。
            ms.Write(rsa.Encrypt(buffer, false), 0, DecryptionBufferSize);
            Array.Clear(buffer, 0, copyLength);
            if (pos >= dataEncoded.Length)
               break;
         }
         return ms.ToArray();
      }
   }

   public static string GetPlainDataBack(string encryptdata)
   {
      string result = string.Empty;
      using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
      {
         const int decryptionBufferSize = 1024 / 8;

         // 使用公鑰加密就必須用私鑰解密。
         rsa.FromXmlString(GetPrivateKey);
         byte[] encryptdataarray = Convert.FromBase64String(encryptdata);
         using (MemoryStream ms = new MemoryStream(encryptdataarray.Length))
         {
            byte[] buffer = new byte[decryptionBufferSize];
            int pos = 0;
            int copyLength = buffer.Length;
            while (true)
            {
               Array.Copy(encryptdataarray, pos, buffer, 0, copyLength);
               pos += copyLength;
               byte[] resp = rsa.Decrypt(buffer, false);
               ms.Write(resp, 0, resp.Length);
               Array.Clear(resp, 0, resp.Length);
               Array.Clear(buffer, 0, buffer.Length);

               if (pos >= encryptdataarray.Length)
                  break;
            }
            result = System.Text.Encoding.UTF8.GetString((ms.ToArray()));
         }
      }
      return result;
   }

   static void Main(string[] args)
   {
      var plaintext = "This is a plain text!";
      var result = Data(plaintext);
      Console.WriteLine("Orignal Text:{0}", plaintext);
      Console.WriteLine("After Encryption:{0}", result);

      result = GetPlainDataBack(result);
      Console.WriteLine("After Decryption:{0}",result);
      Console.Read();
   }
}


結語:
以上就是使用強式名稱工具來達成資訊加密、解密的需求,我們可以發現若對方要可以解讀這個密文那snk檔是對方勢必得取得的檔案,因此該檔案的存取是一定要控管的,這是不方便之處。另外其實實務上除了加解密之外還會順手做數位簽章....這就留到下次吧...


參考資料:

註一:http://msdn.microsoft.com/en-us/library/system.security.cryptography.rsacryptoserviceprovider.encrypt.aspx

參考1:http://www.dotblogs.com.tw/huanlin/archive/2008/04/23/3309.aspx

參考2:http://msdn.microsoft.com/zh-tw/library/system.security.cryptography.rsacryptoserviceprovider%28v=vs.110%29.aspx

參考3:http://msdn.microsoft.com/zh-tw/library/k5b5tt23%28v=vs.110%29.aspx

參考4:http://en.wikipedia.org/wiki/RSA_%28algorithm%29