Asp.Net Core Identity 隱私數據保護

前言

       Asp.Net Core Identity 是 Asp.Net Core 的重要組成部分,他爲 Asp.Net Core 甚至其他 .Net Core 應用程序提供了一個簡單易用且易於擴展的基礎用戶管理系統框架。它包含了基本的用戶、角色、第三方登錄、Claim等功能,使用 Identity Server 4 可以爲其輕鬆擴展 OpenId connection 和 Oauth 2.0 相關功能。網上已經有大量相關文章介紹,不過這還不是 Asp.Net Core Identity 的全部,其中一個就是隱私數據保護。

正文

       乍一看,隱私數據保護是個什麼東西,感覺好像知道,但又說不清楚。確實這個東西光說很難解釋清楚,那就直接上圖:

       這是用戶表的一部分,有沒有發現問題所在?用戶名和 Email 字段變成了一堆看不懂的東西。仔細看會發現這串亂碼好像還有點規律:guid + 冒號 + 貌似是 base64 編碼的字符串,當然這串字符串去在線解碼結果還是一堆亂碼,比如 id 爲 1 的 UserName :svBqhhluYZSiPZVUF4baOQ== 在線解碼後是 ²ðj†na”¢=•T†Ú9 。

       這就是隱私數據保護,如果沒有這個功能,那麼用戶名是明文存儲的,雖然密碼依然是hash難以破解,但如果被拖庫,用戶數據也會面臨更大的風險。因爲很多人喜歡在不同的網站使用相同的賬號信息進行註冊,避免遺忘。如果某個網站的密碼被盜,其他網站被拖庫,黑客就可以比對是否有相同的用戶名,嘗試撞庫,甚至如果 Email 被盜,黑客還可以看着 Email 用找回密碼把賬號給 NTR 了。而隱私數據保護就是一層更堅實的後盾,哪怕被拖庫,黑客依然看不懂裏面的東西。

       然後是這個格式,基本能想到,冒號應該是分隔符,前面一個 guid,後面是加密後的內容。那問題就變成了 guid 又是幹嘛的?直接把加密的內容存進去不就完了。這其實是微軟開發框架注重細節的最佳體現,接下來結合代碼就能一探究竟。

       啓用隱私數據保護

1 //註冊Identity服務(使用EF存儲,在EF上下文之後註冊)
2 services.AddIdentity<ApplicationUser, ApplicationRole>(options =>
3 {
4   //...
5   options.Stores.ProtectPersonalData = true; //在這裏啓用隱私數據保護
6 })
7 //...
8 .AddPersonalDataProtection<AesProtector, AesProtectorKeyRing>(); //在這裏配置數據加密器,一旦啓用保護,這裏必須配置,否則拋出異常

       其中的 AesProtector 和 AesProtectorKeyRing 需要自行實現,微軟並沒有提供現成的類,至少我沒有找到,估計也是這個功能冷門的原因吧。.Neter 都被微軟給慣壞了,都是衣來伸手飯來張口。有沒有發現 AesProtectorKeyRing 中有 KeyRing 字樣?鑰匙串,恭喜你猜對了,guid 就是這個鑰匙串中一把鑰匙的編號。也就是說如果加密的鑰匙被盜,但不是全部被盜,那用戶信息還不會全部泄露。微軟這一手可真是狠啊!

       接下來看看這兩個類是什麼吧。

       AesProtector 是 ILookupProtector 的實現。接口包含兩個方法,分別用於加密和解密,返回字符串,參數包含字符串數據和上面那個 guid,當然實際只要是字符串就行, guid 是我個人的選擇,生成不重複字符串還是 guid 方便。

       AesProtectorKeyRing 則是 ILookupProtectorKeyRing 的實現。接口包含1、獲取當前正在使用的鑰匙編號的只讀屬性,用於提供加密鑰匙;2、根據鑰匙編號獲取字符串的索引器(我這裏就是原樣返回的。。。);3、獲取所有鑰匙編號的方法。

       AesProtector

 1     class AesProtector : ILookupProtector
 2     {
 3         private readonly object _locker;
 4 
 5         private readonly Dictionary<string, SecurityUtil.AesProtector> _protectors;
 6 
 7         private readonly DirectoryInfo _dirInfo;
 8 
 9         public AesProtector(IWebHostEnvironment environment)
10         {
11             _locker = new object();
12 
13             _protectors = new Dictionary<string, SecurityUtil.AesProtector>();
14 
15             _dirInfo = new DirectoryInfo($@"{environment.ContentRootPath}\App_Data\AesDataProtectionKey");
16         }
17 
18         public string Protect(string keyId, string data)
19         {
20             if (data.IsNullOrEmpty())
21             {
22                 return data;
23             }
24 
25             CheckOrCreateProtector(keyId);
26 
27             return _protectors[keyId].Protect(Encoding.UTF8.GetBytes(data)).ToBase64String();
28         }
29 
30         public string Unprotect(string keyId, string data)
31         {
32             if (data.IsNullOrEmpty())
33             {
34                 return data;
35             }
36 
37             CheckOrCreateProtector(keyId);
38 
39             return Encoding.UTF8.GetString(_protectors[keyId].Unprotect(data.ToBytesFromBase64String()));
40         }
41 
42         private void CheckOrCreateProtector(string keyId)
43         {
44             if (!_protectors.ContainsKey(keyId))
45             {
46                 lock (_locker)
47                 {
48                     if (!_protectors.ContainsKey(keyId))
49                     {
50                         var fileInfo = _dirInfo.GetFiles().FirstOrDefault(d => d.Name == $@"key-{keyId}.xml") ??
51                                        throw new FileNotFoundException();
52                         using (var stream = fileInfo.OpenRead())
53                         {
54                             XDocument xmlDoc = XDocument.Load(stream);
55                             _protectors.Add(keyId,
56                                 new SecurityUtil.AesProtector(xmlDoc.Element("key")?.Element("encryption")?.Element("masterKey")?.Value.ToBytesFromBase64String()
57                                     , xmlDoc.Element("key")?.Element("encryption")?.Element("iv")?.Value.ToBytesFromBase64String()
58                                     , int.Parse(xmlDoc.Element("key")?.Element("encryption")?.Attribute("BlockSize")?.Value)
59                                     , int.Parse(xmlDoc.Element("key")?.Element("encryption")?.Attribute("KeySize")?.Value)
60                                     , int.Parse(xmlDoc.Element("key")?.Element("encryption")?.Attribute("FeedbackSize")?.Value)
61                                     , Enum.Parse<PaddingMode>(xmlDoc.Element("key")?.Element("encryption")?.Attribute("Padding")?.Value)
62                                     , Enum.Parse<CipherMode>(xmlDoc.Element("key")?.Element("encryption")?.Attribute("Mode")?.Value)));
63                         }
64                     }
65                 }
66             }
67         }
68     }

       AesProtectorKeyRing

  1     class AesProtectorKeyRing : ILookupProtectorKeyRing
  2     {
  3         private readonly object _locker;
  4         private readonly Dictionary<string, XDocument> _keyRings;
  5         private readonly DirectoryInfo _dirInfo;
  6 
  7         public AesProtectorKeyRing(IWebHostEnvironment environment)
  8         {
  9             _locker = new object();
 10             _keyRings = new Dictionary<string, XDocument>();
 11             _dirInfo = new DirectoryInfo($@"{environment.ContentRootPath}\App_Data\AesDataProtectionKey");
 12 
 13             ReadKeys(_dirInfo);
 14         }
 15 
 16         public IEnumerable<string> GetAllKeyIds()
 17         {
 18             return _keyRings.Keys;
 19         }
 20 
 21         public string CurrentKeyId => NewestActivationKey(DateTimeOffset.Now)?.Element("key")?.Attribute("id")?.Value ?? GenerateKey(_dirInfo)?.Element("key")?.Attribute("id")?.Value;
 22 
 23         public string this[string keyId] =>
 24             GetAllKeyIds().FirstOrDefault(id => id == keyId) ?? throw new KeyNotFoundException();
 25 
 26         private void ReadKeys(DirectoryInfo dirInfo)
 27         {
 28             foreach (var fileInfo in dirInfo.GetFiles().Where(f => f.Extension == ".xml"))
 29             {
 30                 using (var stream = fileInfo.OpenRead())
 31                 {
 32                     XDocument xmlDoc = XDocument.Load(stream);
 33 
 34                     _keyRings.TryAdd(xmlDoc.Element("key")?.Attribute("id")?.Value, xmlDoc);
 35                 }
 36             }
 37         }
 38 
 39         private XDocument GenerateKey(DirectoryInfo dirInfo)
 40         {
 41             var now = DateTimeOffset.Now;
 42             if (!_keyRings.Any(item =>
 43                 DateTimeOffset.Parse(item.Value.Element("key")?.Element("activationDate")?.Value) <= now
 44                 && DateTimeOffset.Parse(item.Value.Element("key")?.Element("expirationDate")?.Value) > now))
 45             {
 46                 lock (_locker)
 47                 {
 48                     if (!_keyRings.Any(item =>
 49                         DateTimeOffset.Parse(item.Value.Element("key")?.Element("activationDate")?.Value) <= now
 50                         && DateTimeOffset.Parse(item.Value.Element("key")?.Element("expirationDate")?.Value) > now))
 51                     {
 52                         var masterKeyId = Guid.NewGuid().ToString();
 53 
 54                         XDocument xmlDoc = new XDocument();
 55                         xmlDoc.Declaration = new XDeclaration("1.0", "utf-8", "yes");
 56 
 57                         XElement key = new XElement("key");
 58                         key.SetAttributeValue("id", masterKeyId);
 59                         key.SetAttributeValue("version", 1);
 60 
 61                         XElement creationDate = new XElement("creationDate");
 62                         creationDate.SetValue(now);
 63 
 64                         XElement activationDate = new XElement("activationDate");
 65                         activationDate.SetValue(now);
 66 
 67                         XElement expirationDate = new XElement("expirationDate");
 68                         expirationDate.SetValue(now.AddDays(90));
 69 
 70                         XElement encryption = new XElement("encryption");
 71                         encryption.SetAttributeValue("BlockSize", 128);
 72                         encryption.SetAttributeValue("KeySize", 256);
 73                         encryption.SetAttributeValue("FeedbackSize", 128);
 74                         encryption.SetAttributeValue("Padding", PaddingMode.PKCS7);
 75                         encryption.SetAttributeValue("Mode", CipherMode.CBC);
 76 
 77                         SecurityUtil.AesProtector protector = new SecurityUtil.AesProtector();
 78                         XElement masterKey = new XElement("masterKey");
 79                         masterKey.SetValue(protector.GenerateKey().ToBase64String());
 80 
 81                         XElement iv = new XElement("iv");
 82                         iv.SetValue(protector.GenerateIV().ToBase64String());
 83 
 84                         xmlDoc.Add(key);
 85                         key.Add(creationDate);
 86                         key.Add(activationDate);
 87                         key.Add(expirationDate);
 88                         key.Add(encryption);
 89                         encryption.Add(masterKey);
 90                         encryption.Add(iv);
 91 
 92                         xmlDoc.Save(
 93                             $@"{dirInfo.FullName}\key-{masterKeyId}.xml");
 94 
 95                         _keyRings.Add(masterKeyId, xmlDoc);
 96 
 97                         return xmlDoc;
 98                     }
 99 
100                     return NewestActivationKey(now);
101                 }
102             }
103 
104             return NewestActivationKey(now);
105         }
106 
107         private XDocument NewestActivationKey(DateTimeOffset now)
108         {
109             return _keyRings.Where(item =>
110                     DateTimeOffset.Parse(item.Value.Element("key")?.Element("activationDate")?.Value) <= now
111                     && DateTimeOffset.Parse(item.Value.Element("key")?.Element("expirationDate")?.Value) > now)
112                 .OrderByDescending(item =>
113                     DateTimeOffset.Parse(item.Value.Element("key")?.Element("expirationDate")?.Value)).FirstOrDefault().Value;
114         }
115     }

       這兩個類也是註冊到 Asp.Net Core DI 中的服務,所有 DI 的功能都支持。

       在其中我還使用了我在其他地方寫的底層基礎工具類,如果想看完整實現可以去我的 Github 克隆代碼實際運行並體驗。在這裏大致說一下這兩個類的設計思路。既然微軟設計了鑰匙串功能,那自然是要利用好。我在代碼裏寫死每個鑰匙有效期90天,過期後會自動生成並使用新的鑰匙,鑰匙的詳細信息使用xml文檔保存在項目文件夾中,具體見下面的截圖。Identity 會使用最新鑰匙進行加密並把鑰匙編號一併存入數據庫,在讀取時會根據編號找到對應的加密器解密數據。這個過程由 EF Core 的值轉換器(EF Core 2.1 增加)完成,也就是說 Identity 向 DbContext 中需要加密的字段註冊了值轉換器。所以我也不清楚早期 Identity 有沒有這個功能,不使用 EF Core 的情況下這個功能是否可用。

       如果希望對自定義用戶數據進行保護,爲對應屬性標註 [PersonalData] 特性即可。Identity 已經對內部的部分屬性進行了標記,比如上面提到的 UserName 。

 

       有幾個要特別注意的點:

       1、在有數據的情況下不要隨便開啓或關閉數據保護功能,否則可能導致嚴重後果。

       2、鑰匙一定要保護好,保存好。否則可能泄露用戶數據或者再也無法解密用戶數據,從刪庫到跑路那種 Shift + Del 的事千萬別幹。

       3、被保護的字段無法在數據庫端執行模糊搜索,只能精確匹配。如果希望進行數據分析,只能先用 Identity 把數據讀取到內存才能繼續做其他事。

       4、鑰匙的有效期不宜過短,因爲在用戶登錄時 Identity 並不知道用戶是什麼時候註冊的,應該用哪個鑰匙,所以 Identity 會用所有鑰匙加密一遍然後查找是否有精確匹配的記錄。鑰匙的有效期越短,隨着網站運行時間的增加,鑰匙數量會增加,要嘗試的鑰匙也會跟着增加,最後對系統性能產生影響。當然這可以用緩存來緩解。

 

效果預覽:

 

       轉載請完整保留以下內容,未經授權刪除以下內容進行轉載盜用的,保留追究法律責任的權利!

  本文地址:https://www.cnblogs.com/coredx/p/12210232.html

  完整源代碼:Github

  裏面有各種小東西,這只是其中之一,不嫌棄的話可以Star一下。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章