前面文章介紹瞭如何使用Identity在ASP.NET MVC中實現用戶的註冊、登錄以及身份驗證。這些功能都是與用戶信息安全相關的功能,數據安全的重要性永遠放在第一位。那麼對於註冊和登錄功能來說要把密碼及用戶其它信息通過表單的形式安全的提交到伺服器上,那麼最適合的方法就是使用HTTPS(如果有條件或 ...
前面文章介紹瞭如何使用Identity在ASP.NET MVC中實現用戶的註冊、登錄以及身份驗證。這些功能都是與用戶信息安全相關的功能,數據安全的重要性永遠放在第一位。那麼對於註冊和登錄功能來說要把密碼及用戶其它信息通過表單的形式安全的提交到伺服器上,那麼最適合的方法就是使用HTTPS(如果有條件或者有安全需求,應該所有請求都基於HTTPS,本章不涉及HTTPS的介紹),而在註冊時用戶的密碼應該加密後保存在資料庫中,包括登錄時對用戶名的驗證也是對密碼明文加密後再進行匹配,對於身份驗證來說,伺服器生成的用戶信息字元串是必須進行加密的,其目的是保護用戶信息並且能夠讓當前的伺服器(或集群)能夠識別。
本章將從以下幾點對Identity中涉及到的加解密進行介紹:
● 常用的加密方法
● .Net對常用加密方法的實現
● Identity用戶密碼的加解密
● Identity用戶身份信息的處理過程
● MachineKey的加解密
● 自定義Identity身份信息的驗證(基於MachineKey)
常用的加密方法
軟體中常用的加密方法分為兩類,一類是密文可解密回明文的,而另一類是密文不可解密的。
對於可解密的這一類主要是通過對稱加密演算法及非對稱加密演算法,如DES、AES、RSA等,它們最主要的特點是需要“密鑰”來進行加解密工作,如果密鑰泄露了,那麼就會造成安全問題。
而不可解密的這一類主要是通過MD5、SHA1這些單向Hash演算法來提取“信息指紋”,已達到“加密”的效果,但這種方法也存在缺陷就是只要演算法相同,那麼對同一個字元串加密後的結果就是相同的,當黑客拿到了用戶資料庫,雖然用戶密碼是被加密存儲,但是黑客可以通過建立“彩虹表”的方式破解密碼。所以又出現了一種通過加“鹽”的演算法,通過加入特殊的“鹽”來保證相同HASH演算法相同字元串的加密不一致性,但如果“鹽”泄露了黑客仍然能夠破解,所以又有了“隨機鹽”。
參考:http://mp.weixin.qq.com/s?__biz=MzIwMzg1ODcwMw==&mid=2247486407&idx=1&sn=51dfbce7d04ab6faeb0f5a27a5bdcbf8&source=41#wechat_redirect
.Net對常用加密方法的實現
.Net的System.Security.Cryptography命名空間下包含了用於加解密的類型,這些類型有些是基於托管代碼的,有些是基於Windows API的。
.Net的加解密類型位於system.dll程式集中(註:非windows平臺下可以通過nuget安裝System.Security.Cryptography.Primitives.dll)的System.Security.Cryptography命名空間下。並且將加密演算法分為了三類:
● 對稱加密演算法:AES、DES等
● 非對稱加密演算法:RSA、ECDH 等
● HASH演算法:SHA1、MD5等
另外要註意的是.Net中使用面向數據流的方式實現了對稱加密和HASH演算法。這樣的設計可以通過串聯的方式將多個加密演算法合併在一起對“數據流”進行操作(這個東西有點類似於owin中間件的方式,可以根據需求動態的對數據加密進行處理)。
微軟官方文檔對加密演算法的使用推薦:
● 數據保護:Aes
● 數據完整性:HMACSHA256、HMACSHA512
● 數字簽名:ECDsa、 RSA
● 密鑰交換:ECDiffieHellman、 RSA
● 隨機數生成:RNGCryptoServiceProvider
● 通過密碼生成Key(使用隨機鹽的Hash演算法):Rfc2898DeriveBytes
更多信息參考文檔:https://docs.microsoft.com/en-us/dotnet/standard/security/cryptography-model
Identity用戶密碼的加解密
用戶的密碼一般來說是一個長度較短的包含各種字元的字元串,而對用戶密碼加密的目的是避免用戶密碼在資料庫中明文存儲,明文存儲密碼會導致系統開發或運營人員對用戶信息安全的威脅以及黑客攻擊數據泄露導致的用戶信息安全。所以一般來說加密密碼使用無法解密的Hash演算法“加密”。
根據前面文章的分析得知,用戶的創建和密碼的匹配都是通過Identity中的UserManager類型完成的:
1. 註冊調用的代碼:
2. 登錄調用的代碼(註:SignInManager位於Microsoft.AspNet.Identity.Owin程式集中):
但通過查看源碼可知,SignInManager實際上也是通過UserManager來匹配密碼的:
3. 所以根據上面的分析,用戶密碼的加密是在UserManager中完成的,而UserManager定義中有一個IPasswordHasher的介面,該介面定義了密碼的Hash加密以及Hash後的密碼校驗:
IPasswordHasher的預設實現是PasswordHasher類型:
從代碼中可以看到PassswordHasher又是通過一個名稱為Crypto類型的靜態方法完成加密和驗證的:
Hash計算:
Hash驗證:
從Crypto的代碼中可以得出以下幾點結論:
1. Identity中預設的密碼加密基於Rfc2898演算法(通過隨機鹽以及設置迭代次數來計算hash值的演算法)。
2. 演算法中的“鹽”長度為16位,迭代計算次數為1000次(註:每次實例化Rfc2898DeriveBytes類型時會根據鹽的長度,創建一個隨機的數組。Rfc2898DeriveBytes的GetBytes的演算法不在此詳解,有興趣可參考文檔和源碼)。
3. 加密時Identity將鹽和加密後的結果進行了拼接,前16位數據為鹽後面的是密碼加密結果。
4. 密碼的“解密”實際上是通過已經加密的結果先獲取其前16位數據拿到鹽,然後再對傳入的密碼和這個鹽進行一次Hash,然後比較兩次的Hash結果是否相同(註:Hash演算法無法解密)。
如果要對Identity中的用戶密碼加密演算法進行變更或者擴展,僅需實現新的IPasswordHasher,然後在創建UserManager實例時將其替換即可。
註:事實上如果黑客拿到了上面數據理論上仍舊是可以破解密碼的,但由於鹽是隨機的,所以導致大批量破解會更加麻煩,這樣哪怕數據泄露了也有時間進行一些補救,所以Rfc2898是一種常用的密碼加密方式。
Identity用戶身份信息的處理過程
Identity的用戶身份信息相對於密碼來說要複雜很多,因為密碼僅僅是一個字元串,對一個字元串的加解密很容易,但是Identity的用戶身份信息實際上是一個AuthenticationTicket實例:
那麼Identity是如何對這個用戶身份信息實例進行處理的呢?
1. 首先我們知道的是Identity通過app.UseCookieAuthentication方法在管道中添加了一個類型為CookieAuthenticationMiddleware的中間件,而通過對源碼分析可以看到,該中間件中實際上是通過創建一個名為CookieAuthenticationHandler的內部類型,通過這個類型完成了請求時Cookie的獲取、驗證,驗證失敗的跳轉以及響應時Cookie的寫入等功能。
其中Cookie的加解密代碼如下:
解密:先獲取Cookie值,然後通過TicketDataFormat的Unprotect方法返回一個AuthenticationTicket實例:
加密:將AuthenticationTicket實例通過TicketDataFormat的Protect方法轉換為一個加密後的字元串。
2. Identity對用戶身份信息的處理主要是通過TicketDataFormat完成,從上面代碼中可以看到TicketDataFormat是來來自Options。這裡的Options實際上就是app.UseCookieAuthentication方法中的參數CookieAuthenticationOptions:
TicketDataFormat預設值是在構造方法中創建的,它需要一個protector(註:Protector實際上就是加解密的組件,本章後面詳解)
3. TicketDataFormat的職責:
由於TicketDataFormat是繼承於SecureDataFormat類型,並且僅僅是在構造方法中硬編碼了傳入基類的參數,所以其功能實際上是基類實現的:
職責一:數據“保護”,先通過序列化器將泛型類型TData進行序列化(這裡的TData實際上是AuthenticationTicket類型),然後通過加密組件對序列化後的二進位進行加密,最後通過編碼器將二進位數據轉換為Base64Url字元串,代碼如下圖:
這裡要註意以下兩點:
1). 序列化器是由TicketDataFormat構造方法中硬編碼的,其真實類型為TicketSerializer(對於序列化這個概念,實際上就是將一個程式中的記憶體實例,用二進位數據或者XML、Json等方式保存下來,然後需要使用的時候在通過這些數據把它反序列化為之前的記憶體實例,這裡的TicketSerializer是一個二進位序列化器):
2). 編碼器的名稱為Base64Url與Base64編碼器的區別是,由於Base64字元串中可能會存在斜杠(/)等特殊符號,但是這些符號在url中是無法被正確識別的,所以Base64Url對這些字元進行了特殊處理:
職責二:數據的“解保護”實際上就是保護功能反過來:先將Base64Url字元串解碼為二進位數據,然後對二進位數據解密,最後對解密後的數據進行反序列化:
而本章的重點實際上是在數據的加解密上,所以protector才是關註重點,這裡的protector從上面的代碼中可以看到是通過IAppBulider創建的:
前面的文章分析過,Owin的核心實際上是一個字典,所以通過Owin來獲取的東西應該是保存在字典中的:
AppBuilder的初始化代碼:
根據上面的分析得出,在沒有指定特殊的數據保護器情況下,Identity使用MachineKeyDataProtector作為預設的數據保護器。
補充說明:
Identity中的身份驗證的原理,實際上是獲取到Cookie成功解密並反序列化為AuthenticationTicket實例後,將通過身份驗證的Identity(該Identity中的IsAuthenticated屬性為true)信息添加到HTTP請求的上下文中的。MVC中需要通過身份驗證的訪問控制就是通過請求上下文中Identity的IsAuthenticated屬性完成判斷的。
MachineKey的加解密
.Net中有一個名為MachineKey的組件,它用於Forms驗證用戶信息、asp.net 的View State以及跨進程的會話狀態數據的加密和驗證,MachineKey可以通過在web.config文件中加入以下的配置文件來對MachineKey的加解密、驗證演算法及其密鑰進行配置,詳情可參考文檔:https://msdn.microsoft.com/en-us/library/w8h3skw9(v=vs.100).aspx
而上面分析知道Identity使用MachineKeyDataProtector作為數據保護器,而MachineKeyDataProtector實際上使用的就是MachineKey:
註:由於MachineKey相關代碼比較複雜,本文中僅對其主要的一些對象以及加解密過程進行介紹:
MachineKey的主要相關對象:
● AspNetCryptoServiceProvider(內部類型):ASP.NET用其獲取適合的加密組件。
● MachineKeySection:用於表示MachineKey的配置信息。
● MachineKeyCryptoAlgorithmFactory(內部類型):MachineKey的加密演算法工廠,依賴MachineKeySection,可以從配置文件中獲取加密演算法類型。
● MachineKeyMasterKeyProvider(內部類型):密鑰提供器,依賴MachineKeySection,可以從配置文件中獲取密鑰信息。
● MachineKeyDataProtectorFactory (內部類型):數據保護器工廠,用於創建自定義加解密類型(配置文件中可以通過alg:algorithm_name方法使用自定義的加密演算法)。
● Purpose(內部類型):用於根據加密目的來生成真正用於加密和校驗的密鑰,Identity使用的目的為User_MacineKey_Protect,User_MacineKey_Protect的主目的為User.MachineKey.Protect,特殊目的為"Microsoft.Owin.Security.Cookies.CookieAuthenticationMiddleware", "ApplicationCookie","v1"(數據來自源碼分析)。換句話說如果密鑰相同,但是加密目的不一樣,那麼真實用於加解密的密鑰也是不同的。
上圖為Purpose的定義,從定義中也可以看出針對功能的不同如Forms驗證的、角色信息的以及WebForm中一系列組件的目的均不相同。
● NetFXCryptoService(內部類型):MachineKey在.Net平臺下使用的加解密服務組件。也是Identity中使用的身份信息加解密組件。
以下代碼為NetFXCryptoService加解密的演算法,其演算法包括了數據加解密以及數據完整性校驗兩個部分:
加密:
1 public byte[] Protect(byte[] clearData) //claerData為需要加密的二進位數據 2 { 3 byte[] buffer4; 4 using (SymmetricAlgorithm algorithm = this._cryptoAlgorithmFactory.GetEncryptionAlgorithm()) //通過工廠獲取加密演算法,實際上就是使用預設的或配置文件指定的如AES等 5 { 6 algorithm.Key = this._encryptionKey.GetKeyMaterial();//Purpose通過配置文件獲取加密密鑰並根據實際目的派生出來的真實密鑰 7 if (this._predictableIV) 8 { 9 algorithm.IV = CryptoUtil.CreatePredictableIV(clearData, algorithm.BlockSize); 10 } 11 else 12 { 13 algorithm.GenerateIV(); 14 } 15 byte[] iV = algorithm.IV; 16 using (MemoryStream stream = new MemoryStream()) 17 { 18 stream.Write(iV, 0, iV.Length); 19 using (ICryptoTransform transform = algorithm.CreateEncryptor()) 20 { 21 using (CryptoStream stream2 = new CryptoStream(stream, transform, CryptoStreamMode.Write)) 22 { 23 stream2.Write(clearData, 0, clearData.Length); 24 stream2.FlushFinalBlock(); 25 using (KeyedHashAlgorithm algorithm2 = this._cryptoAlgorithmFactory.GetValidationAlgorithm())//通過工廠獲取數據校驗的演算法,該演算法在配置文件中配置,如SHA1等 26 { 27 algorithm2.Key = this._validationKey.GetKeyMaterial();//Purpose通過配置文件獲取的數據校驗密鑰並根據實際目的派生出來的真實密鑰 28 byte[] buffer = algorithm2.ComputeHash(stream.GetBuffer(), 0, (int) stream.Length); 29 stream.Write(buffer, 0, buffer.Length); 30 buffer4 = stream.ToArray(); 31 } 32 } 33 } 34 } 35 } 36 return buffer4; 37 }View Code
解密(加密的反過程):
1 public byte[] Unprotect(byte[] protectedData) 2 { 3 byte[] buffer3; 4 using (SymmetricAlgorithm algorithm = this._cryptoAlgorithmFactory.GetEncryptionAlgorithm()) 5 { 6 algorithm.Key = this._encryptionKey.GetKeyMaterial(); 7 using (KeyedHashAlgorithm algorithm2 = this._cryptoAlgorithmFactory.GetValidationAlgorithm()) 8 { 9 algorithm2.Key = this._validationKey.GetKeyMaterial(); 10 int offset = algorithm.BlockSize / 8; 11 int num2 = algorithm2.HashSize / 8; 12 int count = (protectedData.Length - offset) - num2; 13 if (count <= 0) 14 { 15 return null; 16 } 17 byte[] buffer = algorithm2.ComputeHash(protectedData, 0, offset + count); 18 if (!CryptoUtil.BuffersAreEqual(protectedData, offset + count, num2, buffer, 0, buffer.Length)) 19 { 20 buffer3 = null; 21 } 22 else 23 { 24 byte[] dst = new byte[offset]; 25 Buffer.BlockCopy(protectedData, 0, dst, 0, dst.Length); 26 algorithm.IV = dst; 27 using (MemoryStream stream = new MemoryStream()) 28 { 29 using (ICryptoTransform transform = algorithm.CreateDecryptor()) 30 { 31 using (CryptoStream stream2 = new CryptoStream(stream, transform, CryptoStreamMode.Write)) 32 { 33 stream2.Write(protectedData, offset, count); 34 stream2.FlushFinalBlock(); 35 buffer3 = stream.ToArray(); 36 } 37 } 38 } 39 } 40 } 41 } 42 return buffer3; 43 }View Code
自定義Identity身份信息的驗證(基於MachineKey)
本例將在Controller的Action方法中獲取登錄生成的Cookie值,並將其解密後反序列化成AuthenticactionTicket實例:
代碼:
1 public ActionResult Index() 2 { 3 //1.從Cookie中獲取加密後的用戶信息字元串 4 var cookieStr = this.HttpContext.Request.Cookies[".AspNet.ApplicationCookie"].Value.ToString(); 5 //2.將用戶信息字元串以Base64Url的方式轉換為二進位數據 6 var cookieBytes = TextEncodings.Base64Url.Decode(cookieStr); 7 //3.轉換後的二進位數據通過MachineKey進行解密(註:MachinKey預設使用User_MacineKey_Protect為主目的, 8 //特殊目的由Owin Cookie驗證中間件提供) 9 var result = MachineKey.Unprotect(cookieBytes, 10 new string[] { "Microsoft.Owin.Security.Cookies.CookieAuthenticationMiddleware", 11 "ApplicationCookie", 12 "v1"}); 13 TicketSerializer ticketSerializer = new TicketSerializer(); 14 //4.將解密後的二進位數據反序列化為AuthenticationTicket實例 15 var ticket = ticketSerializer.Deserialize(result); 16 17 return View(); 18 }View Code
登錄後的運行結果:
註:MachineKey可以通過配置文件來改變加解密以及數據驗證的演算法及密鑰,該配置文件可以通過IIS的“電腦密鑰”功能來實現:
小結
本章在軟體開發中常用的加密演算法及其在.Net中的應用介紹的基礎上,引出了Identity中用戶密碼以及用戶信息的加解密的過程與方法,其中用戶密碼的加解密較為簡單,而用戶信息作為一個複雜的對象實例,在加解密之前還需要進行序列化與反序列的流程,另外也得知了對於用戶信息的保護不僅僅是加密而且還附帶了數據完整性驗證功能。數據安全是一個非常重要的話題,而Identity的身份驗證是預設ASP.NET MVC帶有獨立身份驗證模板提供的功能,一個一分鐘就能創建的應用程式模板就提供瞭如此複雜的用戶數據安全保護功能,由此可見.Net的強大之處。
另外本章除了介紹Identity,實際上也是介紹了一種數據保護以及身份驗證的方式,在沒有使用Identity的情況下,仍舊可以使用其理念來打造一個符合自身需求的數據保護方案。
參考:
https://docs.microsoft.com/en-us/dotnet/standard/security/cryptography-model
https://msdn.microsoft.com/en-us/library/ff648652.aspx
https://www.rfc-editor.org/rfc/rfc2898.txt
https://www.codeproject.com/articles/16645/asp-net-machinekey-generator
http://www.cnblogs.com/happyhippy/archive/2006/12/23/601353.html
http://mp.weixin.qq.com/s?__biz=MzIwMzg1ODcwMw==&mid=2247486407&idx=1&sn=51dfbce7d04ab6faeb0f5a27a5bdcbf8&source=41#wechat_redirect
本文鏈接:http://www.cnblogs.com/selimsong/p/7771875.html