作者:Rick Anderson、Steve Smith、Diana LaRose 和 Luke Latham
HTTP 是无状态的协议。 不采取其他步骤的情况下,HTTP 请求是不保留用户值或应用状态的独立消息。 本文介绍了几种保留请求间用户数据和应用状态的方法。
可以使用几种方法存储状态。 本主题稍后将对每个方法进行介绍。
存储方法 | 存储机制 |
---|---|
Cookie | HTTP Cookie(可能包括使用服务器端应用代码存储的数据) |
Session State | HTTP Cookie 和服务器端应用代码 |
TempData | HTTP Cookie 或会话状态 |
Query Strings | HTTP 查询字符串 |
Hidden Fields | HTTP 窗体字段 |
HttpContext.Items | 服务器端应用代码 |
Cache | 服务器端应用代码 |
Dependency Injection | 服务器端应用代码 |
Cookie 存储所有请求的数据。 因为 Cookie 是随每个请求发送的,所以它们的大小应该保持在最低限度。 理想情况下,仅标识符应存储在 Cookie 中,而数据则由应用存储。 大多数浏览器 Cookie 大小限制为 4096 个字节。 每个域仅有有限数量的 Cookie 可用。
由于 Cookie 易被篡改,因此它们必须由服务器进行验证。 客户端上的 Cookie 可能被用户删除或者过期。 但是,Cookie 通常是客户端上最持久的数据暂留形式。
Cookie 通常用于个性化设置,其中的内容是为已知用户定制的。 大多数情况下,仅标识用户,但不对其进行身份验证。 Cookie 可以存储用户名、帐户名或唯一的用户 ID(例如 GUID)。 然后,可以使用 Cookie 访问用户的个性化设置,例如首选的网站背景色。
发布 Cookie 和处理隐私问题时,请留意欧盟一般数据保护条例 (GDPR)。 有关详细信息,请参阅 ASP.NET Core 中的一般数据保护条例 (GDPR) 支持。
会话状态是在用户浏览 Web 应用时用来存储用户数据的 ASP.NET Core 方案。 会话状态使用应用维护的存储来保存客户端所有请求的数据。 会话数据由缓存支持并被视为临时数据 - 站点应在没有会话数据的情况下继续运行。 关键应用程序数据应存储在用户数据库中,并仅作为性能优化缓存在会话中。
备注
SignalR 应用不支持会话,因为 SignalR 中心可能独立于 HTTP 上下文执行。 例如,当中心打开的长轮询请求超出请求的 HTTP 上下文的生存期时,可能发生这种情况。
ASP.NET Core 通过向客户端提供包含会话 ID 的 Cookie 来维护会话状态,该会话 ID 与每个请求一起发送到应用。 应用使用会话 ID 来获取会话数据。
会话状态具有以下行为:
警告
请勿将敏感数据存储在会话状态中。 用户可能不会关闭浏览器并清除会话 Cookie。 某些浏览器会保留所有浏览器窗口中的有效会话 Cookie。 会话可能不限于单个用户 - 下一个用户可能继续使用同一会话 Cookie 浏览应用。
内存中缓存提供程序在应用驻留的服务器内存中存储会话数据。 在服务器场方案中:
Microsoft.AspNetCore.App metapackage 中包含的 Microsoft.AspNetCore.Session 包提供中间件来管理会话状态。 若要启用会话中间件,Startup
必须包含:
IDistributedCache
实现用作会话后备存储。 有关详细信息,请参阅 ASP.NET Core 中的分布式缓存。ConfigureServices
中 AddSession 的调用。Configure
中 UseSession 的调用。以下代码演示如何使用 IDistributedCache
的默认内存中实现设置内存中会话提供程序:
public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddDistributedMemoryCache(); services.AddSession(options => { // Set a short timeout for easy testing. options.IdleTimeout = TimeSpan.FromSeconds(10); options.Cookie.HttpOnly = true; // Make the session cookie essential options.Cookie.IsEssential = true; }); services.AddMvc() .SetCompatibilityVersion(CompatibilityVersion.Version_2_2); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Error"); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseSession(); app.UseHttpContextItemsMiddleware(); app.UseMvc(); } }
中间件的顺序很重要。 在前面的示例中,在 UseMvc
之后调用 UseSession
时会发生 InvalidOperationException
异常。 有关详细信息,请参阅中间件排序。
配置会话状态后,HttpContext.Session 可用。
调用 UseSession
以前无法访问 HttpContext.Session
。
在应用已经开始写入到响应流之后,不能创建有新会话 Cookie 的新会话。 此异常记录在 Web 服务器日志中但不显示在浏览器中。
只有在 TryGetValue、Set 或 Remove 方法之前显式调用 ISession.LoadAsync 方法,ASP.NET Core 中的默认会话提供程序才会从基础 IDistributedCache 后备存储以异步方式加载会话记录。 如果未先调用 LoadAsync
,则会同步加载基础会话记录,这可能对性能产生大规模影响。
若要让应用强制实施此模式,如果未在 TryGetValue
、Set
或 Remove
之前调用 LoadAsync
方法,那么使用引起异常的版本包装 DistributedSessionStore 和 DistributedSession 实现。 在服务容器中注册的已包装的版本。
若要替代会话默认值,请使用 SessionOptions。
选项 | 说明 |
---|---|
Cookie | 确定用于创建 Cookie 的设置。 名称默认为 SessionDefaults.CookieName (.AspNetCore.Session )。 路径默认为 SessionDefaults.CookiePath (/ )。 SameSite 默认为 SameSiteMode.Lax (1 )。 HttpOnly 默认为 true 。 IsEssential 默认为 false 。 |
IdleTimeout | IdleTimeout 显示放弃其内容前,内容可以空闲多长时间。 每个会话访问都会重置超时。 此设置仅适用于会话内容,不适用于 Cookie。 默认为 20 分钟。 |
IOTimeout | 允许从存储加载会话或者将其提交回存储的最大时长。 此设置可能仅适用于异步操作。 可以使用 InfiniteTimeSpan 禁用超时。 默认值为 1 分钟。 |
会话使用 Cookie 跟踪和标识来自单个浏览器的请求。 默认情况下,此 Cookie 名为 .AspNetCore.Session
,并使用路径 /
。 由于 Cookie 默认值不指定域,因此它不提供页上的客户端脚本(因为 HttpOnly 默认为 true
)。
若要替换 Cookie 会话默认值,请使用 SessionOptions
:
public void ConfigureServices(IServiceCollection services) { services.Configure<CookiePolicyOptions>(options => { options.CheckConsentNeeded = context => true; options.MinimumSameSitePolicy = SameSiteMode.None; }); services.AddDistributedMemoryCache(); services.AddMvc() .SetCompatibilityVersion(CompatibilityVersion.Version_2_2); services.AddSession(options => { options.Cookie.Name = ".AdventureWorks.Session"; options.IdleTimeout = TimeSpan.FromSeconds(10); options.Cookie.IsEssential = true; }); }
应用使用 IdleTimeout 属性确定放弃服务器缓存中的内容前,内容可以空闲多长时间。 此属性独立于 Cookie 到期时间。 通过会话中间件传递的每个请求都会重置超时。
会话状态为“非锁定” 。 如果两个请求同时尝试修改同一会话的内容,则后一个请求替代前一个请求。 Session
是作为一个连贯会话实现的,这意味着所有内容都存储在一起 。 两个请求试图修改不同的会话值时,后一个请求可能替代前一个做出的会话更改。
使用 HttpContext.Session 从 Razor Pages PageModel 类或 MVC 控制器类访问会话状态。 此属性是 ISession 实现。
ISession
实现提供用于设置和检索整数和字符串值的若干扩展方法。 项目引用 Microsoft.AspNetCore.Http.Extensions 包时,扩展方法位于 Microsoft.AspNetCore.Http 命名空间中(添加 using Microsoft.AspNetCore.Http;
语句获取对扩展方法的访问权限)。 这两个包均包括在 Microsoft.AspNetCore.App 元包中。
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, JsonConvert.SerializeObject(value)); } public static T Get<T>(this ISession session, string key) { var value = session.GetString(key); return value == null ? default(T) : JsonConvert.DeserializeObject<T>(value); } }
以下示例演示如何使用扩展方法设置和获取可序列化的对象:
// Requires you add the Set and Get extension method mentioned in the topic. if (HttpContext.Session.Get<DateTime>(SessionKeyTime) == default(DateTime)) { HttpContext.Session.Set<DateTime>(SessionKeyTime, currentTime); }
ASP.NET Core 公开 Razor Pages TempData 或控制器 TempData。 在另一个请求读取数据之前,此属性将读取此数据。 Keep(String) 和 Peek(string) 方法可用于检查数据,而无需在请求结束时删除。 Keep() 将标记字典中的所有项以进行保留。 当多个请求需要数据时,TempData
非常有助于进行重定向。 TempData
提供程序使用 Cookie 或会话状态实现 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.*@
基于 cookie 的 TempData 提供程序默认用于存储 cookie 中的 TempData。
使用由 Base64UrlTextEncoder 编码的 IDataProtector 对 Cookie 数据进行加密,然后进行分块。 因为 Cookie 进行了分块,所以 ASP.NET Core 1.x 中的单个 Cookie 大小限制不适用。 未压缩 Cookie 数据,因为压缩加密的数据会导致安全问题,如 CRIME 和 BREACH 攻击。 有关基于 Cookie 的 TempData 提供程序的详细信息,请参阅 CookieTempDataProvider。
选择 TempData 提供程序涉及几个注意事项,例如:
备注
大多数 Web 客户端(如 Web 浏览器)针对每个 Cookie 的最大大小和/或 Cookie 总数强制实施限制。 使用 Cookie TempData 提供程序时,请验证应用未超过这些限制。 考虑数据的总大小。 解释加密和分块导致的 Cookie 大小增加。
默认情况下启用基于 Cookie 的 TempData 提供程序。
若要启用基于会话的 TempData 提供程序,请使用 AddSessionStateTempDataProvider 扩展方法:
public void ConfigureServices(IServiceCollection services) { services.Configure<CookiePolicyOptions>(options => { options.CheckConsentNeeded = context => true; options.MinimumSameSitePolicy = SameSiteMode.None; }); services.AddMvc() .SetCompatibilityVersion(CompatibilityVersion.Version_2_2) .AddSessionStateTempDataProvider(); services.AddSession(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseDatabaseErrorPage(); } else { app.UseExceptionHandler("/Error"); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseCookiePolicy(); app.UseSession(); app.UseMvc(); }
中间件的顺序很重要。 在前面的示例中,在 UseMvc
之后调用 UseSession
时会发生 InvalidOperationException
异常。 有关详细信息,请参阅中间件排序。
重要
如果面向 .NET Framework 并使用基于会话的 TempData 提供程序,请将 Microsoft.AspNetCore.Session 包添加到项目。
可以将有限的数据从一个请求传递到另一个请求,方法是将其添加到新请求的查询字符串中。 这有利于以一种持久的方式捕获状态,这种方式允许通过电子邮件或社交网络共享嵌入式状态的链接。 由于 URL 查询字符串是公共的,因此请勿对敏感数据使用查询字符串。
除了意外的共享,在查询字符串中包含数据还会为跨站点请求伪造 (CSRF) 攻击创造机会,从而欺骗用户在通过身份验证时访问恶意网站。 然后,攻击者可以从应用中窃取用户数据,或者代表用户采取恶意操作。 任何保留的应用或会话状态必须防止 CSRF 攻击。 有关详细信息,请参阅预防跨网站请求伪造 (XSRF/CSRF) 攻击。
数据可以保存在隐藏的表单域中,并在下一个请求上回发。 这在多页窗体中很常见。 由于客户端可能篡改数据,因此应用必须始终重新验证存储在隐藏字段中的数据。
处理单个请求时,使用 HttpContext.Items 集合存储数据。 处理请求后,放弃集合的内容。 通常使用 Items
集合允许组件或中间件在请求期间在不同时间点操作且没有直接传递参数的方法时进行通信。
在下面示例中,中间件将 isVerified
添加到 Items
集合。
app.Use(async (context, next) => { // perform some verification context.Items["isVerified"] = true; await next.Invoke(); });
然后,在管道中,另一个中间件可以访问 isVerified
的值:
app.Run(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 builder) { return builder.UseMiddleware<HttpContextItemsMiddleware>(); } }
其他代码可以使用通过中间件类公开的键访问存储在 HttpContext.Items
中的值:
HttpContext.Items .TryGetValue(HttpContextItemsMiddleware.HttpContextItemsMiddlewareKey, out var middlewareSetValue); SessionInfo_MiddlewareValue = middlewareSetValue?.ToString() ?? "Middleware value not set!";
此方法还有避免在代码中使用关键字符串的优势。
缓存是存储和检索数据的有效方法。 应用可以控制缓存项的生存期。
缓存数据未与特定请求、用户或会话相关联。 请注意不要缓存可能由其他用户请求检索的特定于用户的数据。
有关详细信息,请参阅 响应缓存在 ASP.NET Core。
使用依赖关系注入可向所有用户提供数据:
定义一项包含数据的服务。 例如,定义一个名为 MyAppData
的类:
public class MyAppData { // Declare properties and methods }
将服务类添加到 Startup.ConfigureServices
:
public void ConfigureServices(IServiceCollection services) { services.AddSingleton<MyAppData>(); }
使用数据服务类:
public class IndexModel : PageModel { public IndexModel(MyAppData myService) { // Do something with the service // Examples: Read data, store in a field or property } }
“在尝试激活‘Microsoft.AspNetCore.Session.DistributedSessionStore’时无法为类型‘Microsoft.Extensions.Caching.Distributed.IDistributedCache’解析服务。”
这通常是由于不能配置至少一个 IDistributedCache
实现而造成的。 有关详细信息,请参阅 ASP.NET Core 中的分布式缓存 和 缓存在内存中 ASP.NET Core。
在会话中间件保存会话失败的事件中(例如,如果后备存储不可用),中间件记录异常而请求继续正常进行。 这会导致不可预知的行为。
例如,用户将购物车存储在会话中。 用户将商品添加到购物车,但提交失败。 应用不知道有此失败,因此它向用户报告商品已添加到购物车,但事实并非如此。
检查此类错误的建议方法是完成将应用写入到该会话后,从应用代码调用 await feature.Session.CommitAsync();
。 如果后备存储不可用,则 CommitAsync
引发异常。 如果 CommitAsync
失败,应用可以处理异常。 在与数据存储不可用的相同的条件下,LoadAsync
引发异常。
SignalR 应用不应使用会话状态来存储信息。 SignalR 应用可以将每个连接状态存储在中心的 Context.Items
中。