Loading

为什么我的会话状态在ASP.NET Core中不工作了?

原文: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可能依然适用于你

这里主要的可选项如下:

  1. 在用户同意Cookie之前,接受该会话状态可能不可用。
  2. 在用户同意Cookie之前,禁用需要会话状态的功能。
  3. 禁用Cookie同意功能
  4. 将会话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和类似法规的认识。

posted @ 2019-03-14 09:26  LamondLu  阅读(2098)  评论(6编辑  收藏  举报