Asp.NetCoreIdentity隱私數(shù)據(jù)保護(hù)的實現(xiàn)方法-創(chuàng)新互聯(lián)

這篇文章給大家分享的是有關(guān)Asp.Net Core Identity隱私數(shù)據(jù)保護(hù)的實現(xiàn)方法的內(nèi)容。小編覺得挺實用的,因此分享給大家做個參考,一起跟隨小編過來看看吧。

成都創(chuàng)新互聯(lián)公司專注為客戶提供全方位的互聯(lián)網(wǎng)綜合服務(wù),包含不限于網(wǎng)站設(shè)計制作、做網(wǎng)站、婁煩網(wǎng)絡(luò)推廣、微信小程序開發(fā)、婁煩網(wǎng)絡(luò)營銷、婁煩企業(yè)策劃、婁煩品牌公關(guān)、搜索引擎seo、人物專訪、企業(yè)宣傳片、企業(yè)代運(yùn)營等,從售前售中售后,我們都將竭誠為您服務(wù),您的肯定,是我們大的嘉獎;成都創(chuàng)新互聯(lián)公司為所有大學(xué)生創(chuàng)業(yè)者提供婁煩建站搭建服務(wù),24小時服務(wù)熱線:13518219792,官方網(wǎng)址:muchs.cn

前言

Asp.Net Core Identity 是Asp.Net Core 的重要組成部分,他為 Asp.Net Core 甚至其他 .Net Core 應(yīng)用程序提供了一個簡單易用且易于擴(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 的全部,其中一個就是隱私數(shù)據(jù)保護(hù)。

正文

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

Asp.Net Core Identity隱私數(shù)據(jù)保護(hù)的實現(xiàn)方法

這是用戶表的一部分,有沒有發(fā)現(xiàn)問題所在?用戶名和 Email 字段變成了一堆看不懂的東西。仔細(xì)看會發(fā)現(xiàn)這串亂碼好像還有點規(guī)律:guid + 冒號 +貌似是 base64 編碼的字符串,當(dāng)然這串字符串去在線解碼結(jié)果還是一堆亂碼,比如 id 為 1 的 UserName :svBqhhluYZSiPZVUF4baOQ== 在線解碼后是²ðj?na”¢=?T?Ú9 。

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

然后是這個格式,基本能想到,冒號應(yīng)該是分隔符,前面一個 guid,后面是加密后的內(nèi)容。那問題就變成了 guid 又是干嘛的?直接把加密的內(nèi)容存進(jìn)去不就完了。這其實是微軟開發(fā)框架注重細(xì)節(jié)的很好體現(xiàn),接下來結(jié)合代碼就能一探究竟。

啟用隱私數(shù)據(jù)保護(hù)

 //注冊Identity服務(wù)(使用EF存儲,在EF上下文之后注冊)
services.AddIdentity<ApplicationUser, ApplicationRole>(options =>
{
 //...
 options.Stores.ProtectPersonalData = true; //在這里啟用隱私數(shù)據(jù)保護(hù)
})
//...
.AddPersonalDataProtection<AesProtector, AesProtectorKeyRing>(); //在這里配置數(shù)據(jù)加密器,一旦啟用保護(hù),這里必須配置,否則拋出異常

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

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

AesProtector 是 ILookupProtector 的實現(xiàn)。接口包含兩個方法,分別用于加密和解密,返回字符串,參數(shù)包含字符串?dāng)?shù)據(jù)和上面那個 guid,當(dāng)然實際只要是字符串就行, guid 是我個人的選擇,生成不重復(fù)字符串還是 guid 方便。

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

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;
    }
  }

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

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

如果希望對自定義用戶數(shù)據(jù)進(jìn)行保護(hù),為對應(yīng)屬性標(biāo)注 [PersonalData] 特性即可。Identity 已經(jīng)對內(nèi)部的部分屬性進(jìn)行了標(biāo)記,比如上面提到的 UserName 。

有幾個要特別注意的點:

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、鑰匙的有效期不宜過短,因為在用戶登錄時 Identity 并不知道用戶是什么時候注冊的,應(yīng)該用哪個鑰匙,所以 Identity 會用所有鑰匙加密一遍然后查找是否有精確匹配的記錄。鑰匙的有效期越短,隨著網(wǎng)站運(yùn)行時間的增加,鑰匙數(shù)量會增加,要嘗試的鑰匙也會跟著增加,最后對系統(tǒng)性能產(chǎn)生影響。當(dāng)然這可以用緩存來緩解。

效果預(yù)覽:

Asp.Net Core Identity隱私數(shù)據(jù)保護(hù)的實現(xiàn)方法

Asp.Net Core Identity隱私數(shù)據(jù)保護(hù)的實現(xiàn)方法

感謝各位的閱讀!關(guān)于“Asp.Net Core Identity隱私數(shù)據(jù)保護(hù)的實現(xiàn)方法”這篇文章就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,讓大家可以學(xué)到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!

網(wǎng)站欄目:Asp.NetCoreIdentity隱私數(shù)據(jù)保護(hù)的實現(xiàn)方法-創(chuàng)新互聯(lián)
鏈接URL:http://muchs.cn/article22/dejejc.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站收錄、微信小程序、網(wǎng)站營銷移動網(wǎng)站建設(shè)、微信公眾號、用戶體驗

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)

成都做網(wǎng)站