1. 程式人生 > >ASP.NET Core 中的會話和應用狀態

ASP.NET Core 中的會話和應用狀態

原文地址:https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/app-state?view=aspnetcore-2.1

作者:Rick AndersonSteve SmithDiana LaRoseLuke Latham

HTTP 是無狀態的協議。 不採取其他步驟的情況下,HTTP 請求是不保留使用者值或應用狀態的獨立訊息。 本文介紹了幾種保留請求間使用者資料和應用狀態的方法。

檢視或下載示例程式碼如何下載

狀態管理

可以使用幾種方法儲存狀態。 本主題稍後將對每個方法進行介紹。

儲存方法 儲存機制
Cookie HTTP Cookie(可能包括使用伺服器端應用程式碼儲存的資料)
會話狀態 HTTP Cookie 和伺服器端應用程式碼
TempData HTTP Cookie 或會話狀態
查詢字串 HTTP 查詢字串
隱藏欄位 HTTP 窗體欄位
HttpContext.Items 伺服器端應用程式碼
快取 伺服器端應用程式碼
依賴關係注入 伺服器端應用程式碼

Cookie

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,則建立使用相同會話 Cookie 的新會話。
  • 不會保留空會話 - 會話中必須設定了至少一個值以儲存所有請求的會話。 會話未保留時,為每個新的請求生成新會話 ID。
  • 應用在上次請求後保留會話的時間有限。 應用設定會話超時,或者使用 20 分鐘的預設值。 會話狀態適用於儲存特定於特定會話的使用者資料,但該資料無需永久的會話儲存。
  • 呼叫 ISession.Clear 實現或者會話過期時,會刪除會話資料。
  • 沒有預設機制告知客戶端瀏覽器已關閉或者客戶端上的會話 Cookie 被刪除或過期的應用程式碼。

警告

請勿將敏感資料儲存在會話狀態中。 使用者可能不會關閉瀏覽器並清除會話 Cookie。 某些瀏覽器會保留所有瀏覽器視窗中的有效會話 Cookie。 會話可能不限於單個使用者 - 下一個使用者可能繼續使用同一會話 Cookie 瀏覽應用。

記憶體中快取提供程式在應用駐留的伺服器記憶體中儲存會話資料。 在伺服器場方案中:

配置會話狀態

Microsoft.AspNetCore.App metapackage 中包含的 Microsoft.AspNetCore.Session 包提供中介軟體來管理會話狀態。 若要啟用會話中介軟體,Startup 必須包含:

以下程式碼演示如何使用 IDistributedCache 的預設記憶體中實現設定記憶體中會話提供程式:

C#

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<CookiePolicyOptions>(options =>
        {
            options.CheckConsentNeeded = context => true;
            options.MinimumSameSitePolicy = SameSiteMode.None;
        });

        services.AddDistributedMemoryCache();

        services.AddSession(options =>
        {
            // Set a short timeout for easy testing.
            options.IdleTimeout = TimeSpan.FromSeconds(10);
            options.Cookie.HttpOnly = true;
        });

        services.AddMvc()
            .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    }

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

        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseCookiePolicy();
        app.UseSession();
        app.UseHttpContextItemsMiddleware();
        app.UseMvc();
    }
}

中介軟體的順序很重要。 在前面的示例中,在 UseMvc 之後呼叫 UseSession 時會發生 InvalidOperationException 異常。 有關詳細資訊,請參閱中介軟體排序

配置會話狀態後,HttpContext.Session 可用。

呼叫 UseSession 以前無法訪問 HttpContext.Session

在應用已經開始寫入到響應流之後,不能建立有新會話 Cookie 的新會話。 此異常記錄在 Web 伺服器日誌中但不顯示在瀏覽器中。

以非同步方式載入會話狀態

只有在 TryGetValueSetRemove 方法之前顯式呼叫 ISession.LoadAsync 方法,ASP.NET Core 中的預設會話提供程式才會從基礎 IDistributedCache 後備儲存以非同步方式載入會話記錄。 如果未先呼叫 LoadAsync,則會同步載入基礎會話記錄,這可能對效能產生大規模影響。

若要讓應用強制實施此模式,如果未在 TryGetValueSetRemove 之前呼叫 LoadAsync 方法,那麼使用引起異常的版本包裝 DistributedSessionStoreDistributedSession 實現。 在服務容器中註冊的已包裝的版本。

會話選項

若要替代會話預設值,請使用 SessionOptions

選項 描述
Cookie 確定用於建立 Cookie 的設定。 名稱預設為 SessionDefaults.CookieName (.AspNetCore.Session)。 路徑預設為 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

C#

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_1);

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

應用使用 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)的會話值:

C#

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

...

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

以下示例顯示如何設定和獲取整數和字串:

C#

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)序列化複雜型別。

新增以下擴充套件方法以設定和獲取可序列化的物件:

C#

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);
    }
}

以下示例演示如何使用擴充套件方法設定和獲取可序列化的物件:

C#

// 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);
}

TempData

ASP.NET Core 公開 Razor Pages 頁模型的 TempData 屬性MVC 控制器的 TempData。 此屬性儲存未讀取的資料。 KeepPeek 方法可用於檢查資料,而不執行刪除。 多個請求需要資料時,TempData 非常有助於進行重定向。 使用 Cookie 或會話狀態通過 TempData 提供程式實現 TempData。

TempData 提供程式

ASP.NET Core 2.0 或更高版本中,預設使用基於 Cookie 的 TempData 提供程式,以將 TempData 儲存在 Cookie 中。

使用由 Base64UrlTextEncoder 編碼的 IDataProtector 對 Cookie 資料進行加密,然後進行分塊。 因為 Cookie 進行了分塊,所以 ASP.NET Core 1.x 中的單個 Cookie 大小限制不適用。 未壓縮 Cookie 資料,因為壓縮加密的資料會導致安全問題,如 CRIMEBREACH 攻擊。 有關基於 Cookie 的 TempData 提供程式的詳細資訊,請參閱 CookieTempDataProvider

選擇 TempData 提供程式

選擇 TempData 提供程式涉及幾個注意事項,例如:

  1. 應用是否已使用會話狀態? 如果是,使用會話狀態 TempData 提供程式對應用沒有額外的成本(除了資料的大小)。
  2. 應用是否只對相對較小的資料量(最多 500 個位元組)使用 TempData? 如果是,Cookie TempData 提供程式將為每個攜帶 TempData 的請求增加較小的成本。 如果不是,會話狀態 TempData 提供程式有助於在使用 TempData 前,避免在每個請求中來回切換大量資料。
  3. 應用是否在多個伺服器上的伺服器場中執行? 如果是,無需其他任何配置,即可在資料保護外使用 Cookie TempData 提供程式(請參閱 ASP.NET Core 資料保護金鑰儲存提供程式)。

備註

大多數 Web 客戶端(如 Web 瀏覽器)針對每個 Cookie 的最大大小和/或 Cookie 總數強制實施限制。 使用 Cookie TempData 提供程式時,請驗證應用未超過這些限制。 考慮資料的總大小。 解釋加密和分塊導致的 Cookie 大小增加。

配置 TempData 提供程式

預設情況下啟用基於 Cookie 的 TempData 提供程式。

若要啟用基於會話的 TempData 提供程式,請使用 AddSessionStateTempDataProvider 擴充套件方法:

C#

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });

    services.AddMvc()
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
        .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

處理單個請求時,使用 HttpContext.Items 集合儲存資料。 處理請求後,放棄集合的內容。 通常使用 Items 集合允許元件或中介軟體在請求期間在不同時間點操作且沒有直接傳遞引數的方法時進行通訊。

在下面示例中,中介軟體isVerified 新增到 Items 集合。

C#

app.Use(async (context, next) =>
{
    // perform some verification
    context.Items["isVerified"] = true;
    await next.Invoke();
});

然後,在管道中,另一箇中間件可以訪問 isVerified 的值:

C#

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

對於只供單個應用使用的中介軟體,string 鍵是可以接受的。 應用例項間共享的中介軟體應使用唯一的物件鍵以避免鍵衝突。 以下示例演示如何使用中介軟體類中定義的唯一物件鍵:

C#

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 中的值:

C#

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

此方法還有避免在程式碼中使用關鍵字串的優勢。

快取

快取是儲存和檢索資料的有效方法。 應用可以控制快取項的生存期。

快取資料未與特定請求、使用者或會話相關聯。 請注意不要快取可能由其他使用者請求檢索的特定於使用者的資料。

有關詳細資訊,請參閱快取響應主題。

依賴關係注入

使用依賴關係注入可向所有使用者提供資料:

  1. 定義一項包含資料的服務。 例如,定義一個名為 MyAppData 的類:

    C#

  • public class MyAppData
    {
        // Declare properties and methods
    }
    
  • 將服務類新增到 Startup.ConfigureServices

    C#

  • public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<MyAppData>();
    }
    
  • 使用資料服務類:

    C#

  1. 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 引發異常。