• 《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 异常

    知识共享许可协议

    本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

    欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

    如有任何疑问,请与我联系 (MingsonZheng@outlook.com) 。

  • 相关阅读:
    java 调用webService的各种方法
    log4j.properties配置详细
    js的with语句使用方法
    Hibernate 拦截器 Hibernate 监听器
    Axis1.4 WebService
    Servlet
    ROOT android 原理。 基于(zergRush)
    创建固定大小的文件 Linux shell 脚本编写实例
    makefile 自动推导命令
    makefile的变量定义和赋值
  • 原文地址:https://www.cnblogs.com/MingsonZheng/p/13521155.html
Copyright © 2020-2023  润新知