在前面兩篇介紹了Socket框架的設計思路以及數據傳輸方面的內容,整個框架的設計指導原則就是易於使用及安全性較好,可以用來從客戶端到服務端的數據安全傳輸,那么實現這個目標就需要設計好消息的傳輸和數據加密的處理。本篇主要介紹如何利用Socket傳輸協議來實現數據加密和數據完整性校驗的處理,數據加密我們可以采用基於RSA非對稱加密的方式來實現,數據的完整性,我們可以對傳輸的內容進行MD5數據的校驗對比。
1、Socket框架傳輸內容分析
前面介紹過Socket的協議,除了起止標識符外,整個內容是一個JSON的字符串內容,這種格式如下所示。
上述消息內容,我們可以通過開始標識位和結束標識位,抽取出一個完整的Socket消息,這樣我們對其中的JSON內容進行序列號就可以得到對應的實體類,我們定義實體類的內容如下所示。
我們把消息對象分為請求消息對象和應答消息對象,他們對應的是Request和Response的消息,也就是一個是發起的消息,一個是應答的消息。其中上圖的“承載的JSON內容就是我們另一個傳輸對象的JSON字符串,這樣我們通過這種字符串來傳輸不同對象的信息,就構造出了一個通用的消息實體對象。
另外這些傳輸的消息對象,它本身可以繼承於一個實體類的基類,這樣方便我們對它們的統一處理,如下圖所示,就是一個通用的消息對象BaseMessage和其中JSON內容的對象關系圖,如AuthRequest是登陸驗證請求,AuthorRepsonse是登陸驗證的應答。
當然,我們整個Socket應用,可以派生出很多類似的Request和Response的消息對象,如下所示是部分消息的定義。
對於非對稱加密的處理,一般來說會有一些性能上的損失,不過我們考慮到如果是安全環境的數據傳輸處理的話,我們使用非對稱加密還是比較好的。
當然也有人建議采用非對稱加密部分內容,如雙方采用約定的對稱加密鍵,通過非對稱加密的方式來傳輸這個加密鍵,然后兩邊采用對稱加密算法來處理也是可以的。不過本框架主要介紹采用非對稱加密的方式來加密其中的JSON內容,其他部分常規的信息不進行加密。
2、非對稱加密的公鑰傳遞
消息加密數據的傳輸前,我們需要交換算法的公鑰,也就是服務器把自己公鑰給客戶端,客戶端收到服務器的公鑰請求后,返回客戶端的公鑰給服務器,實現兩者的交換,以后雙方的消息都通過對方公鑰加密,把加密內容通過標准的Socket消息對象傳遞,這樣對方收到的加密內容,就可以通過自身的私鑰進行解密了。
那么要在傳遞消息前處理這個公鑰交換的話,我們可以設計在服務器接入一個新的客戶端連接后(在登錄處理前),向客戶端發送服務器的公鑰,客戶端受到服務器的公鑰后,回應自己的公鑰信息,並存儲服務器的公鑰。這樣我們就可以在登陸的時候以及后面的消息傳遞過程中,使用對方公鑰進行加密數據,實現較好的安全性。
公鑰傳遞的過程如下圖所示,也就是客戶端發起連接服務器請求后,由服務器主動發送一個公鑰請求命令,客戶端收到后進行響應,發送自身的公鑰給服務器,服務器把客戶端的公鑰信息存儲在對應的Socket對象上,以后所有消息都通過客戶端公鑰加密,然后發送給客戶端。
前面我們介紹過,我們所有的自定義Socket對象,都是繼承於一個BaseSocketClient這樣的基類對象,那么我們只需要在它的對象里面增加幾個屬性幾個,一個是自己的公鑰、私鑰,一個是對方的公鑰信息,如下所示。
在程序的啟動后,包括客戶端啟動,服務器啟動,我們都需要構建好自己的公鑰私鑰信息,如下代碼是產生對應的公鑰私鑰信息,並存儲在屬性里面。
using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider()) { this.RSAPublicKey = rsa.ToXmlString(false);// 公鑰 this.RSAPrivateKey = rsa.ToXmlString(true);// 私鑰 }
例如在服務器端,在客戶端Socket成功接入后,我們就給對應的客戶端發送公鑰請求消息,如下代碼所示。
/// <summary> /// 客戶端連接后的處理(如發送公鑰秘鑰) /// </summary> /// <param name="client">連接客戶端</param> protected override void OnAfterClientConnected(ClientOfShop client) { //先記錄服務端的公鑰,私鑰 client.RSAPrivateKey = Portal.gc.RSAPrivateKey; client.RSAPublicKey = Portal.gc.RSAPublicKey; //發送一個公鑰交換命令 var request = new RsaKeyRequest(Portal.gc.RSAPublicKey); var data = request.PackData(); client.SendData(data); Thread.Sleep(100); }
那么在客戶端,接收到服務端的消息后,對消息類型判斷,如果是公鑰請求,那么我們需要進行回應,把自己的公鑰發給服務器,否則就進行其他的業務處理了。
/// <summary> /// 重寫讀取消息的處理 /// </summary> /// <param name="message">獲取到的完整Socket消息對象</param> protected override void OnMessageReceived(BaseMessage message) { if (message.MsgType == DataTypeKey.RSARequest) { var info = JsonTools.DeserializeObject<RsaKeyRequest>(message.Content); if (info != null) { //記錄對方的公鑰到Socket對象里面 this.PeerRSAPublicKey = Portal.gc.UseRSAEncrypt ? info.RSAPublicKey : ""; Console.WriteLine("使用RAS加密:{0},獲取到加密公鑰為:{1}", Portal.gc.UseRSAEncrypt, info.RSAPublicKey); //公鑰請求應答 var publicKey = Portal.gc.UseRSAEncrypt ? Portal.gc.RSAPublicKey : ""; var data = new RsaKeyResponse(publicKey);//返回客戶端的公鑰 var msg = data.PackData(message); SendData(msg); Thread.Sleep(100);//暫停下 } } else { //交給業務消息處理過程 this.MessageReceiver.AppendMessage(message); this.MessageReceiver.Check(); } }
如果我們交換成功后,我們后續的消息,就可以通過RSA非對稱加密進行處理了,如下代碼所示。
data.Content = RSASecurityHelper.RSAEncrypt(this.PeerRSAPublicKey, data.Content);
而解密消息,則是上面代碼的逆過程,如下所示。
message.Content = RSASecurityHelper.RSADecrypt(this.RSAPrivateKey, message.Content);
最后我們把加密后的內容組成一個待發送的Socket消息,包含起止標識符,如下所示。
//轉為JSON,並組裝為發送協議格式 var json = JsonTools.ObjectToJson(data); toSendData = string.Format("{0}{1}{2}", (char)this.StartByte, json, (char)this.EndByte);
這樣就是我們需要發送的消息內容了,我們攔截內容,可以看到大概的內容如下所示。
上面紅色框的內容,必須使用原有的私鑰才能進行解密,也就是在網絡上,被誰攔截了,也無法進行解開,保證了數據的安全性。
3、數據完整性檢查
數據的完整性,我們可以通過消息內容的MD5值進行比對,實現檢查是否內容被篡改過,不過如果是采用了非對稱加密,這種 完整性檢查也可以忽略,不過我們可以保留它作為一個檢查處理。
因此在封裝數據的時候,就把內容部分MD5值計算出來,如下所示。
data.MD5 = MD5Util.GetMD5_32(data.Content);//獲取內容的MD5值
然后在獲得消息,並進行解密后(如果有),那么在服務器端計算一下MD5值,並和傳遞過來的MD5值進行比對,如果一致則說明沒有被篡改過,如下代碼所示。
var md5 = MD5Util.GetMD5_32(message.Content); if (md5 == message.MD5) { OnMessageReceived(message);//給子類重載 } else { Log.WriteInfo(string.Format("收到一個被修改過的消息:\r\n{0}", message.Content)); }
以上就是我在Socket開發框架里面,實現傳輸數據的非對稱加密,以及數據完整性校驗的處理過程。