第21章 深入理解IsPersistent、IhttpContextAccessor和UseDefaultServiceProvider

IsPersistent

    微软通过定义2个通用的类及其属性来抽象的存储现实世界中证件的数据,从而为本地身份认证提供数据支撑这2个类分别是:Claim、ClaimsPrincipal。其中为了减少输入操作的失误的产生,微软又定义了ClaimTypes和ClaimValueTypes这两个类为Claim类提供一些常规且通用的数据支撑,所以这两个类都被限定为静态类且有默认的数据值。通过下面的图片你可以直观的理解这些类对实现世界事物抽象实现的逻辑关系

第21章 深入理解IsPersistent、IhttpContextAccessor和UseDefaultServiceProvider_第1张图片 

    如果对上述的数据抽象实现有了深入的理解,那么下现则是对实现世界业务实现抽象的实现的理解。微软在实现本地的身份认证操作实际上作用有2个,1、如果当前程序的数据库中有该用户,则在用户可以在成功执行登录操作后,该用户可以浏览其权限内的授权页面。2、用户执行登录操作后所用户的登录信息分别在服务器端和客户端分别持久化进行保存,在客户端的浏览器中,当前用户执行登录操作且成功后,浏览器会所该用户的身份证信息,(如果“IsPersistent”属性的实例值为:true时)会被持久化存储到该浏览器的Cookies文件中。但是该被持久化存储的身份证信息中由时间限制的,如果在限制时间内重新打关闭的浏览器并直接打开授权页面,浏览器会通过验证的Cookies文件中所存储的用户的身份证信息,从而直接打开该授权页面(该业务实现可能是由服务器端通过session自动执行的)。如果已经错过限制时间,客户端操作系统会在时间过期后自动的删除浏览器的Cookies文件中,所持久化存储的用户的身份证信息,这时重新打关闭的浏览器并直接打开授权页面,由于浏览器会通过验证的Cookies文件中,没有存储的用户的身份证信息,会自动跳转到登录页面。所以微软在实现本地的身份认证操作,也会被大多数开发者称为Cookies身份认证。下图是Chrome浏览器Cookies文件的存储位置,如果客户用Chrome浏览器进行登录操作,且选择“记住我”(IsPersistent=true)操作,则该用户的身份证数据信息会在该文件中持久化保存一段时间。实际上客户如果长时间使用1个浏览器访问网站,则该用户针对不同网站的所有身份证信息都将会被持久化存储到该Cookies文件中,该Cookies文件的大小也会不断的变大。

第21章 深入理解IsPersistent、IhttpContextAccessor和UseDefaultServiceProvider_第2张图片
    下面通过示例程序来深入理解微软身份认证实现的业务逻辑机制。身份认证的数据信息是有时间限制的(主要基于数据安全的考虑),这种时间限制是由3个属性实例值决定的它们别是:IssuedUtc签发时间,相当于身份证中有效期的开始时间和IsPersistent或option.ExpireTimeSpan过期限定时间和IsPersistent=true,在示例程序中我把过期限定时间设置为60秒,即在60秒内第1次登录成功后,关闭浏览器,用户依然不用执行登录操作,可以直接访问其授权页面(该业务实现可能是由服务器端通过session自动执行的)。如果IsPersistent=false,由于身份认证的数据信息没有被持久化到指定的Cookies文件中,即使在限定时间,如果直接访问其授权页面依然会被自动跳转到登录页面。

 第21章 深入理解IsPersistent、IhttpContextAccessor和UseDefaultServiceProvider_第3张图片

 


    第1次执行登录,且选择“记住我”(IsPersistent=true)操作。且选择“记住我”(IsPersistent=true)操作
    从上图可以看出,由于Cookies文件中并没有存储身份认证数据信息所以HttpContext.User.Claims.ToList()方法获取的对象实例的计数为:0。  

第21章 深入理解IsPersistent、IhttpContextAccessor和UseDefaultServiceProvider_第4张图片

第21章 深入理解IsPersistent、IhttpContextAccessor和UseDefaultServiceProvider_第5张图片  

    先关闭浏览器,再开启浏览器,在限定的时间内,第2次执行登录操作,从上图可以看出,由于Cookies文件中已经存储身份认证数据信息所以HttpContext.User.Claims.ToList()方法获取的对象实例的计数为:9。但是如果在限定的时间过期后再开启浏览器,第2次执行登录操作,HttpContext.User.Claims.ToList()方法获取的对象实例的计数依然为:0,因为这时,客户端的操作系统已经从Cookies文件中删除了该持久化存储的身份认证数据信息。

IHttpContextAccessor

    Web程序是基于HTTP或HTTPS协议基础上进行构建的,而于HTTP或HTTPS协议中规则不保存状态数据信息(本人考虑,这是数据安全的基本规则),但是随着需求的发展,由军转民,保障功能的强大,虽然这种安全的基本规则没有改变,但是微软为了减少开发者的工作复杂度,在.Net Framework和.NetCore框架中都内置了HttpContext类及其属性成员(HttpContext?.Items(字典属性,用于存储缓存数据)、HttpContext?.User(用户属性,用于存储登录用户数据信息)、HttpContext?.Request/.Headers[HeaderNames.Host] (字典属性,用于存储主机名、IP地址等))用来存储用户在页面上所保存的数据信息(例如:用户的登录信息、缓存数据信息等)。

    在上1个示例程序IsPersistent中登录用户数据信息是存储在Microsoft.AspNetCore.Mvc.ControllerBase.HttpContext.User属性成员中,即.Net6框架的内置MVC中间件中,MVC中间件又与MVC模板紧密耦合,而MVC模板又紧密集成在展示层中(在.NetCore框架中这种耦合程序相比于.Net Framework框架更加的紧密),而展示层基本上属于整个Web程序的顶层,即展示层能够耦合(集成)其它属性,其它层最好不要耦合(集成)展示层这造成了需求矛盾,所以其它层要想获取HttpContext.User属性成员中数据,或对HttpContext.User属性成员进行赋值操作就不能使用ControllerBase.HttpContext属性成员实例,微软在.Net6框架中通过调用内置的IHttpContextAccessor接口及其具体实现类 HttpContextAccessor,来解决上述问题。即通过IHttpContextAccessor接口调用HttpContext属性成员实例用来在展示层之外的实现来存存储用户在页面上的状态数据,实际上在整个Microsoft.AspNetCore.Http.IhttpContextAccessor接口中,只存在HttpContext这1个属性成员的声明。

    @IHttpContextAccessor示例就是通过对在上1个示例程序IsPersistent的改造来说明IHttpContextAccessor接口的功用,但是这个示程序也是在Web程序的展示层中,对IHttpContextAccessor接口的功用的说明不是那么突出,更好的示例是在nopCommerce程序核心层通过_httpContextAccessor.HttpContext?.Items字典属性实例,对缓存数据的存储。

第21章 深入理解IsPersistent、IhttpContextAccessor和UseDefaultServiceProvider_第6张图片 

先关闭浏览器,再开启浏览器,在限定的时间内,第2次执行登录操作,从上图可以看出,由于Cookies文件中已经存储身份认证数据信息所以_httpContextAccessor.HttpContext?.User.Claims.ToList()方法获取的对象实例的计数依然为:9。

使用内置IHttpContextAccessor接口及其具体实现类 HttpContextAccessor,必须先注入到内置容器实例中:builder.Services.AddSingleton();否则就会出现逻辑异常:“Unable to resolve service for type 'Microsoft.AspNetCore.Http.IHttpContextAccessor' while attempting to activate '_IHttpContextAccessor.Controllers.AccountController”。

注意:

IHttpContextAccessor接口及其具体实现类 HttpContextAccessor实际上的唯一作用,就是让开发者脱离对内置MVC中间件中HttpContext属性实例的依赖,通过IHttpContextAccessor接口的唯一HttpContext属性实例,以更加灵活的方式在不同的实现层中来获取存储用户在页面上执行操作的状态数据。

UseDefaultServiceProvider

    要想理解UseDefaultServiceProvider,必须先从根上理解AddSingleton,AddTransient,AddScoped这个3个内置依赖注入方法。我们知道要想通过反射方式从.Net6框架中获取具体实现类的实例,就必须先把该具体实现类及其接口注入到内置或第3方依赖注入容器中,微软预先为开发者提供的AddSingleton,AddTransient,AddScoped这个3个内置依赖注入方法用户把具体实现类及其接口注入到内置依赖注入容器中,那么为什么微软不只提供1种方法,而是必须提供3种方法来实现前面的操作呢?

    通过常规操作获取具体实现类的实例,该实列根据程序实现的需要有3种生命周期的形态:全局(实例在声明或定义时直接由关键字:static所限定=反射方式的AddSingleton),该实例如果不被覆盖操作(单例实例),则它会存储整个程序的执行过程中;具体实现类中或该实现类的指定方法中(=反射方式的AddScoped) 该实例如果不被覆盖操作,它会存在于指定类的实例中或指定类的方法中;指定方法的if{…}之类的语句块中(=反射方式的AddTransient),当语句块执行结束后,该实例的生命周期也结束了,上面的3种描述状态以极其直白的语言说明了实例的生命周期的是微软提供3种相应的方法来保障注入在内置容器中的具体实现类,通过反射方式进行实例化时该实例就拥有了与之相对应的生命周期。下面的图片按照程序图的方式来说明反射方式是怎样获取对象的实例的。

第21章 深入理解IsPersistent、IhttpContextAccessor和UseDefaultServiceProvider_第7张图片

    下面我们通过示例程序来进一步详细的理解AddSingleton,AddTransient,AddScoped这个3个内置依赖注入方法,首先反射方式通过构造方法来获取DataInfo实例。

第21章 深入理解IsPersistent、IhttpContextAccessor和UseDefaultServiceProvider_第8张图片 

       在示例程序的HomeController构造方法中根据AddSingleton,AddTransient,AddScoped这3种不同的注入选择分别分定义了2个不同的xxxServer实例,通过执行可以看出在同1次请求操作中AddSingleton和AddScoped这两个方法注入的xxxServer实例所实例化的DataInfo实例其实例结果是相等的。

在程序中实例相等有2种情况,一是实例不变(包含实例的内存地址),但是实例的名称可以有多个,那么这些实例即是相等的,二是对同1个实例进行复制,那么这些实例也是相等的,但是这些实例的内存地址是不同的。通过VisualStudio的内存控件分别查看字典实例中存储的以AddSingleton和AddScoped注入的DataInfo实例的内存地址是相同的,即同1个实例有多个实例名称。

第21章 深入理解IsPersistent、IhttpContextAccessor和UseDefaultServiceProvider_第9张图片

    在执行第2次请求操作后,反射方式通过构造方法获取以AddSingleton注入的实例,其实例是不变的(包含实例的内存地址),通过VisualStudio的内存控件分别查看字典实例中存储的这个4个实例 其内存地址也是相同的。从这些现这些观测结果可以得出以AddSingleton注入的具体现类的实例是全局性的,且该实例的调用是对其名称的改变,其实例本身(包含实例的内存地址)并无变化。


    结合第1、2次请求操作后结果,反射方式通过构造方法获取以AddScoped注入的实例,其实例是在同一次请求中是不变的(包含实例的内存地址),.Net框架是用什么来控制在同一个请求内来保证AddScoped注入的实例不改变的呢?

第21章 深入理解IsPersistent、IhttpContextAccessor和UseDefaultServiceProvider_第10张图片

    在.Net框架中如果使用反射方式通过GetServer内置高方法获取以AddScoped注入的具体实现类的实例,由于程序的配置定义不到位从而导致逻辑异常“System.InvalidOperationException:“Cannot resolve scoped service 'UseDefaultServiceProvider.Services.IScopedService' from root provider.”,这种异常在第20章已经出现过,虽然本人已经给出了解决方案,在该示例中依然给出了2种解决方案。

解决方案1:从调用ServiceProvider.GetService方法,转换为调用ServiceProvider.CreateScope().ServiceProvider.GetService方法。

       //异常示例:会显示逻辑异常信息:"System.InvalidOperationException:“Cannot resolve scoped service 'UseDefaultServiceProvider.Services.IScopedService' from root provider.”"

            //如果没有定义“builder.Host.UseDefaultServiceProvider”主机配置方法,下面2行语句必须被注释掉。

            DataInfoDictionary.Add($"_scopedService_____1_____Exception",_ServiceProviderContext.ServiceProvider.GetService().GetDataInfo("1"));

            DataInfoDictionary.Add($"_scopedService_____1_____1_____Exception", _ServiceProviderContext.ServiceProvider.GetService().GetDataInfo("1"));

            //异常示例解决方案--1:

            DataInfoDictionary.Add($"_scopedService_____1", _ServiceProviderContext.ServiceProvider.CreateScope().ServiceProvider.GetService().GetDataInfo("1"));

            DataInfoDictionary.Add($"_scopedService_____1_____1", _ServiceProviderContext.ServiceProvider.CreateScope().ServiceProvider.GetService().GetDataInfo("1"));

    解决方案2:在Program中定义主机全局配置实例。

    //异常示例解决方案--2(内置依赖注入容器):

builder.Host.UseDefaultServiceProvider(options =>

{

    //  通过“options.ValidateScopes = false;”配置属性实例,来解决逻辑异常:"System.InvalidOperationException:“Cannot resolve scoped service 'UseDefaultServiceProvider.Services.IScopedService' from root provider.”",

    //虽然该配置是全局性的,但功能的实现的结果与IHttpContextAccessor的HttpContext相同(它们实现的依赖是否相同我不得而知),即它们是调用同1个内存地址中的实例。

    options.ValidateScopes = false;

    // 如果配置“ options.ValidateOnBuild”配置属性实例的值为:true,那么注入到内置容器中的具体实现类的构造方法就不能被限定为:private,

    // 否则就会出现逻辑异常:“Sytem.AggregateException:Some Services are not able to be constructed...”

    options.ValidateOnBuild = true;

});

//异常示例解决方案--2(第3方依赖注入容器):

//builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

    通过上面的示例,可以得出,可以确定的是同一个IHttpContextAccessor的HttpContext控制了AddScoped注入具体实现类的实例(反射方式获取),在同1次请求内是不变的(包含实例的内存地址),想来构造方法和主机全局配置方法(Host.UseDefaultServiceProvider),内在的具体实现,也是调用了同一个IHttpContextAccessor的HttpContext从而保证了AddScoped注入具体实现类的实例(反射方式获取),在同1次请求内是不变的(包含实例的内存地址)。这就回答了:“对于Net框架是用什么来控制在同一个请求内来保证AddScoped注入的实例不改变的呢?”

    但是通过ServiceProvider.CreateScope().ServiceProvider.GetService方法,所获取的AddScoped注入具体实现类的实例(反射方式获取),即使在同一个请求内,实例也是不同的。

    总结:IHttpContextAccessor的HttpContext控制了,AddScoped注入具体实现类实例的生命周期;但不能控制AddSingleton注入具体实现类实例的生命周期,因为它没有这个权力;也不能控制AddTransient注入具体实现类实例的生命周期,因为不需要控制,在达到控制区域的界限之前,该实例的生命周期已经结束了。IHttpContextAccessor的HttpContext对实例生命周期的控制,是根据实例所具有生命周期的生命特征,而特定设计和定义实现的;即先存在具有3种情况生命特征的生命周期,之后微软才根据这3种情况的生命特征和反射获取实例的需要,在内置HttpContextAccessor的HttpContext中定义实现了,对AddScoped注入具体实现类实例生命周期的控制。

对以上功能更为具体实现和注释见22-01-26-021_IsPersistent_IHttpContextAccessor_UseDefaultServiceProvider(深入理解)。

你可能感兴趣的:(nopCommerce,.Net5,网站开发)