前面文章介紹瞭如何使用Identity在ASP.NET MVC中實現用戶的註冊、登陸以及身份驗證。這些功能都是與用戶信息安全相關的功能,數據安全的重要性永遠放在第一位。那麼對於註冊和登陸功能來講要把密碼及用戶其它信息經過表單的形式安全的提交到服務器上,那麼最適合的方法就是使用HTTPS(若是有條件或者有安全需求,應該全部請求都基於HTTPS,本章不涉及HTTPS的介紹),而在註冊時用戶的密碼應該加密後保存在數據庫中,包括登陸時對用戶名的驗證也是對密碼明文加密後再進行匹配,對於身份驗證來講,服務器生成的用戶信息字符串是必須進行加密的,其目的是保護用戶信息而且可以讓當前的服務器(或集羣)可以識別。html
本章將從如下幾點對Identity中涉及到的加解密進行介紹:
● 經常使用的加密方法
● .Net對經常使用加密方法的實現
● Identity用戶密碼的加解密
● Identity用戶身份信息的處理過程
● MachineKey的加解密
● 自定義Identity身份信息的驗證(基於MachineKey)web
軟件中經常使用的加密方法分爲兩類,一類是密文可解密回明文的,而另外一類是密文不可解密的。
對於可解密的這一類主要是經過對稱加密算法及非對稱加密算法,如DES、AES、RSA等,它們最主要的特色是須要「密鑰」來進行加解密工做,若是密鑰泄露了,那麼就會形成安全問題。
而不可解密的這一類主要是經過MD五、SHA1這些單向Hash算法來提取「信息指紋」,已達到「加密」的效果,但這種方法也存在缺陷就是隻要算法相同,那麼對同一個字符串加密後的結果就是相同的,當黑客拿到了用戶數據庫,雖然用戶密碼是被加密存儲,可是黑客能夠經過創建「彩虹表」的方式破解密碼。因此又出現了一種經過加「鹽」的算法,經過加入特殊的「鹽」來保證相同HASH算法相同字符串的加密不一致性,但若是「鹽」泄露了黑客仍然可以破解,因此又有了「隨機鹽」。
參考:http://mp.weixin.qq.com/s?__biz=MzIwMzg1ODcwMw==&mid=2247486407&idx=1&sn=51dfbce7d04ab6faeb0f5a27a5bdcbf8&source=41#wechat_redirect算法
.Net的System.Security.Cryptography命名空間下包含了用於加解密的類型,這些類型有些是基於託管代碼的,有些是基於Windows API的。
.Net的加解密類型位於system.dll程序集中(注:非windows平臺下能夠經過nuget安裝System.Security.Cryptography.Primitives.dll)的System.Security.Cryptography命名空間下。而且將加密算法分爲了三類:
● 對稱加密算法:AES、DES等
● 非對稱加密算法:RSA、ECDH 等
● HASH算法:SHA一、MD5等
另外要注意的是.Net中使用面向數據流的方式實現了對稱加密和HASH算法。這樣的設計能夠經過串聯的方式將多個加密算法合併在一塊兒對「數據流」進行操做(這個東西有點相似於owin中間件的方式,能夠根據需求動態的對數據加密進行處理)。
微軟官方文檔對加密算法的使用推薦:
● 數據保護:Aes
● 數據完整性:HMACSHA25六、HMACSHA512
● 數字簽名:ECDsa、 RSA
● 密鑰交換:ECDiffieHellman、 RSA
● 隨機數生成:RNGCryptoServiceProvider
● 經過密碼生成Key(使用隨機鹽的Hash算法):Rfc2898DeriveBytes
更多信息參考文檔:https://docs.microsoft.com/en-us/dotnet/standard/security/cryptography-model數據庫
用戶的密碼通常來講是一個長度較短的包含各類字符的字符串,而對用戶密碼加密的目的是避免用戶密碼在數據庫中明文存儲,明文存儲密碼會致使系統開發或運營人員對用戶信息安全的威脅以及黑客攻擊數據泄露致使的用戶信息安全。因此通常來講加密密碼使用沒法解密的Hash算法「加密」。
根據前面文章的分析得知,用戶的建立和密碼的匹配都是經過Identity中的UserManager類型完成的:
1. 註冊調用的代碼:windows
2. 登陸調用的代碼(注:SignInManager位於Microsoft.AspNet.Identity.Owin程序集中):數組
但經過查看源碼可知,SignInManager實際上也是經過UserManager來匹配密碼的:安全
3. 因此根據上面的分析,用戶密碼的加密是在UserManager中完成的,而UserManager定義中有一個IPasswordHasher的接口,該接口定義了密碼的Hash加密以及Hash後的密碼校驗:服務器
IPasswordHasher的默認實現是PasswordHasher類型:cookie
從代碼中能夠看到PassswordHasher又是經過一個名稱爲Crypto類型的靜態方法完成加密和驗證的:app
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的用戶身份信息其實是一個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屬性完成判斷的。
.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 }
解密(加密的反過程):
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 }
本例將在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 }
登陸後的運行結果:
注: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