ASP.NET Core 中的会话和状态管理

作者:Rick AndersonKirk LarkinDiana LaRose

HTTP 是无状态的协议。 默认情况下,HTTP 请求是不保留用户值的独立消息。 本文介绍了几种保留请求间用户数据的方法。

有关 Blazor 状态管理指南(补充或取代本文中的指南),请参阅 ASP.NET Core Blazor 状态管理

状态管理

可以使用几种方法存储状态。 本文稍后将对每个方法进行介绍。

存储方法 存储机制
Cookie HTTP Cookie。 可能包括使用服务器端应用代码存储的数据。
Session State HTTP Cookie 和服务器端应用代码
TempData HTTP Cookie 或会话状态
Query Strings HTTP 查询字符串
Hidden Fields HTTP 窗体字段
HttpContext.Items 服务器端应用代码
Cache 服务器端应用代码

SignalR/Blazor Server 和基于 HTTP 上下文的状态管理

SignalR 应用不应使用依赖于稳定的 HTTP 上下文来存储信息的会话状态和其他状态管理方法。 SignalR 应用可以将每个连接状态存储在中心的 Context.Items 中。 有关 Blazor Server 应用的详细信息和备用状态管理方法,请参阅 ASP.NET Core Blazor 状态管理

Cookie

Cookie 存储所有请求的数据。 因为 Cookie 是随每个请求发送的,所以它们的大小应该保持在最低限度。 理想情况下,仅标识符应存储在 cookie 中,而数据则由应用存储。 大多数浏览器 cookie 大小限制为 4096 个字节。 每个域仅有有限数量的 Cookie 可用。

由于 Cookie 易被篡改,因此它们必须由服务器进行验证。 客户端上的 Cookie 可能被用户删除或者过期。 但是,Cookie 通常是客户端上最持久的数据暂留形式。

Cookie 通常用于个性化设置,其中的内容是为已知用户定制的。 大多数情况下,仅标识用户,但不对其进行身份验证。 cookie 可以存储用户名、帐户名或唯一的用户 ID(例如 GUID)。 cookie 可用于访问用户的个性化设置,例如首选的网站背景色。

发布 Cookie 和处理 privacy 问题时,请参阅欧盟通用数据保护条例 (GDPR)。 有关详细信息,请参阅 ASP.NET Core 中的一般数据保护条例 (GDPR) 支持

会话状态

会话状态是在用户浏览 Web 应用时用来存储用户数据的 ASP.NET Core 方案。 会话状态使用应用维护的存储来保存客户端所有请求的数据。 会话数据由缓存提供支持,并被视为临时数据。 站点应在没有会话数据的情况下继续运行。 关键应用程序数据应存储在用户数据库中,并仅作为性能优化缓存在会话中。

SignalR 应用不支持会话,因为 SignalR 中心可能独立于 HTTP 上下文执行。 例如,当中心打开的长轮询请求超出请求的 HTTP 上下文的生存期时,可能发生这种情况。

ASP.NET Core 通过向客户端提供包含会话 ID 的 cookie 来维护会话状态。 cookie 会话 ID:

  • 会随每个请求发送到应用。
  • 由应用用于提取会话数据。

会话状态具有以下行为:

  • 会话 cookie 特定于浏览器。 会话不会跨浏览器进行共享。
  • 浏览器会话结束时删除会话 Cookie。
  • 如果收到过期的会话 cookie,则创建使用相同会话 cookie 的新会话。
  • 不会保留空会话。 会话中必须设置了至少一个值以保存所有请求的会话。 会话未保留时,为每个新的请求生成新会话 ID。
  • 应用在上次请求后保留会话的时间有限。 应用设置会话超时,或者使用 20 分钟的默认值。 在以下情况下,会话状态适合存储用户数据:
    • 特定于某个特定会话。
    • 数据不需要跨会话永久存储。
  • 会话数据在调用 ISession.Clear 实现或会话到期时删除。
  • 没有默认机制告知客户端浏览器已关闭或者客户端上的会话 cookie 被删除或过期的应用代码。
  • 默认情况下,会话状态 Cookie 不标记为“基本”。 除非站点访问者允许跟踪,否则会话状态不起作用。 有关详细信息,请参阅 ASP.NET Core 中的一般数据保护条例 (GDPR) 支持
  • 备注:ASP.NET 框架中的无 Cookie 会话功能没有替代,因为它被视为不安全,可能会导致会话固定攻击。

警告

请勿将敏感数据存储在会话状态中。 用户可能不会关闭浏览器或清除会话 cookie。 某些浏览器会保留所有浏览器窗口中的有效会话 Cookie。 会话可能不限于单个用户。 下一个用户可能继续使用同一会话 cookie 浏览应用。

内存中缓存提供程序在应用驻留的服务器内存中存储会话数据。 在服务器场方案中:

配置会话状态

用于管理会话状态的中间件包含在框架中。 若要启用会话中间件,Program.cs 必须包含:

以下代码演示如何使用 IDistributedCache 的默认内存中实现设置内存中会话提供程序:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

builder.Services.AddDistributedMemoryCache();

builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromSeconds(10);
    options.Cookie.HttpOnly = true;
    options.Cookie.IsEssential = true;
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.UseSession();

app.MapRazorPages();
app.MapDefaultControllerRoute();

app.Run();

前面的代码设置较短的超时来简化测试。

中间件的顺序很重要。 在 UseRouting 之后和 MapRazorPagesMapDefaultControllerRoute 之前调用 UseSession。 请参阅中间件排序

配置会话状态后,HttpContext.Session 可用。

调用 UseSession 以前无法访问 HttpContext.Session

在应用已经开始写入到响应流之后,不能创建有新会话 cookie 的新会话。 此异常记录在 Web 服务器日志中但不显示在浏览器中。

以异步方式加载会话状态

只有当 ISession.LoadAsync 方法是先于 TryGetValueSetRemove 方法显式调用时,ASP.NET Core 中的默认会话提供程序才会从基础 IDistributedCache 后备存储中异步加载会话记录。 如果未先调用 LoadAsync,则会同步加载基础会话记录,这可能对性能产生大规模影响。

若要让应用强制执行此模式,请使用在 LoadAsync 方法没有先于 TryGetValueSetRemove 调用时抛出异常的版本来包装 DistributedSessionStoreDistributedSession 实现。 在服务容器中注册的已包装的版本。

会话选项

若要重写会话默认值,请使用 SessionOptions

选项 描述
Cookie 确定用于创建 cookie 的设置。 Name 默认为 SessionDefaults.CookieName (.AspNetCore.Session)。 Path 默认为 SessionDefaults.CookiePath (/)。 SameSite 默认为 SameSiteMode.Lax (1)。 HttpOnly 默认为 trueIsEssential 默认为 false
IdleTimeout IdleTimeout 显示放弃其内容前,内容可以空闲多长时间。 每个会话访问都会重置超时。 此设置仅适用于会话内容,不适用于 cookie。 默认为 20 分钟。
IOTimeout 允许从存储加载会话或者将其提交回存储的最大时长。 此设置可能仅适用于异步操作。 可以使用 InfiniteTimeSpan 来禁用此超时。 默认值为 1 分钟。

会话使用 cookie 跟踪和标识来自单个浏览器的请求。 默认情况下,此 cookie 名为 .AspNetCore.Session,并使用路径 /。 由于 cookie 默认值没有指定域,因此页面上的客户端脚本无法使用它(因为 HttpOnly 默认为 true)。

若要重写 cookie 会话默认值,请使用 SessionOptions

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

builder.Services.AddDistributedMemoryCache();

builder.Services.AddSession(options =>
{
    options.Cookie.Name = ".AdventureWorks.Session";
    options.IdleTimeout = TimeSpan.FromSeconds(10);
    options.Cookie.IsEssential = true;
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.UseSession();

app.MapRazorPages();
app.MapDefaultControllerRoute();

app.Run();

应用使用 IdleTimeout 属性来确定在会话空闲多长时间后它在服务器缓存中的内容就会被放弃。 此属性独立于 cookie 到期时间。 通过会话中间件传递的每个请求都会重置超时。

会话状态为“非锁定”。 如果两个请求同时尝试修改同一会话的内容,则后一个请求替代前一个请求。 Session 是作为一个连贯会话实现的,这意味着所有内容都存储在一起。 两个请求试图修改不同的会话值时,后一个请求可能替代前一个做出的会话更改。

设置和获取会话值

会话状态是通过 Razor Pages PageModel 类或包含 HttpContext.Session 的 MVC Controller 类进行访问。 此属性是 ISession 实现。

ISession 实现提供用于设置和检索整数和字符串值的若干扩展方法。 扩展方法位于 Microsoft.AspNetCore.Http 命名空间中。

ISession 扩展方法:

以下示例在 Razor Pages 页中检索 IndexModel.SessionKeyName 键(示例应用中的 _Name)的会话值:

@page
@using Microsoft.AspNetCore.Http
@model IndexModel

...

Name: @HttpContext.Session.GetString(IndexModel.SessionKeyName)

以下示例显示如何设置和获取整数和字符串:

public class IndexModel : PageModel
{
    public const string SessionKeyName = "_Name";
    public const string SessionKeyAge = "_Age";

    private readonly ILogger<IndexModel> _logger;

    public IndexModel(ILogger<IndexModel> logger)
    {
        _logger = logger;
    }

    public void OnGet()
    {
        if (string.IsNullOrEmpty(HttpContext.Session.GetString(SessionKeyName)))
        {
            HttpContext.Session.SetString(SessionKeyName, "The Doctor");
            HttpContext.Session.SetInt32(SessionKeyAge, 73);
        }
        var name = HttpContext.Session.GetString(SessionKeyName);
        var age = HttpContext.Session.GetInt32(SessionKeyAge).ToString();

        _logger.LogInformation("Session Name: {Name}", name);
        _logger.LogInformation("Session Age: {Age}", age);
    }
}

以下标记在 Razor 页面上显示会话值:

@page
@model PrivacyModel
@{
    ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>

<div class="text-center">
<p><b>Name:</b> @HttpContext.Session.GetString("_Name");<b>Age:

</b> @HttpContext.Session.GetInt32("_Age").ToString()</p>
</div>


必须对所有会话数据进行序列化以启用分布式缓存方案,即使是在使用内存中缓存的时候。 字符串和整数序列化程序是由 ISession 的扩展方法提供。 用户必须使用另一种机制(例如 JSON)序列化复杂类型。

使用以下示例代码序列化对象:

public static class SessionExtensions
{
    public static void Set<T>(this ISession session, string key, T value)
    {
        session.SetString(key, JsonSerializer.Serialize(value));
    }

    public static T? Get<T>(this ISession session, string key)
    {
        var value = session.GetString(key);
        return value == null ? default : JsonSerializer.Deserialize<T>(value);
    }
}

以下示例演示如何使用 SessionExtensions 类设置和获取可序列化的对象:

using Microsoft.AspNetCore.Mvc.RazorPages;
using Web.Extensions;    // SessionExtensions

namespace SessionSample.Pages
{
    public class Index6Model : PageModel
    {
        const string SessionKeyTime = "_Time";
        public string? SessionInfo_SessionTime { get; private set; }
        private readonly ILogger<Index6Model> _logger;

        public Index6Model(ILogger<Index6Model> logger)
        {
            _logger = logger;
        }

        public void OnGet()
        {
            var currentTime = DateTime.Now;

            // Requires SessionExtensions from sample.
            if (HttpContext.Session.Get<DateTime>(SessionKeyTime) == default)
            {
                HttpContext.Session.Set<DateTime>(SessionKeyTime, currentTime);
            }
            _logger.LogInformation("Current Time: {Time}", currentTime);
            _logger.LogInformation("Session Time: {Time}", 
                           HttpContext.Session.Get<DateTime>(SessionKeyTime));

        }
    }
}

警告

应谨慎使用在会话中存储实时对象,因为序列化对象可能会出现许多问题。 有关详细信息,请参阅应允许会话存储对象 (dotnet/aspnetcore #18159)

TempData

ASP.NET Core 公开 Razor Pages TempData 或控制器 TempData。 在另一个请求读取数据之前,此属性将读取此数据。 Keep(String)Peek(string) 方法可用于检查数据,而无需在请求结束时删除。 Keep 将标记字典中的所有项以进行保留。 TempData 为:

  • 在多个请求需要数据的情况下对重定向很有用。
  • 使用 Cookie 或会话状态通过 TempData 提供程序实现。

TempData 示例

考虑创建客户的以下页面:

public class CreateModel : PageModel
{
    private readonly RazorPagesContactsContext _context;

    public CreateModel(RazorPagesContactsContext context)
    {
        _context = context;
    }

    public IActionResult OnGet()
    {
        return Page();
    }

    [TempData]
    public string Message { get; set; }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Customer.Add(Customer);
        await _context.SaveChangesAsync();
        Message = $"Customer {Customer.Name} added";

        return RedirectToPage("./IndexPeek");
    }
}

以下页面显示 TempData["Message"]

@page
@model IndexModel

<h1>Peek Contacts</h1>

@{
    if (TempData.Peek("Message") != null)
    {
        <h3>Message: @TempData.Peek("Message")</h3>
    }
}

@*Content removed for brevity.*@

在前面的标记中,在请求结束时,不会删除 TempData["Message"],因为正在使用 Peek。 刷新页面将显示 TempData["Message"] 的内容。

以下标记类似于前面的代码,但使用 Keep 在请求结束时保留数据:

@page
@model IndexModel

<h1>Contacts Keep</h1>

@{
    if (TempData["Message"] != null)
    {
        <h3>Message: @TempData["Message"]</h3>
    }
    TempData.Keep("Message");
}

@*Content removed for brevity.*@

在 IndexPeek 和 IndexKeep 页面之间导航不会删除 TempData["Message"]

以下代码显示 TempData["Message"],但请求结束时,将删除 TempData["Message"]

@page
@model IndexModel

<h1>Index no Keep or Peek</h1>

@{
    if (TempData["Message"] != null)
    {
        <h3>Message: @TempData["Message"]</h3>
    }
}

@*Content removed for brevity.*@

TempData 提供程序

基于 cookie 的 TempData 提供程序默认用于存储 Cookie 中的 TempData。

cookie 数据是先使用 IDataProtector(用 Base64UrlTextEncoder 编码)进行加密,再进行区块处理。 由于加密和分块,最大 cookie 大小小于 4096 个字节。 未压缩 cookie 数据,因为压缩加密的数据会导致安全问题,如 CRIMEBREACH 攻击。 若要详细了解基于 cookie 的 TempData 提供程序,请参阅 CookieTempDataProvider

选择 TempData 提供程序

选择 TempData 提供程序涉及几个注意事项,例如:

  • 应用是否已使用会话状态? 如果是,使用会话状态 TempData 提供程序对应用没有额外的成本(除了数据的大小)。
  • 应用是否只对相对较小的数据量(最多 500 个字节)使用 TempData? 如果是,cookie TempData 提供程序将为每个携带 TempData 的请求增加较小的成本。 如果不是,会话状态 TempData 提供程序有助于在使用 TempData 前,避免在每个请求中来回切换大量数据。
  • 应用是否在多个服务器上的服务器场中运行? 如果是,无需其他任何配置,即可在数据保护外使用 cookie TempData 提供程序。 有关详细信息,请参阅 ASP.NET Core 数据保护概述密钥存储提供程序

大多数 Web 客户端(如 Web 浏览器)针对每个 cookie 的最大大小和 Cookie 总数强制实施限制。 使用 cookie TempData 提供程序时,请验证应用未超过这些限制。 考虑数据的总大小。 解释加密和分块导致的 cookie 大小增加。

配置 TempData 提供程序

默认情况下启用基于 cookie 的 TempData 提供程序。

若要启用基于会话的 TempData 提供程序,请使用 AddSessionStateTempDataProvider 扩展方法。 只需要调用 AddSessionStateTempDataProvider

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages()
                    .AddSessionStateTempDataProvider();
builder.Services.AddControllersWithViews()
                    .AddSessionStateTempDataProvider();

builder.Services.AddSession();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.UseSession();

app.MapRazorPages();
app.MapDefaultControllerRoute();

app.Run();

查询字符串

可以将有限的数据从一个请求传递到另一个请求,方法是将其添加到新请求的查询字符串中。 这有利于以一种持久的方式捕获状态,这种方式允许通过电子邮件或社交网络共享嵌入式状态的链接。 由于 URL 查询字符串是公共的,因此请勿对敏感数据使用查询字符串。

除了意外共享之外,在查询字符串中包含数据还会使应用遭受跨站点请求伪造 (CSRF) 攻击。 任何保留的会话状态必须防止 CSRF 攻击。 有关详细信息,请参阅在 ASP.NET Core 中预防跨网站请求伪造 (XSRF/CSRF) 攻击

隐藏字段

数据可以保存在隐藏的表单域中,并在下一个请求上回发。 这在多页窗体中很常见。 由于客户端可能篡改数据,因此应用必须始终重新验证存储在隐藏字段中的数据。

HttpContext.Items

HttpContext.Items 集合用于在处理单个请求时存储数据。 处理请求后,放弃集合的内容。 通常使用 Items 集合允许组件或中间件在请求期间在不同时间点操作且没有直接传递参数的方法时进行通信。

在下面示例中,中间件isVerified 添加到 Items 集合:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

ILogger logger = app.Logger;

app.Use(async (context, next) =>
{
    // context.Items["isVerified"] is null
    logger.LogInformation($"Before setting: Verified: {context.Items["isVerified"]}");
    context.Items["isVerified"] = true;
    await next.Invoke();
});

app.Use(async (context, next) =>
{
    // context.Items["isVerified"] is true
    logger.LogInformation($"Next: Verified: {context.Items["isVerified"]}");
    await next.Invoke();
});

app.MapGet("/", async context =>
{
    await context.Response.WriteAsync($"Verified: {context.Items["isVerified"]}");
});

app.Run();

对于仅在单个应用中使用的中间件,使用固定的 string 键不太可能导致键冲突。 但是,为了完全避免发生键冲突的可能性,可以将 object 用作项键。 此方法对于在应用之间共享的中间件特别有用,并且具有消除在代码中使用键字符串的优势。 以下示例演示如何使用中间件类中定义的 object 键:

public class HttpContextItemsMiddleware
{
    private readonly RequestDelegate _next;
    public static readonly object HttpContextItemsMiddlewareKey = new();

    public HttpContextItemsMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        httpContext.Items[HttpContextItemsMiddlewareKey] = "K-9";

        await _next(httpContext);
    }
}

public static class HttpContextItemsMiddlewareExtensions
{
    public static IApplicationBuilder 
        UseHttpContextItemsMiddleware(this IApplicationBuilder app)
    {
        return app.UseMiddleware<HttpContextItemsMiddleware>();
    }
}

其他代码可以使用通过中间件类公开的键访问存储在 HttpContext.Items 中的值:

public class Index2Model : PageModel
{
    private readonly ILogger<Index2Model> _logger;

    public Index2Model(ILogger<Index2Model> logger)
    {
        _logger = logger;
    }

    public void OnGet()
    {
        HttpContext.Items
            .TryGetValue(HttpContextItemsMiddleware.HttpContextItemsMiddlewareKey,
                out var middlewareSetValue);

        _logger.LogInformation("Middleware value {MV}",
            middlewareSetValue?.ToString() ?? "Middleware value not set!");
    }
}

缓存

缓存是存储和检索数据的有效方法。 应用可以控制缓存项的生存期。 有关详细信息,请参阅 ASP.NET Core 中的响应缓存

缓存数据未与特定请求、用户或会话相关联。 请不要缓存可能由其他用户请求检索的特定于用户的数据。

若要缓存应用程序范围内的数据,请参阅 ASP.NET Core 中的内存中缓存

检查会话状态

ISession.IsAvailable 旨在检查暂时性故障。 在会话中间件运行之前调用 IsAvailable 会引发 InvalidOperationException

对于需要测试会话可用性的库,可以使用 HttpContext.Features.Get<ISessionFeature>()?.Session != null

常见错误

  • “在尝试激活‘Microsoft.AspNetCore.Session.DistributedSessionStore’时无法为类型‘Microsoft.Extensions.Caching.Distributed.IDistributedCache’解析服务。”

    这通常是由于不能配置至少一个 IDistributedCache 实现而造成的。 有关详细信息,请参阅 ASP.NET Core 中的分布式缓存ASP.NET Core 中的内存中缓存

如果会话中间件无法保留会话:

  • 中间件记录异常而请求继续正常进行。
  • 这会导致不可预知的行为。

如果后备存储不可用,则会话中间件可能无法保留会话。 例如,用户将购物车存储在会话中。 用户将商品添加到购物车,但提交失败。 应用不知道有此失败,因此它向用户报告商品已添加到购物车,但事实并非如此。

检查此类错误的建议方法是完成将应用写入到该会话后,调用 await feature.Session.CommitAsync。 如果后备存储不可用,则 CommitAsync 引发异常。 如果 CommitAsync 失败,应用可以处理异常。 在与数据存储不可用的相同的条件下,LoadAsync 引发异常。

其他资源

查看或下载示例代码如何下载

在 Web 场中托管 ASP.NET Core

作者:Rick AndersonKirk LarkinDiana LaRose

HTTP 是无状态的协议。 默认情况下,HTTP 请求是不保留用户值的独立消息。 本文介绍了几种保留请求间用户数据的方法。

查看或下载示例代码如何下载

状态管理

可以使用几种方法存储状态。 本文稍后将对每个方法进行介绍。

存储方法 存储机制
Cookie HTTP Cookie。 可能包括使用服务器端应用代码存储的数据。
Session State HTTP Cookie 和服务器端应用代码
TempData HTTP Cookie 或会话状态
Query Strings HTTP 查询字符串
Hidden Fields HTTP 窗体字段
HttpContext.Items 服务器端应用代码
Cache 服务器端应用代码

SignalR/Blazor Server 和基于 HTTP 上下文的状态管理

SignalR 应用不应使用依赖于稳定的 HTTP 上下文来存储信息的会话状态和其他状态管理方法。 SignalR 应用可以将每个连接状态存储在中心的 Context.Items 中。 有关 Blazor Server 应用的详细信息和备用状态管理方法,请参阅 ASP.NET Core Blazor 状态管理

Cookie

Cookie 存储所有请求的数据。 因为 Cookie 是随每个请求发送的,所以它们的大小应该保持在最低限度。 理想情况下,仅标识符应存储在 cookie 中,而数据则由应用存储。 大多数浏览器 cookie 大小限制为 4096 个字节。 每个域仅有有限数量的 Cookie 可用。

由于 Cookie 易被篡改,因此它们必须由服务器进行验证。 客户端上的 Cookie 可能被用户删除或者过期。 但是,Cookie 通常是客户端上最持久的数据暂留形式。

Cookie 通常用于个性化设置,其中的内容是为已知用户定制的。 大多数情况下,仅标识用户,但不对其进行身份验证。 cookie 可以存储用户名、帐户名或唯一的用户 ID(例如 GUID)。 cookie 可用于访问用户的个性化设置,例如首选的网站背景色。

发布 Cookie 和处理 privacy 问题时,请参阅欧盟通用数据保护条例 (GDPR)。 有关详细信息,请参阅 ASP.NET Core 中的一般数据保护条例 (GDPR) 支持

会话状态

会话状态是在用户浏览 Web 应用时用来存储用户数据的 ASP.NET Core 方案。 会话状态使用应用维护的存储来保存客户端所有请求的数据。 会话数据由缓存提供支持,并被视为临时数据。 站点应在没有会话数据的情况下继续运行。 关键应用程序数据应存储在用户数据库中,并仅作为性能优化缓存在会话中。

SignalR 应用不支持会话,因为 SignalR 中心可能独立于 HTTP 上下文执行。 例如,当中心打开的长轮询请求超出请求的 HTTP 上下文的生存期时,可能发生这种情况。

ASP.NET Core 通过向客户端提供包含会话 ID 的 cookie 来维护会话状态。 cookie 会话 ID:

  • 会随每个请求发送到应用。
  • 由应用用于提取会话数据。

会话状态具有以下行为:

  • 会话 cookie 特定于浏览器。 会话不会跨浏览器进行共享。
  • 浏览器会话结束时删除会话 Cookie。
  • 如果收到过期的会话 cookie,则创建使用相同会话 cookie 的新会话。
  • 不会保留空会话。 会话中必须设置了至少一个值以保存所有请求的会话。 会话未保留时,为每个新的请求生成新会话 ID。
  • 应用在上次请求后保留会话的时间有限。 应用设置会话超时,或者使用 20 分钟的默认值。 在以下情况下,会话状态适合存储用户数据:
    • 特定于某个特定会话。
    • 数据不需要跨会话永久存储。
  • 会话数据在调用 ISession.Clear 实现或会话到期时删除。
  • 没有默认机制告知客户端浏览器已关闭或者客户端上的会话 cookie 被删除或过期的应用代码。
  • 默认情况下,会话状态 Cookie 不标记为“基本”。 除非站点访问者允许跟踪,否则会话状态不起作用。 有关详细信息,请参阅 ASP.NET Core 中的一般数据保护条例 (GDPR) 支持

警告

请勿将敏感数据存储在会话状态中。 用户可能不会关闭浏览器或清除会话 cookie。 某些浏览器会保留所有浏览器窗口中的有效会话 Cookie。 会话可能不限于单个用户。 下一个用户可能继续使用同一会话 cookie 浏览应用。

内存中缓存提供程序在应用驻留的服务器内存中存储会话数据。 在服务器场方案中:

配置会话状态

Microsoft.AspNetCore.Session 包:

  • 由框架隐式包含。
  • 提供用于管理会话状态的中间件。

若要启用会话中间件,Startup 必须包含:

以下代码演示如何使用 IDistributedCache 的默认内存中实现设置内存中会话提供程序:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDistributedMemoryCache();

        services.AddSession(options =>
        {
            options.IdleTimeout = TimeSpan.FromSeconds(10);
            options.Cookie.HttpOnly = true;
            options.Cookie.IsEssential = true;
        });

        services.AddControllersWithViews();
        services.AddRazorPages();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseAuthentication();
        app.UseAuthorization();

        app.UseSession();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapDefaultControllerRoute();
            endpoints.MapRazorPages();
        });
    }
}

前面的代码设置较短的超时来简化测试。

中间件的顺序很重要。 在 UseRouting 之后和 UseEndpoints 之前调用 UseSession。 请参阅中间件排序

配置会话状态后,HttpContext.Session 可用。

调用 UseSession 以前无法访问 HttpContext.Session

在应用已经开始写入到响应流之后,不能创建有新会话 cookie 的新会话。 此异常记录在 Web 服务器日志中但不显示在浏览器中。

以异步方式加载会话状态

只有当 ISession.LoadAsync 方法是先于 TryGetValueSetRemove 方法显式调用时,ASP.NET Core 中的默认会话提供程序才会从基础 IDistributedCache 后备存储中异步加载会话记录。 如果未先调用 LoadAsync,则会同步加载基础会话记录,这可能对性能产生大规模影响。

若要让应用强制执行此模式,请使用在 LoadAsync 方法没有先于 TryGetValueSetRemove 调用时抛出异常的版本来包装 DistributedSessionStoreDistributedSession 实现。 在服务容器中注册的已包装的版本。

会话选项

若要重写会话默认值,请使用 SessionOptions

选项 描述
Cookie 确定用于创建 cookie 的设置。 Name 默认为 SessionDefaults.CookieName (.AspNetCore.Session)。 Path 默认为 SessionDefaults.CookiePath (/)。 SameSite 默认为 SameSiteMode.Lax (1)。 HttpOnly 默认为 trueIsEssential 默认为 false
IdleTimeout IdleTimeout 显示放弃其内容前,内容可以空闲多长时间。 每个会话访问都会重置超时。 此设置仅适用于会话内容,不适用于 cookie。 默认为 20 分钟。
IOTimeout 允许从存储加载会话或者将其提交回存储的最大时长。 此设置可能仅适用于异步操作。 可以使用 InfiniteTimeSpan 来禁用此超时。 默认值为 1 分钟。

会话使用 cookie 跟踪和标识来自单个浏览器的请求。 默认情况下,此 cookie 名为 .AspNetCore.Session,并使用路径 /。 由于 cookie 默认值没有指定域,因此页面上的客户端脚本无法使用它(因为 HttpOnly 默认为 true)。

若要重写 cookie 会话默认值,请使用 SessionOptions

public void ConfigureServices(IServiceCollection services)
{
    services.AddDistributedMemoryCache();

    services.AddSession(options =>
    {
        options.Cookie.Name = ".AdventureWorks.Session";
        options.IdleTimeout = TimeSpan.FromSeconds(10);
        options.Cookie.IsEssential = true;
    });

    services.AddControllersWithViews();
    services.AddRazorPages();
}

应用使用 IdleTimeout 属性来确定在会话空闲多长时间后它在服务器缓存中的内容就会被放弃。 此属性独立于 cookie 到期时间。 通过会话中间件传递的每个请求都会重置超时。

会话状态为“非锁定”。 如果两个请求同时尝试修改同一会话的内容,则后一个请求替代前一个请求。 Session 是作为一个连贯会话实现的,这意味着所有内容都存储在一起。 两个请求试图修改不同的会话值时,后一个请求可能替代前一个做出的会话更改。

设置和获取会话值

会话状态是通过 Razor Pages PageModel 类或包含 HttpContext.Session 的 MVC Controller 类进行访问。 此属性是 ISession 实现。

ISession 实现提供用于设置和检索整数和字符串值的若干扩展方法。 扩展方法位于 Microsoft.AspNetCore.Http 命名空间中。

ISession 扩展方法:

以下示例在 Razor Pages 页中检索 IndexModel.SessionKeyName 键(示例应用中的 _Name)的会话值:

@page
@using Microsoft.AspNetCore.Http
@model IndexModel

...

Name: @HttpContext.Session.GetString(IndexModel.SessionKeyName)

以下示例显示如何设置和获取整数和字符串:

public class IndexModel : PageModel
{
    public const string SessionKeyName = "_Name";
    public const string SessionKeyAge = "_Age";
    const string SessionKeyTime = "_Time";

    public string SessionInfo_Name { get; private set; }
    public string SessionInfo_Age { get; private set; }
    public string SessionInfo_CurrentTime { get; private set; }
    public string SessionInfo_SessionTime { get; private set; }
    public string SessionInfo_MiddlewareValue { get; private set; }

    public void OnGet()
    {
        // Requires: using Microsoft.AspNetCore.Http;
        if (string.IsNullOrEmpty(HttpContext.Session.GetString(SessionKeyName)))
        {
            HttpContext.Session.SetString(SessionKeyName, "The Doctor");
            HttpContext.Session.SetInt32(SessionKeyAge, 773);
        }

        var name = HttpContext.Session.GetString(SessionKeyName);
        var age = HttpContext.Session.GetInt32(SessionKeyAge);

必须对所有会话数据进行序列化以启用分布式缓存方案,即使是在使用内存中缓存的时候。 字符串和整数序列化程序是由 ISession 的扩展方法提供。 用户必须使用另一种机制(例如 JSON)序列化复杂类型。

使用以下示例代码序列化对象:

public static class SessionExtensions
{
    public static void Set<T>(this ISession session, string key, T value)
    {
        session.SetString(key, JsonSerializer.Serialize(value));
    }

    public static T Get<T>(this ISession session, string key)
    {
        var value = session.GetString(key);
        return value == null ? default : JsonSerializer.Deserialize<T>(value);
    }
}

以下示例演示如何使用 SessionExtensions 类设置和获取可序列化的对象:

// Requires SessionExtensions from sample download.
if (HttpContext.Session.Get<DateTime>(SessionKeyTime) == default)
{
    HttpContext.Session.Set<DateTime>(SessionKeyTime, currentTime);
}

TempData

ASP.NET Core 公开 Razor Pages TempData 或控制器 TempData。 在另一个请求读取数据之前,此属性将读取此数据。 Keep(String)Peek(string) 方法可用于检查数据,而无需在请求结束时删除。 Keep 将标记字典中的所有项以进行保留。 TempData 为:

  • 在多个请求需要数据的情况下对重定向很有用。
  • 使用 Cookie 或会话状态通过 TempData 提供程序实现。

TempData 示例

考虑创建客户的以下页面:

public class CreateModel : PageModel
{
    private readonly RazorPagesContactsContext _context;

    public CreateModel(RazorPagesContactsContext context)
    {
        _context = context;
    }

    public IActionResult OnGet()
    {
        return Page();
    }

    [TempData]
    public string Message { get; set; }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Customer.Add(Customer);
        await _context.SaveChangesAsync();
        Message = $"Customer {Customer.Name} added";

        return RedirectToPage("./IndexPeek");
    }
}

以下页面显示 TempData["Message"]

@page
@model IndexModel

<h1>Peek Contacts</h1>

@{
    if (TempData.Peek("Message") != null)
    {
        <h3>Message: @TempData.Peek("Message")</h3>
    }
}

@*Content removed for brevity.*@

在前面的标记中,在请求结束时,不会删除 TempData["Message"],因为正在使用 Peek。 刷新页面将显示 TempData["Message"] 的内容。

以下标记类似于前面的代码,但使用 Keep 在请求结束时保留数据:

@page
@model IndexModel

<h1>Contacts Keep</h1>

@{
    if (TempData["Message"] != null)
    {
        <h3>Message: @TempData["Message"]</h3>
    }
    TempData.Keep("Message");
}

@*Content removed for brevity.*@

在 IndexPeek 和 IndexKeep 页面之间导航不会删除 TempData["Message"]

以下代码显示 TempData["Message"],但请求结束时,将删除 TempData["Message"]

@page
@model IndexModel

<h1>Index no Keep or Peek</h1>

@{
    if (TempData["Message"] != null)
    {
        <h3>Message: @TempData["Message"]</h3>
    }
}

@*Content removed for brevity.*@

TempData 提供程序

基于 cookie 的 TempData 提供程序默认用于存储 Cookie 中的 TempData。

cookie 数据是先使用 IDataProtector(用 Base64UrlTextEncoder 编码)进行加密,再进行区块处理。 由于加密和分块,最大 cookie 大小小于 4096 个字节。 未压缩 cookie 数据,因为压缩加密的数据会导致安全问题,如 CRIMEBREACH 攻击。 若要详细了解基于 cookie 的 TempData 提供程序,请参阅 CookieTempDataProvider

选择 TempData 提供程序

选择 TempData 提供程序涉及几个注意事项,例如:

  • 应用是否已使用会话状态? 如果是,使用会话状态 TempData 提供程序对应用没有额外的成本(除了数据的大小)。
  • 应用是否只对相对较小的数据量(最多 500 个字节)使用 TempData? 如果是,cookie TempData 提供程序将为每个携带 TempData 的请求增加较小的成本。 如果不是,会话状态 TempData 提供程序有助于在使用 TempData 前,避免在每个请求中来回切换大量数据。
  • 应用是否在多个服务器上的服务器场中运行? 如果是,无需其他任何配置,即可在数据保护外使用 cookie TempData 提供程序(请参阅 ASP.NET Core 数据保护概述密钥存储提供程序)。

大多数 Web 客户端(如 Web 浏览器)针对每个 cookie 的最大大小和 Cookie 总数强制实施限制。 使用 cookie TempData 提供程序时,请验证应用未超过这些限制。 考虑数据的总大小。 解释加密和分块导致的 cookie 大小增加。

配置 TempData 提供程序

默认情况下启用基于 cookie 的 TempData 提供程序。

若要启用基于会话的 TempData 提供程序,请使用 AddSessionStateTempDataProvider 扩展方法。 只需要调用 AddSessionStateTempDataProvider

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews()
        .AddSessionStateTempDataProvider();
    services.AddRazorPages()
        .AddSessionStateTempDataProvider();

    services.AddSession();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
        app.UseHsts();
    }
    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseSession();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapDefaultControllerRoute();
        endpoints.MapRazorPages();
    });
}

查询字符串

可以将有限的数据从一个请求传递到另一个请求,方法是将其添加到新请求的查询字符串中。 这有利于以一种持久的方式捕获状态,这种方式允许通过电子邮件或社交网络共享嵌入式状态的链接。 由于 URL 查询字符串是公共的,因此请勿对敏感数据使用查询字符串。

除了意外共享之外,在查询字符串中包含数据还会使应用遭受跨站点请求伪造 (CSRF) 攻击。 任何保留的会话状态必须防止 CSRF 攻击。 有关详细信息,请参阅在 ASP.NET Core 中预防跨网站请求伪造 (XSRF/CSRF) 攻击

隐藏字段

数据可以保存在隐藏的表单域中,并在下一个请求上回发。 这在多页窗体中很常见。 由于客户端可能篡改数据,因此应用必须始终重新验证存储在隐藏字段中的数据。

HttpContext.Items

HttpContext.Items 集合用于在处理单个请求时存储数据。 处理请求后,放弃集合的内容。 通常使用 Items 集合允许组件或中间件在请求期间在不同时间点操作且没有直接传递参数的方法时进行通信。

在下面示例中,中间件isVerified 添加到 Items 集合:

public void Configure(IApplicationBuilder app, ILogger<Startup> logger)
{
    app.UseRouting();

    app.Use(async (context, next) =>
    {
        logger.LogInformation($"Before setting: Verified: {context.Items["isVerified"]}");
        context.Items["isVerified"] = true;
        await next.Invoke();
    });

    app.Use(async (context, next) =>
    {
        logger.LogInformation($"Next: Verified: {context.Items["isVerified"]}");
        await next.Invoke();
    });

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync($"Verified: {context.Items["isVerified"]}");
        });
    });
}

对于只在单个应用中使用的中间件,固定 string 键是可以接受的。 应用间共享的中间件应使用唯一的对象键以避免键冲突。 以下示例演示如何使用中间件类中定义的唯一对象键:

public class HttpContextItemsMiddleware
{
    private readonly RequestDelegate _next;
    public static readonly object HttpContextItemsMiddlewareKey = new Object();

    public HttpContextItemsMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        httpContext.Items[HttpContextItemsMiddlewareKey] = "K-9";

        await _next(httpContext);
    }
}

public static class HttpContextItemsMiddlewareExtensions
{
    public static IApplicationBuilder 
        UseHttpContextItemsMiddleware(this IApplicationBuilder app)
    {
        return app.UseMiddleware<HttpContextItemsMiddleware>();
    }
}

其他代码可以使用通过中间件类公开的键访问存储在 HttpContext.Items 中的值:

HttpContext.Items
    .TryGetValue(HttpContextItemsMiddleware.HttpContextItemsMiddlewareKey, 
        out var middlewareSetValue);
SessionInfo_MiddlewareValue = 
    middlewareSetValue?.ToString() ?? "Middleware value not set!";

此方法还有避免在代码中使用关键字符串的优势。

缓存

缓存是存储和检索数据的有效方法。 应用可以控制缓存项的生存期。 有关详细信息,请参阅 ASP.NET Core 中的响应缓存

缓存数据未与特定请求、用户或会话相关联。 请不要缓存可能由其他用户请求检索的特定于用户的数据。

若要缓存应用程序范围内的数据,请参阅 ASP.NET Core 中的内存中缓存

常见错误

  • “在尝试激活‘Microsoft.AspNetCore.Session.DistributedSessionStore’时无法为类型‘Microsoft.Extensions.Caching.Distributed.IDistributedCache’解析服务。”

    这通常是由于不能配置至少一个 IDistributedCache 实现而造成的。 有关详细信息,请参阅 ASP.NET Core 中的分布式缓存ASP.NET Core 中的内存中缓存

如果会话中间件无法保留会话:

  • 中间件记录异常而请求继续正常进行。
  • 这会导致不可预知的行为。

如果后备存储不可用,则会话中间件可能无法保留会话。 例如,用户将购物车存储在会话中。 用户将商品添加到购物车,但提交失败。 应用不知道有此失败,因此它向用户报告商品已添加到购物车,但事实并非如此。

检查此类错误的建议方法是完成将应用写入到该会话后,调用 await feature.Session.CommitAsync。 如果后备存储不可用,则 CommitAsync 引发异常。 如果 CommitAsync 失败,应用可以处理异常。 在与数据存储不可用的相同的条件下,LoadAsync 引发异常。

其他资源

在 Web 场中托管 ASP.NET Core