天天看點

《ASP.NET Core 與 RESTful API 開發實戰》-- (第8章)-- 讀書筆記(下)

第 8 章 認證和安全

8.3 HTTPS

HTTP 協定能夠在用戶端和伺服器之間傳遞資訊,特點是以明文的方式發送内容,并不提供任何方式的資料加密

為了解決 HTTP 協定這一缺陷,需要使用另一種協定:HTTPS,它在 HTTP 的基礎上加入了安全套接層 SSL 協定

SSL 層依靠證書來驗證伺服器的身份,并在傳輸層為浏覽器和伺服器之間的通信加密

自 ASP.NET Core 2.1 起,在預設情況下,所建立的 ASP.NET Core 應用程式都啟用了 HTTPS

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseHsts();
    }

    app.UseHttpsRedirection();

    。。。
}
      

在 launchSettings.json 配置檔案中也包含了 HTTPS 端口配置

"sslPort": 44304

"applicationUrl": "https://localhost:5001;http://localhost:5000",
      

HTTPS 重定向中間件會将所有的非安全請求重定向到安全的 HTTPS 協定上,它使用 HttpsRedirectionOptions 對象中的配置來進行重定向

namespace Microsoft.AspNetCore.HttpsPolicy
{
  public class HttpsRedirectionOptions
  {
    public int RedirectStatusCode { get; set; } = 307;// 用于設定重定向時的狀态碼,預設值307 Temporary Redirect

    public int? HttpsPort { get; set; }// 重定向URL中要用到的端口号
  }
}
      

若要修改重定向選項,則可以在 ConfigureServices 方法中添加如下代碼

services.AddHttpsRedirection(option =>
    {
        option.RedirectStatusCode = StatusCodes.Status307TemporaryRedirect;
        option.HttpsPort = 5001;
    }
);
      

HSTS 中間件使用 HSTS 來進一步保證用戶端和伺服器之間資料傳輸的安全,作用是強制用戶端使用 HTTPS 與伺服器建立連結,實作方式是在響應消息中添加 Strict-Transport-Security 消息頭,該消息頭可以使浏覽器在接下來指定的時間内,強制目前域名隻能通過 HTTPS 進行通路

services.AddHsts(options =>
{
    options.IncludeSubDomains = true;// 表明該網站所有子域名也必須通過HTTPS協定來通路
    options.Preload = true;// 可選參數,隻有在申請将目前網站的域名加入浏覽器内置清單時,才需要使用它
    options.MaxAge = TimeSpan.FromDays(120);// 指定時間内,這個網站必須通過HTTPS協定來通路
    options.ExcludedHosts.Clear();// 由于本地伺服器不會使用HTTPS,為了檢視效果,需要清除所有被排除的主機清單
});
      

之是以應該在正式環境中使用 HSTS,是因為 HSTS 配置會被浏覽器緩存,是以不建議在開發環境中使用 HSTS

8.4 資料保護

Web 應用程式通常需要存儲安全敏感資料,ASP.NET Core 提供了資料保護 API,用于加密和解密資料功能

資料保護 API 主要包含兩個接口:IDataProtectionProvider 與 IDataProtector

IDataProtectionProvider 接口主要用于建立 IDataProtector 類型對象

namespace Microsoft.AspNetCore.DataProtection
{
  public interface IDataProtectionProvider
  {
    IDataProtector CreateProtector(string purpose);
  }
}
      

IDataProtector 接口用于執行實際的資料保護操作

namespace Microsoft.AspNetCore.DataProtection
{
  public interface IDataProtector : IDataProtectionProvider
  {
    byte[] Protect(byte[] plaintext);

    byte[] Unprotect(byte[] protectedData);
  }
}
      

為了友善使用上述兩個接口,在相同的命名空間中還包含了為它們定義的擴充方法

namespace Microsoft.AspNetCore.DataProtection
{
  public static class DataProtectionCommonExtensions
  {
    public static IDataProtector CreateProtector(
      this IDataProtectionProvider provider,
      IEnumerable<string> purposes)
    {
      if (provider == null)
        throw new ArgumentNullException(nameof (provider));
      if (purposes == null)
        throw new ArgumentNullException(nameof (purposes));
      bool flag = true;
      IDataProtectionProvider protectionProvider = provider;
      foreach (string purpose in purposes)
      {
        if (purpose == null)
          throw new ArgumentException(Resources.DataProtectionExtensions_NullPurposesCollection, nameof (purposes));
        protectionProvider = (IDataProtectionProvider) (protectionProvider.CreateProtector(purpose) ?? CryptoUtil.Fail<IDataProtector>("CreateProtector returned null."));
        flag = false;
      }
      if (flag)
        throw new ArgumentException(Resources.DataProtectionExtensions_NullPurposesCollection, nameof (purposes));
      return (IDataProtector) protectionProvider;
    }

    public static IDataProtector CreateProtector(
      this IDataProtectionProvider provider,
      string purpose,
      params string[] subPurposes)
    {
      if (provider == null)
        throw new ArgumentNullException(nameof (provider));
      if (purpose == null)
        throw new ArgumentNullException(nameof (purpose));
      IDataProtector provider1 = provider.CreateProtector(purpose);
      if (subPurposes != null && subPurposes.Length != 0)
        provider1 = provider1 != null ? provider1.CreateProtector((IEnumerable<string>) subPurposes) : (IDataProtector) null;
      return provider1 ?? CryptoUtil.Fail<IDataProtector>("CreateProtector returned null.");
    }

    public static IDataProtectionProvider GetDataProtectionProvider(
      this IServiceProvider services)
    {
      if (services == null)
        throw new ArgumentNullException(nameof (services));
      IDataProtectionProvider service = (IDataProtectionProvider) services.GetService(typeof (IDataProtectionProvider));
      if (service != null)
        return service;
      throw new InvalidOperationException(Resources.FormatDataProtectionExtensions_NoService((object) typeof (IDataProtectionProvider).FullName));
    }

    public static IDataProtector GetDataProtector(
      this IServiceProvider services,
      IEnumerable<string> purposes)
    {
      if (services == null)
        throw new ArgumentNullException(nameof (services));
      if (purposes == null)
        throw new ArgumentNullException(nameof (purposes));
      return services.GetDataProtectionProvider().CreateProtector(purposes);
    }

    public static IDataProtector GetDataProtector(
      this IServiceProvider services,
      string purpose,
      params string[] subPurposes)
    {
      if (services == null)
        throw new ArgumentNullException(nameof (services));
      if (purpose == null)
        throw new ArgumentNullException(nameof (purpose));
      return services.GetDataProtectionProvider().CreateProtector(purpose, subPurposes);
    }

    public static string Protect(this IDataProtector protector, string plaintext)
    {
      if (protector == null)
        throw new ArgumentNullException(nameof (protector));
      if (plaintext == null)
        throw new ArgumentNullException(nameof (plaintext));
      try
      {
        byte[] bytes = EncodingUtil.SecureUtf8Encoding.GetBytes(plaintext);
        return WebEncoders.Base64UrlEncode(protector.Protect(bytes));
      }
      catch (Exception ex) when (ex.RequiresHomogenization())
      {
        throw Error.CryptCommon_GenericError(ex);
      }
    }

    public static string Unprotect(this IDataProtector protector, string protectedData)
    {
      if (protector == null)
        throw new ArgumentNullException(nameof (protector));
      if (protectedData == null)
        throw new ArgumentNullException(nameof (protectedData));
      try
      {
        byte[] protectedData1 = WebEncoders.Base64UrlDecode(protectedData);
        byte[] bytes = protector.Unprotect(protectedData1);
        return EncodingUtil.SecureUtf8Encoding.GetString(bytes);
      }
      catch (Exception ex) when (ex.RequiresHomogenization())
      {
        throw Error.CryptCommon_GenericError(ex);
      }
    }
  }
}
      

前兩個方法用于根據多個目的的字元串來建立 IDataProtector,後兩個方法使用 IDataProtector 的 Protect 和 Unprotect 方法能夠接受并傳回字元串

要在程式中使用資料保護 API,需要先添加服務

services.AddDataProtection();
      

之後,在需要的位置,将 IDataProtectionProvider 接口注入即可

namespace WebApplication1.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ValueController : Controller
    {
        private List<Student> students = new List<Student>();
        public IDataProtectionProvider DataProtectionProvider { get; set; }

        public ValueController(IDataProtectionProvider dataProtectionProvider)
        {
            DataProtectionProvider = dataProtectionProvider;
            students.Add(new Student
            {
                Id = "1",
                Name = "Jim"
            });
        }

        [HttpGet]
        public ActionResult<IEnumerable<Student>> Get()
        {
            var protector = DataProtectionProvider.CreateProtector("ProtectResourceId");
            var result = students.Select(s => new Student
            {
                Id = protector.Protect(s.Id),// 加密
                Name = s.Name
            });

            return result.ToList();
        }

        [HttpGet]
        public ActionResult<Student> Get(string id)
        {
            var protector = DataProtectionProvider.CreateProtector("ProtectResourceId");
            var rawId = protector.Unprotect(id);// 解密
            var targetItem = students.FirstOrDefault(s => s.Id == rawId);
            return new Student {Id = id, Name = targetItem.Name};
        }
    }

    public class Student
    {
        public string Id { get; set; }

        public string Name { get; set; }
    }
}
      

由于 IDataProtector 接口同樣可同于建立 IDataProtector 對象,是以可以建立具有層次的 IDataProtector 對象

var protectorA = DataProtectionProvider.CreateProtector("A");
var protectorB = protectorA.CreateProtector("B");
var protectorC = protectorB.CreateProtector("C");
      

需要注意的是,在對資料解密時,必須使用與加密時相同的方式建立的 IDataProtector 對象

為了更友善地建立具有層次的 IDataProtector 對象,可以使用如下 IDataProtectionProvider 接口的擴充方法

DataProtectionProvider.CreateProtector("Parent", "Child");
      

如果使用上述 protectorC 對象加密資訊,則可以使用如下方式進行解密

var content = protectorC.Protect("Hello");
var protector = DataProtectionProvider.CreateProtector("A", "B", "C");
var rawContent = protector.Unprotect(content);
      

使用 protectorC 加密的内容,可以使用 CreateProtector("A", "B", "C") 建立的 IDataProtector 進行解密。這種具有層次的 IDataProtector 在根據不同版本或不同使用者保護資料時非常友善

var protectV1 = DataProtectionProvider.CreateProtector("DemoApp.ValueController", "v1");
var protectV2 = DataProtectionProvider.CreateProtector("DemoApp.ValueController", "v2");
      

為資料加密設定有效時間,在 Microsoft.AspNetCore.DataProtection 包中為 IDataProtector 接口定義了一個擴充方法

public static ITimeLimitedDataProtector ToTimeLimitedDataProtector(
  this IDataProtector protector)
{
  if (protector == null)
    throw new ArgumentNullException(nameof (protector));
  return protector is ITimeLimitedDataProtector limitedDataProtector ? limitedDataProtector : (ITimeLimitedDataProtector) new TimeLimitedDataProtector(protector);
}
      

該方法能夠将 IDataProtector 對象轉換為 ITimeLimitedDataProtector 類型的對象,為密文增加有效時間

ITimeLimitedDataProtector 接口定義如下

namespace Microsoft.AspNetCore.DataProtection
{
  public interface ITimeLimitedDataProtector : IDataProtector, IDataProtectionProvider
  {
    ITimeLimitedDataProtector CreateProtector(string purpose);

    byte[] Protect(byte[] plaintext, DateTimeOffset expiration);

    byte[] Unprotect(byte[] protectedData, out DateTimeOffset expiration);
  }
}
      

DateTimeOffset 類型參數表示有效期

以下示例展示了 ITimeLimitedDataProtector 的使用方法

var protector = DataProtectionProvider.CreateProtector("testing").ToTimeLimitedDataProtector();
var content = protector.Protect("Hello", DateTimeOffset.Now.AddMinutes(10));
// 等待一段時間
try
{
    var rawContent = protector.Unprotect(content, out DateTimeOffset expiration);
}
catch (CryptographicException ex)
{
    Logger.logError(ex.Message, ex);
}
      

Microsoft.AspNetCore.DataProtection 包中還提供了 EphemeralDataProtectionProvider 類,作為 IDataProtectionProvider 接口的一個實作,它的加密和解密功能具有“一次性”的特點,當密文不需要持久化時,可以使用這種方式

private void EphemeralDataProtectionTest()
{
    const string Purpose = "DemoPurpose";

    EphemeralDataProtectionProvider provider = new EphemeralDataProtectionProvider();
    var protector = provider.CreateProtector(Purpose);
    var content = protector.Protect("Hello");
    var rawContent = protector.Unprotect(content);

    EphemeralDataProtectionProvider provider2 = new EphemeralDataProtectionProvider();
    var protector2 = provider2.CreateProtector(Purpose);
    rawContent = protector2.Unprotect(content);// 這裡會出現異常
}
      

對于第二個 EphemeralDataProtectionProvider 盡管建立了 IDataProtector 時,使用了相同的字元串,但由于是不同的執行個體,是以嘗試解密第一個對象加密的内容時,将會出錯,抛出 CryptographicException 異常

《ASP.NET Core 與 RESTful API 開發實戰》-- (第8章)-- 讀書筆記(下)