原文:Why isn't my session state working in ASP.NET Core? Session state, GDPR, and non-essential cookies
作者:Andrew Lock
译文:https://www.cnblogs.com/lwqlun/p/10526380.html
译者:Lamond Lu
在本篇博客中,我将描述一个关于会话状态(Session State)的问题, 这个问题我已经被询问了好几次了。这个问题的场景如下:
- 创建一个新的ASP.NET Core应用程序
- 一个用户在会话状态中设置了一个字符串值,例如
HttpContext.Session.SetString("theme", "Dark");
- 在下一次请求中,尝试从会话中读取这个自字符串的值
HttpContext.Session.GetString("theme");
, 但是得到的结果却是null
! - “额,这个愚蠢的框架不工作了”(╯°□°)╯︵ ┻━┻
这个问题的原因是ASP.NET Core 2.1中引入的GDPR功能与会话状态互相影响了。在本篇博客中,我将描述为什么你会看到这种行为,以及一些处理它的方法。
GDPR中ASP.NET Core 2.1中引入的一个特性,如果你使用NET Core 1.x或2.0版本,你将不会遇到这个问题。但是请记住,自2019年6月27起,1.x版本即将失去支持,2.0版本已经不受支持了,因此你应该考虑升级到2.1及以上版本。
说明:
- 《通用数据保护条例》(General Data Protection Regulation,简称GDPR)为欧洲联盟的条例,前身是欧盟在1995年制定的《计算机数据保护法》。
- 2018年5月25日,欧洲联盟出台《通用数据保护条例》。
ASP.NET Core中的会话状态
就像我前面所说的,如果你使用的是ASP.NET Core 2.0及以前的版本,你不会遇到这个问题。这里我将借助ASP.NET Core 2.0展示一下预期的行为,以便说明遇到这个问题的人期望的会话状态行为。然后我将在ASP.NET Core 2.2中创建等效的应用程序,并显示会话状态不再起作用了。
什么是会话状态?
会话状态是一种可以回溯到ASP.NET(非核心)的功能,你可以使用它为浏览站点的用户存储和检索服务器端的值。 会话状态经常在ASP.NET应用程序中广泛使用,但经常由于一些原因而出现问题,主要是性能和可伸缩性。
ASP.NET Core中的你应该把会话状态看作针对每用户的缓存。 从技术角度来看,ASP.NET Core中的会话状态的功能需要2个独立的部分来完成:
- 一个Cookie。 用来指定每个用户的唯一ID(Session ID)
- 一个分布式缓存。用来存储与每个用户唯一ID关联的数据项
在一般的情况下,我会尽量避免使用会话状态,使用会话状态可能会有很多陷阱,如果不注意,就会引起一起不必要的问题。例如:
- 会话是针对每个浏览器的,而不是每个登录用户的
- 会话结束的时候,应该删除会话Cookie,但可能不会
- 如果会话中没有任何值,它将会被删除,并重新生成一个新的会话ID
- 本文中即将描述的GDPR问题
这里我们讲解了什么是会话状态,以及其工作的原理。在下一节中,我将创建一个小程序,这个小程序会使用会话状态存储你访问过的页面,然后在首页上显示该列表。
在ASP.NET Core 2.0项目中使用会话状态
为了说明ASP.NET Core 2.0版本和2.1以上版本的行为变化,我将先创建一个ASP.NET Core 2.0项目,因为我的电脑上安装了许多.NET Core SDK, 这里我将使用2.0 SDK(版本号2.1.202)来构建一个2.0项目模板。
这里我们首先创建一个global.json, 将当前app目录的SDK版本固定为2.1.202版本。
dotnet new globaljson --sdk-version 2.1.202
然后使用dotnet new
命令创建一个新的ASP.NET Core MVC 2.0应用程序
dotnet new mvc --framework netcoreapp2.0
会话状态默认情况下是没有启用的,所以这里你需要先添加必要的服务。我们修改Startup.cs文件ConfigureServices
方法来添加会话服务。默认情况下,ASP.NET Core将使用内存来存储会话信息,这对于测试来说很友好,但是生产环境中可能就需要替换为其他方式。
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddSession(); // add session
}
当然,只添加服务是没有用的,我们还需要在管道中注册会话中间件。只有注册在会话中间件之后的中间件才可以访问会话状态,所以你需要将会话中间件放在MVC中间件之前。
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// ...其他配置
app.UseSession();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
对于这个简单的例子,我将使用会话密钥"actions"来存储并读取一个字符串类型的会话值,这个会话值中会保存你访问过的所有页面。当你在不同的页面间浏览时,我们会将你访问过的页面以分号分隔的形式保存在"actions"会话值中。现在我们更新HomeController
的代码:
public class HomeController : Controller
{
public IActionResult Index()
{
RecordInSession("Home");
return View();
}
public IActionResult About()
{
RecordInSession("About");
return View();
}
private void RecordInSession(string action)
{
var paths = HttpContext.Session.GetString("actions") ?? string.Empty;
HttpContext.Session.SetString("actions", paths + ";" + action);
}
}
注意:
Session.GetString(key)
是Microsoft.AspNetCore.Http
命名空间中的一个扩展方法。
最后,我们修改Index.cshtml页面的代码如下,在页面中显示当前"actions"的会话值
@using Microsoft.AspNetCore.Http
@{
ViewData["Title"] = "Home Page";
}
<div>
@Context.Session.GetString("actions")
</div>
如果你现在运行应用程序并浏览几次,你将看到会话页面访问历史列表的构建。 在下面的示例中,我访问了主页三次,关于页面两次:
如果查看当前页面关联的Cookie信息,你就会看到一个名为.AspNetCore.Session
的Cookie, 它的值就是一个加密会话ID, 如果你删除这个Cookie, 你将会看到"actions"的值被重置,页面访问历史列表丢失。
这种会话状态的行为就是大部分人所期望的,所以这里没有问题。但是当你使用ASP.NET Core 2.1/2.2版本创建相同项目之后,情况就不一样了。
在ASP.NET Core 2.2项目中使用会话状态
为了创建ASP.NET Core 2.2应用程序,我使用了几乎相同的行为,但这次我没有固定SDK。 我安装了ASP.NET Core 2.2 SDK(2.2.102),因此以下命令会生成一个ASP.NET Core 2.2 MVC应用程序:
dotnet new mvc
这里你依然需要显示注册会话服务,并启用会话中间件,这一部分代码和前面一模一样。
与以前的版本相比,较新的2.2模板已经简化,因此为了保持一致性,我从2.0应用程序复制了HomeController。 我还复制了Index.chtml,About.chtml和Contact.cshtml视图文件。 最后,我更新了Layout.cshtml,为标题中的About和Contact页面添加了链接。
这2个应用程序,除了使用的ASP.NET Core版本不一样,其他的部分基本都是一样的。然而这次运行的时候,当你浏览一些页面之后,首页只会显示你访问过首页,而不会显示你访问过其他页面。
不要点击隐私政策的横幅 - 后面你将马上知道原因
现在如果你去查看一下你的Cookies, 你会发现加密会话ID.AspNetCore.Session
不存在。
一切都显然配置正确,并且会话本身似乎也在工作(因为可以在Index.cshtml中成功检索HomeController.Index中设置的值)。 但当页面重新加载,或者在导航之间跳转的时候,没有保存会话状态。
那么为什么会话状态在ASP.NET Core 2.0中正常工作, 在ASP.NET Core 2.1/2.2中反而没有正常工作了呢?
到底发生了什么?GDPR
问题的原因,是因为ASP.NET Core 2.1版本之后,引入了一些新功能。为了帮助开发人员遵守2018年生效的GDPR规则,ASP.NET Core 2.1版本引入了一些扩展点,以及模板的更新。
针对这些新功能的官方文档写的都很详细,这里我只做简单总结:
- 同意Cookie对话框 - 默认情况下,在用户点击同意对话框之前,ASP.NET Core不会将“非必要”的cookies写入响应中
- Cookie可以被设置为必要或者非必要的 - 无论用户是否同意,必要的Cookies都会发送给浏览器,非必要的Cookies需要得到用户的同意
- 会话Cookie被认为是非必要的 - 因此,在用户同意之前,无法跨导航或页面重新加载跟踪会话。
- 临时数据(Temp Data)是非必要的 - ASP.NET Core 2.0以上版本中,临时数据提供器使用Cookie来存储数据项,所以在用户同意之前,临时数据功能是不可用的
所以问题是我们需要用户同意使用Cookie。 如果单击隐私横幅上的“Accept”,则ASP.NET Core可以编写会话cookie,并恢复预期的功能。
如何在ASP.NET Core 2.1及以上版本中使用会话状态
根据你正在构建的程序,你可以使用多种选项。哪一个最适合你取决于你的使用场景,但是请注意,这些功能是为了帮助开发人员遵守GDPR而添加的。
如果你不在欧洲国家,或者你认为GDPR对自己没有什么影响,最好请阅读一下[https://andrewlock.net/session-state-gdpr-and-non-essential-cookies/](Tory Hunt的这篇博文) - GDPR可能依然适用于你
这里主要的可选项如下:
- 在用户同意Cookie之前,接受该会话状态可能不可用。
- 在用户同意Cookie之前,禁用需要会话状态的功能。
- 禁用Cookie同意功能
- 将会话Cookie标记为必要的
我将在下面详细介绍每个选项,请记住考虑你的选择可能会影响你是否遵守GDPR!
接受当前的行为
“最简单”的选择就是接受现有的行为。 ASP.NET Core中的会话状态通常只应用于临时数据,因此你的应用程序需要能够处理会话状态不可用的情况。
这取决于你使用会话的目的,可能可以实现或可能不能实现,但这是使用现有模板的最简单方法,并且将你接触GDPR问题方面风险降到了最低。
禁用需要会话的功能
第二种选择和第一种选择类似,应为你需要保持现有的行为。区别在于第一种选项会将会话简单的视为缓存,因此你始终需要假设会话值是可以读取和保存的。而第二种选项略有不同,因为你需要明确知道系统中哪些部分是需要会话状态的,并在用户同意Cookie之前,禁用它们。
例如, 你可以需要一个会话状态保存当前页面选择的主题。如果用户没有同意Cookie, 那么你只需要隐藏主题选择的功能。只要用户同意,再将它显示出来。
这感觉就像是针对选择一的改进,因为它主要改善了用户体验。如果你不考虑哪些功能是需要会话的,用户可能会产生一些疑惑。例如,如果你使用选项一,用户在切换主题的时候,程序永远不会记住它们的选择,这就很让人沮丧。
禁用Cookie同意功能
如果你确定不需要Cookie同意功能,你也可以很容易的禁用它。 默认模板在Startup.ConfigureServices
中显式启用了Cookie同意功能。
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddSession(); // added to enable session
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
这里CheckConsentNeeded
属性是一个标记,它用于检查是否应将非必要的cookie写入响应。 如果函数返回true(如上所述,模板中的默认值),则跳过非必要的cookie。 将此更改为false并且会话状态将起作用,而不需要用户明确同意cookie。
标记会话Cookie是必要的
完全禁用cookie同意功能可能会对你的应用程序造成一定的负担。 如果是这种情况,你可以将会话cookie标记为必要。
services.AddSession
的重载方法,允许你传入一个会话配置对象。你可以使用它设置会话的超时时间,以及自定义会话Cookie。为了将会话Cookie标记为必要的,我们需要显式配置IsEssential
的值是true。
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddSession(opts =>
{
opts.Cookie.IsEssential = true;
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
使用这种方法,虽然应用程序依然会显示Cookie同意横幅,并且在点击之前不会写入非必要的Cookie。 但会议状态将在用户同意Cookie之前立即生效,因为它被认为是必要的。
总结
在这篇文章中,我描述了一个曾经多次被问过问题。开发人员发现他们的会话状态没有正确保存。 这通常是由于ASP.NET Core 2.1中引入的Cookie同意和非必要cookie的GDPR功能引起的。
我展示了一个问题的实例,以及它在2.0 app和2.2 app之间的区别。 我描述了会话状态如何依赖于默认情况下被认为是非必要的会话Cookie,因此在用户同意Cookie之前不会写入响应。
最后,我描述了处理这种行为的四种方法:
什么也不做,接受它
禁用依赖会话状态的功能,直到同意为止
取消同意要求
标记会话Cookie为必要的Cookie。
哪种选择最适合你将取决于你正在构建的应用程序,以及你对GDPR和类似法规的认识。