Asp.Net Core Identity 隱私數(shù)據(jù)保護(hù)的實(shí)現(xiàn)
前言
Asp.Net Core Identity 是Asp.Net Core 的重要組成部分,他為 Asp.Net Core 甚至其他 .Net Core 應(yīng)用程序提供了一個(gè)簡單易用且易于擴(kuò)展的基礎(chǔ)用戶管理系統(tǒng)框架。它包含了基本的用戶、角色、第三方登錄、Claim等功能,使用 Identity Server 4 可以為其輕松擴(kuò)展 OpenId connection 和 Oauth 2.0 相關(guān)功能。網(wǎng)上已經(jīng)有大量相關(guān)文章介紹,不過這還不是 Asp.Net Core Identity 的全部,其中一個(gè)就是隱私數(shù)據(jù)保護(hù)。
正文
乍一看,隱私數(shù)據(jù)保護(hù)是個(gè)什么東西,感覺好像知道,但又說不清楚。確實(shí)這個(gè)東西光說很難解釋清楚,那就直接上圖:
這是用戶表的一部分,有沒有發(fā)現(xiàn)問題所在?用戶名和 Email 字段變成了一堆看不懂的東西。仔細(xì)看會(huì)發(fā)現(xiàn)這串亂碼好像還有點(diǎn)規(guī)律:guid + 冒號(hào) +貌似是 base64 編碼的字符串,當(dāng)然這串字符串去在線解碼結(jié)果還是一堆亂碼,比如 id 為 1 的 UserName :svBqhhluYZSiPZVUF4baOQ== 在線解碼后是²ðj†na”¢=•T†Ú9 。
這就是隱私數(shù)據(jù)保護(hù),如果沒有這個(gè)功能,那么用戶名是明文存儲(chǔ)的,雖然密碼依然是hash難以破解,但如果被拖庫,用戶數(shù)據(jù)也會(huì)面臨更大的風(fēng)險(xiǎn)。因?yàn)楹芏嗳讼矚g在不同的網(wǎng)站使用相同的賬號(hào)信息進(jìn)行注冊,避免遺忘。如果某個(gè)網(wǎng)站的密碼被盜,其他網(wǎng)站被拖庫,黑客就可以比對是否有相同的用戶名,嘗試撞庫,甚至如果 Email 被盜,黑客還可以看著 Email 用找回密碼把賬號(hào)給 NTR 了。而隱私數(shù)據(jù)保護(hù)就是一層更堅(jiān)實(shí)的后盾,哪怕被拖庫,黑客依然看不懂里面的東西。
然后是這個(gè)格式,基本能想到,冒號(hào)應(yīng)該是分隔符,前面一個(gè) guid,后面是加密后的內(nèi)容。那問題就變成了 guid 又是干嘛的?直接把加密的內(nèi)容存進(jìn)去不就完了。這其實(shí)是微軟開發(fā)框架注重細(xì)節(jié)的最佳體現(xiàn),接下來結(jié)合代碼就能一探究竟。
啟用隱私數(shù)據(jù)保護(hù)
//注冊Identity服務(wù)(使用EF存儲(chǔ),在EF上下文之后注冊) services.AddIdentity<ApplicationUser, ApplicationRole>(options => { //... options.Stores.ProtectPersonalData = true; //在這里啟用隱私數(shù)據(jù)保護(hù) }) //... .AddPersonalDataProtection<AesProtector, AesProtectorKeyRing>(); //在這里配置數(shù)據(jù)加密器,一旦啟用保護(hù),這里必須配置,否則拋出異常
其中的AesProtector 和AesProtectorKeyRing 需要自行實(shí)現(xiàn),微軟并沒有提供現(xiàn)成的類,至少我沒有找到,估計(jì)也是這個(gè)功能冷門的原因吧。.Neter 都被微軟給慣壞了,都是衣來伸手飯來張口。有沒有發(fā)現(xiàn)AesProtectorKeyRing 中有KeyRing 字樣?鑰匙串,恭喜你猜對了,guid 就是這個(gè)鑰匙串中一把鑰匙的編號(hào)。也就是說如果加密的鑰匙被盜,但不是全部被盜,那用戶信息還不會(huì)全部泄露。微軟這一手可真是狠?。?/p>
接下來看看這兩個(gè)類是什么吧。
AesProtector 是 ILookupProtector 的實(shí)現(xiàn)。接口包含兩個(gè)方法,分別用于加密和解密,返回字符串,參數(shù)包含字符串?dāng)?shù)據(jù)和上面那個(gè) guid,當(dāng)然實(shí)際只要是字符串就行, guid 是我個(gè)人的選擇,生成不重復(fù)字符串還是 guid 方便。
AesProtectorKeyRing 則是 ILookupProtectorKeyRing 的實(shí)現(xiàn)。接口包含1、獲取當(dāng)前正在使用的鑰匙編號(hào)的只讀屬性,用于提供加密鑰匙;2、根據(jù)鑰匙編號(hào)獲取字符串的索引器(我這里就是原樣返回的。。。);3、獲取所有鑰匙編號(hào)的方法。
AesProtector
class AesProtector : ILookupProtector { private readonly object _locker; private readonly Dictionary<string, SecurityUtil.AesProtector> _protectors; private readonly DirectoryInfo _dirInfo; public AesProtector(IWebHostEnvironment environment) { _locker = new object(); _protectors = new Dictionary<string, SecurityUtil.AesProtector>(); _dirInfo = new DirectoryInfo($@"{environment.ContentRootPath}\App_Data\AesDataProtectionKey"); } public string Protect(string keyId, string data) { if (data.IsNullOrEmpty()) { return data; } CheckOrCreateProtector(keyId); return _protectors[keyId].Protect(Encoding.UTF8.GetBytes(data)).ToBase64String(); } public string Unprotect(string keyId, string data) { if (data.IsNullOrEmpty()) { return data; } CheckOrCreateProtector(keyId); return Encoding.UTF8.GetString(_protectors[keyId].Unprotect(data.ToBytesFromBase64String())); } private void CheckOrCreateProtector(string keyId) { if (!_protectors.ContainsKey(keyId)) { lock (_locker) { if (!_protectors.ContainsKey(keyId)) { var fileInfo = _dirInfo.GetFiles().FirstOrDefault(d => d.Name == $@"key-{keyId}.xml") ?? throw new FileNotFoundException(); using (var stream = fileInfo.OpenRead()) { XDocument xmlDoc = XDocument.Load(stream); _protectors.Add(keyId, new SecurityUtil.AesProtector(xmlDoc.Element("key")?.Element("encryption")?.Element("masterKey")?.Value.ToBytesFromBase64String() , xmlDoc.Element("key")?.Element("encryption")?.Element("iv")?.Value.ToBytesFromBase64String() , int.Parse(xmlDoc.Element("key")?.Element("encryption")?.Attribute("BlockSize")?.Value) , int.Parse(xmlDoc.Element("key")?.Element("encryption")?.Attribute("KeySize")?.Value) , int.Parse(xmlDoc.Element("key")?.Element("encryption")?.Attribute("FeedbackSize")?.Value) , Enum.Parse<PaddingMode>(xmlDoc.Element("key")?.Element("encryption")?.Attribute("Padding")?.Value) , Enum.Parse<CipherMode>(xmlDoc.Element("key")?.Element("encryption")?.Attribute("Mode")?.Value))); } } } } } }
AesProtectorKeyRing
class AesProtectorKeyRing : ILookupProtectorKeyRing { private readonly object _locker; private readonly Dictionary<string, XDocument> _keyRings; private readonly DirectoryInfo _dirInfo; public AesProtectorKeyRing(IWebHostEnvironment environment) { _locker = new object(); _keyRings = new Dictionary<string, XDocument>(); _dirInfo = new DirectoryInfo($@"{environment.ContentRootPath}\App_Data\AesDataProtectionKey"); ReadKeys(_dirInfo); } public IEnumerable<string> GetAllKeyIds() { return _keyRings.Keys; } public string CurrentKeyId => NewestActivationKey(DateTimeOffset.Now)?.Element("key")?.Attribute("id")?.Value ?? GenerateKey(_dirInfo)?.Element("key")?.Attribute("id")?.Value; public string this[string keyId] => GetAllKeyIds().FirstOrDefault(id => id == keyId) ?? throw new KeyNotFoundException(); private void ReadKeys(DirectoryInfo dirInfo) { foreach (var fileInfo in dirInfo.GetFiles().Where(f => f.Extension == ".xml")) { using (var stream = fileInfo.OpenRead()) { XDocument xmlDoc = XDocument.Load(stream); _keyRings.TryAdd(xmlDoc.Element("key")?.Attribute("id")?.Value, xmlDoc); } } } private XDocument GenerateKey(DirectoryInfo dirInfo) { var now = DateTimeOffset.Now; if (!_keyRings.Any(item => DateTimeOffset.Parse(item.Value.Element("key")?.Element("activationDate")?.Value) <= now && DateTimeOffset.Parse(item.Value.Element("key")?.Element("expirationDate")?.Value) > now)) { lock (_locker) { if (!_keyRings.Any(item => DateTimeOffset.Parse(item.Value.Element("key")?.Element("activationDate")?.Value) <= now && DateTimeOffset.Parse(item.Value.Element("key")?.Element("expirationDate")?.Value) > now)) { var masterKeyId = Guid.NewGuid().ToString(); XDocument xmlDoc = new XDocument(); xmlDoc.Declaration = new XDeclaration("1.0", "utf-8", "yes"); XElement key = new XElement("key"); key.SetAttributeValue("id", masterKeyId); key.SetAttributeValue("version", 1); XElement creationDate = new XElement("creationDate"); creationDate.SetValue(now); XElement activationDate = new XElement("activationDate"); activationDate.SetValue(now); XElement expirationDate = new XElement("expirationDate"); expirationDate.SetValue(now.AddDays(90)); XElement encryption = new XElement("encryption"); encryption.SetAttributeValue("BlockSize", 128); encryption.SetAttributeValue("KeySize", 256); encryption.SetAttributeValue("FeedbackSize", 128); encryption.SetAttributeValue("Padding", PaddingMode.PKCS7); encryption.SetAttributeValue("Mode", CipherMode.CBC); SecurityUtil.AesProtector protector = new SecurityUtil.AesProtector(); XElement masterKey = new XElement("masterKey"); masterKey.SetValue(protector.GenerateKey().ToBase64String()); XElement iv = new XElement("iv"); iv.SetValue(protector.GenerateIV().ToBase64String()); xmlDoc.Add(key); key.Add(creationDate); key.Add(activationDate); key.Add(expirationDate); key.Add(encryption); encryption.Add(masterKey); encryption.Add(iv); xmlDoc.Save( $@"{dirInfo.FullName}\key-{masterKeyId}.xml"); _keyRings.Add(masterKeyId, xmlDoc); return xmlDoc; } return NewestActivationKey(now); } } return NewestActivationKey(now); } private XDocument NewestActivationKey(DateTimeOffset now) { return _keyRings.Where(item => DateTimeOffset.Parse(item.Value.Element("key")?.Element("activationDate")?.Value) <= now && DateTimeOffset.Parse(item.Value.Element("key")?.Element("expirationDate")?.Value) > now) .OrderByDescending(item => DateTimeOffset.Parse(item.Value.Element("key")?.Element("expirationDate")?.Value)).FirstOrDefault().Value; } }
這兩個(gè)類也是注冊到 Asp.Net Core DI 中的服務(wù),所有 DI 的功能都支持。
在其中我還使用了我在其他地方寫的底層基礎(chǔ)工具類,如果想看完整實(shí)現(xiàn)可以去我的 Github 克隆代碼實(shí)際運(yùn)行并體驗(yàn)。在這里大致說一下這兩個(gè)類的設(shè)計(jì)思路。既然微軟設(shè)計(jì)了鑰匙串功能,那自然是要利用好。我在代碼里寫死每個(gè)鑰匙有效期90天,過期后會(huì)自動(dòng)生成并使用新的鑰匙,鑰匙的詳細(xì)信息使用xml文檔保存在項(xiàng)目文件夾中,具體見下面的截圖。Identity 會(huì)使用最新鑰匙進(jìn)行加密并把鑰匙編號(hào)一并存入數(shù)據(jù)庫,在讀取時(shí)會(huì)根據(jù)編號(hào)找到對應(yīng)的加密器解密數(shù)據(jù)。這個(gè)過程由 EF Core 的值轉(zhuǎn)換器(EF Core 2.1 增加)完成,也就是說 Identity 向 DbContext 中需要加密的字段注冊了值轉(zhuǎn)換器。所以我也不清楚早期 Identity 有沒有這個(gè)功能,不使用 EF Core 的情況下這個(gè)功能是否可用。
如果希望對自定義用戶數(shù)據(jù)進(jìn)行保護(hù),為對應(yīng)屬性標(biāo)注 [PersonalData] 特性即可。Identity 已經(jīng)對內(nèi)部的部分屬性進(jìn)行了標(biāo)記,比如上面提到的 UserName 。
有幾個(gè)要特別注意的點(diǎn):
1、在有數(shù)據(jù)的情況下不要隨便開啟或關(guān)閉數(shù)據(jù)保護(hù)功能,否則可能導(dǎo)致嚴(yán)重后果。
2、鑰匙一定要保護(hù)好,保存好。否則可能泄露用戶數(shù)據(jù)或者再也無法解密用戶數(shù)據(jù),從刪庫到跑路那種 Shift + Del 的事千萬別干。
3、被保護(hù)的字段無法在數(shù)據(jù)庫端執(zhí)行模糊搜索,只能精確匹配。如果希望進(jìn)行數(shù)據(jù)分析,只能先用 Identity 把數(shù)據(jù)讀取到內(nèi)存才能繼續(xù)做其他事。
4、鑰匙的有效期不宜過短,因?yàn)樵谟脩舻卿洉r(shí) Identity 并不知道用戶是什么時(shí)候注冊的,應(yīng)該用哪個(gè)鑰匙,所以 Identity 會(huì)用所有鑰匙加密一遍然后查找是否有精確匹配的記錄。鑰匙的有效期越短,隨著網(wǎng)站運(yùn)行時(shí)間的增加,鑰匙數(shù)量會(huì)增加,要嘗試的鑰匙也會(huì)跟著增加,最后對系統(tǒng)性能產(chǎn)生影響。當(dāng)然這可以用緩存來緩解。
效果預(yù)覽:
本文地址:https://www.cnblogs.com/coredx/p/12210232.html
完整源代碼:Github
里面有各種小東西,這只是其中之一,不嫌棄的話可以Star一下。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
ASP.NET中DropDownList和ListBox實(shí)現(xiàn)兩級(jí)聯(lián)動(dòng)功能
這篇文章主要介紹了ASP.NET中DropDownList和ListBox實(shí)現(xiàn)兩級(jí)聯(lián)動(dòng)功能的相關(guān)資料,需要的朋友可以參考下2016-01-01利用Typings為Visual Studio Code實(shí)現(xiàn)智能提示功能
最近在學(xué)習(xí)Node.js及ThinkJS這個(gè)框架,用vscode作為開發(fā)環(huán)境。默認(rèn)情況下vscode對ThinkJS的代碼提示并不好,所以研究了一下,原來可以同通過Typings來讓vscode擁有強(qiáng)大的智能代碼提示功能。下面本文就介紹了如何利用Typings為Visual Studio Code實(shí)現(xiàn)智能提示功能。2017-02-02使用ASP.NET MVC 4 Async Action+jQuery實(shí)現(xiàn)消息通知機(jī)制的實(shí)現(xiàn)代碼
這兩天在使用Asp.net MVC 4開發(fā)COMET消息通知機(jī)制,在后端使用異步線程對消息進(jìn)行訂閱,客戶端通過AJAX長連接請求MVC中的ACTION2013-02-02Asp.net 獲取指定目錄下的后綴名為".doc" 的所有文件名和文件路徑
Asp.net 獲取指定目錄下的后綴名為“.doc” 的所有文件名和文件路徑,幫寫一個(gè)方法2011-07-07ASP.NET?Core實(shí)時(shí)庫SignalR簡介及使用
這篇文章介紹了ASP.NET?Core實(shí)時(shí)庫SignalR簡介及使用方法,文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-01-01Visual?Studio?2022常見的報(bào)錯(cuò)以及處理方案圖文詳解
許多用戶在使用Visual Studio的過程中常會(huì)遇到各種問題,下面這篇文章主要給大家介紹了關(guān)于Visual?Studio?2022常見的報(bào)錯(cuò)以及處理方案的相關(guān)資料,文中通過圖文介紹的非常詳細(xì),需要的朋友可以參考下2023-04-04.Net使用RabbitMQ即時(shí)發(fā)消息Demo
RabbitMQ是一個(gè)在AMQP基礎(chǔ)上完整的,可復(fù)用的企業(yè)消息系統(tǒng),下面這篇文章主要給大家介紹了關(guān)于.Net使用RabbitMQ即時(shí)發(fā)消息的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2018-07-07