ASP.NET Core 中的工作階段和狀態管理 (機器翻譯)

作者:Rick AndersonKirk LarkinDiana LaRose

HTTP 是無狀態的通訊協定。 根據預設,HTTP 要求是不會保留使用者值的獨立訊息。 本文描述數種方法來在要求之間保留使用者資料。

狀態管理

可以使用數種方法來儲存狀態。 本文稍後將提供每種方法的描述。

儲存方法 儲存機制
Cookies HTTP cookie。 可能包含使用伺服器端應用程式程式碼所儲存的資料。
工作階段狀態 HTTP cookie 和伺服器端應用程式程式碼
TempData HTTP cookie 或工作階段狀態
查詢字串 HTTP 查詢字串
隱藏欄位 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 可以儲存使用者的名稱、帳戶名稱或唯一使用者識別碼,例如 GUID。 cookie 可以用來存取使用者的個人化設定,例如其慣用網站的背景色彩。

在發出 cookie 及處理隱私權顧慮時,請參閱歐盟一般資料保護規定 (GDPR)。 如需詳細資訊,請參閱 ASP.NET Core 中的一般資料保護規定 (GDPR) 支援

工作階段狀態

工作階段狀態是用來在使用者瀏覽 Web 應用程式時存放使用者資料的 ASP.NET Core 情節。 工作階段狀態使用應用程式所維護的存放區,在用戶端的要求之間保存資料。 工作階段資料是由快取所支援,並視為暫時性資料。 網站應該會繼續運作,而不需工作階段資料。 重要應用程式資料應該儲存在使用者資料庫,並只在工作階段中快取以獲得效能最佳化。

SignalR 應用程式中不支援工作階段,因為 SignalR 中樞可獨立於 HTTP 內容之外而執行。 例如,當長時間輪詢要求由中樞維持開啟,超過要求的 HTTP 內容存留期時,便可能發生此情況。

ASP.NET Core 可維護工作階段狀態,方法是提供包含工作階段識別碼的 cookie 給用戶端。 cookie 工作階段識別碼:

  • 會隨著每個要求傳送至應用程式。
  • 由應用程式用來擷取工作階段資料。

工作階段狀態表現下列行為:

  • 工作階段 cookie 是瀏覽器特有的。 工作階段不會跨瀏覽器共用。
  • 在瀏覽器工作階段結束時,會刪除工作階段 cookie。
  • 如果收到過期工作階段的 cookie,則會建立使用相同工作階段 cookie 的新工作階段。
  • 不會保留空白工作階段。 工作階段必須至少設定一個值,才能在要求之間保存工作階段。 不保留工作階段時,會為每個新的要求產生新的工作階段識別碼。
  • 應用程式會在最後一個要求之後,保留工作階段一段有限的時間。 應用程式會設定工作階段逾時或使用預設值 20 分鐘。 工作階段狀態很適合用來儲存使用者資料:
    • 這是特定工作階段特有的。
    • 資料不需要跨工作階段永久儲存。
  • 工作階段資料會在呼叫 ISession.Clear 實作或工作階段過期時刪除。
  • 沒有任何預設機制可通知應用程式程式碼,用戶端瀏覽器已關閉,或工作階段 cookie 遭到刪除或在用戶端上已過期。
  • 工作階段狀態 cookie 預設不會標記。 除非網站訪客允許追蹤,否則工作階段狀態無法運作。 如需詳細資訊,請參閱 ASP.NET Core 中的一般資料保護規定 (GDPR) 支援
  • 注意:ASP.NET Framework 中 cookieless 的工作階段功能沒有取代,因為它被視為不安全,而且可能會導致工作階段固定攻擊。

警告

請勿將敏感性資料存放在工作階段狀態。 使用者可能不會關閉瀏覽器,並清除工作階段 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 伺服器記錄檔中,而不會顯示在瀏覽器。

非同步載入工作階段狀態

只有在 TryGetValueSetRemove 方法之前明確呼叫 ISession.LoadAsync 方法時,ASP.NET Core 中的預設工作階段提供者才會從基礎 IDistributedCache 備份存放區以非同步方式載入工作階段記錄。 如果並未先呼叫 LoadAsync,則基礎工作階段記錄會同步載入,這可能大規模地為效能帶來負面影響。

若要讓應用程式強制執行此模式,請使用未在 TryGetValueSetRemove 之前呼叫 LoadAsync 方法時擲回例外狀況的版本來包裝 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 或 Controller TempData。 此屬性會儲存資料,直到資料在另一個要求中讀取為止。 KeepKeep(String)PPeek(string) 方法可用來檢查資料,而不需在要求結束時刪除。 將字典中的所有項目都標記為保留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.*@

IndexPeekIndexKeep 頁面之間瀏覽將不會刪除 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 提供者,以將 TempData 儲存在 cookie 中。

cookie 資料會使用 IDataProtector 加密,並編碼為 Base64UrlTextEncoder,然後進行區塊化。 由於加密和區塊化,大小上限 cookie 小於 4096 個位元組。 cookie 資料不會壓縮,因為壓縮加密資料可能會導致安全性問題,例如 CRIMEBREACH 攻擊。 如需以 cookie 為基礎的 TempData 提供者詳細資訊,請參閱 CookieTempDataProvider

選擇 TempData 提供者

選擇 TempData 提供者涉及數項考量,例如:

  • 應用程式已經使用工作階段狀態了嗎? 如果是的話,使用工作階段狀態 TempData 提供者對超過資料大小沒有額外應用程式成本。
  • 應用程式是否盡量只將 TempData 用於相對少量的資料,最多 500 個位元組? 如果是的話,cookie TempData 提供者將對包含 TempData 的每個要求新增少量成本。 如果不是的話,則工作階段狀態 TempData 提供者可能有助於避免在每個要求中來回傳送大量資料,直到取用 TempData 為止。
  • 應用程式在伺服器陣列中的多部伺服器上執行? 如果是的話,不需要額外設定,即可在資料保護之外使用 cookie TempData 提供者。 如需詳細資訊,請參閱 ASP.NET Core 的資料保護概觀金鑰儲存提供者

大部分的 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!");
    }
}

Cache

快取是儲存和擷取資料的有效方式。 應用程式可以控制快取項目的存留期。 如需詳細資訊,請參閱 ASP.NET Core 中的回應快取

快取的資料未與特定要求、使用者或工作階段建立關聯。 請勿快取可能由其他使用者要求所擷取的特定使用者資料。

若要快取整個應用程式的資料,請參閱 ASP.NET Core 中的快取記憶體內部

檢查工作階段狀態

ISession.IsAvailable 是要檢查暫時性失敗。 在工作階段中介軟體執行之前呼叫 IsAvailable 會擲回 InvalidOperationException

需要測試工作階段可用性的程式庫可以使用 HttpContext.Features.Get<ISessionFeature>()?.Session != null

常見錯誤

如果工作階段中介軟體無法保存工作階段:

  • 中介軟體會記錄例外狀況,而且要求會正常繼續。
  • 這會導致無法預期的行為。

如果備份存放區無法使用,工作階段中介軟體將無法保存工作階段。 例如,使用者在工作階段中存放購物車。 使用者在購物車新增一個項目,但認可失敗。 應用程式未察覺到失敗,因此它向使用者報告項目已新增至購物車,但這並不正確。

檢查是否有錯誤的建議方法是,當應用程式完成寫入至工作階段後,呼叫 await feature.Session.CommitAsync。 備份存放區無法使用時,CommitAsync 會擲回例外狀況。 如果 CommitAsync 失敗,應用程式可以處理例外狀況。 無法使用資料存放區時的相同情況下會擲回 LoadAsync

其他資源

檢視或下載範例程式碼 \(英文\) (如何下載)

在 Web 伺服陣列上裝載 ASP.NET Core

作者:Rick AndersonKirk LarkinDiana LaRose

HTTP 是無狀態的通訊協定。 根據預設,HTTP 要求是不會保留使用者值的獨立訊息。 本文描述數種方法來在要求之間保留使用者資料。

檢視或下載範例程式碼 \(英文\) (如何下載)

狀態管理

可以使用數種方法來儲存狀態。 本文稍後將提供每種方法的描述。

儲存方法 儲存機制
Cookies HTTP cookie。 可能包含使用伺服器端應用程式程式碼所儲存的資料。
工作階段狀態 HTTP cookie 和伺服器端應用程式程式碼
TempData HTTP cookie 或工作階段狀態
查詢字串 HTTP 查詢字串
隱藏欄位 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 可以儲存使用者的名稱、帳戶名稱或唯一使用者識別碼,例如 GUID。 cookie 可以用來存取使用者的個人化設定,例如其慣用網站的背景色彩。

在發出 cookie 及處理隱私權顧慮時,請參閱歐盟一般資料保護規定 (GDPR)。 如需詳細資訊,請參閱 ASP.NET Core 中的一般資料保護規定 (GDPR) 支援

工作階段狀態

工作階段狀態是用來在使用者瀏覽 Web 應用程式時存放使用者資料的 ASP.NET Core 情節。 工作階段狀態使用應用程式所維護的存放區,在用戶端的要求之間保存資料。 工作階段資料是由快取所支援,並視為暫時性資料。 網站應該會繼續運作,而不需工作階段資料。 重要應用程式資料應該儲存在使用者資料庫,並只在工作階段中快取以獲得效能最佳化。

SignalR 應用程式中不支援工作階段,因為 SignalR 中樞可獨立於 HTTP 內容之外而執行。 例如,當長時間輪詢要求由中樞維持開啟,超過要求的 HTTP 內容存留期時,便可能發生此情況。

ASP.NET Core 可維護工作階段狀態,方法是提供包含工作階段識別碼的 cookie 給用戶端。 cookie 工作階段識別碼:

  • 會隨著每個要求傳送至應用程式。
  • 由應用程式用來擷取工作階段資料。

工作階段狀態表現下列行為:

  • 工作階段 cookie 是瀏覽器特有的。 工作階段不會跨瀏覽器共用。
  • 在瀏覽器工作階段結束時,會刪除工作階段 cookie。
  • 如果收到過期工作階段的 cookie,則會建立使用相同工作階段 cookie 的新工作階段。
  • 不會保留空白工作階段。 工作階段必須至少設定一個值,才能在要求之間保存工作階段。 不保留工作階段時,會為每個新的要求產生新的工作階段識別碼。
  • 應用程式會在最後一個要求之後,保留工作階段一段有限的時間。 應用程式會設定工作階段逾時或使用預設值 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 伺服器記錄檔中,而不會顯示在瀏覽器。

非同步載入工作階段狀態

只有在 TryGetValueSetRemove 方法之前明確呼叫 ISession.LoadAsync 方法時,ASP.NET Core 中的預設工作階段提供者才會從基礎 IDistributedCache 備份存放區以非同步方式載入工作階段記錄。 如果並未先呼叫 LoadAsync,則基礎工作階段記錄會同步載入,這可能大規模地為效能帶來負面影響。

若要讓應用程式強制執行此模式,請使用未在 TryGetValueSetRemove 之前呼叫 LoadAsync 方法時擲回例外狀況的版本來包裝 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 或 Controller TempData。 此屬性會儲存資料,直到資料在另一個要求中讀取為止。 KeepKeep(String)PPeek(string) 方法可用來檢查資料,而不需在要求結束時刪除。 將字典中的所有項目都標記為保留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.*@

IndexPeekIndexKeep 頁面之間瀏覽將不會刪除 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 提供者,以將 TempData 儲存在 cookie 中。

cookie 資料會使用 IDataProtector 加密,並編碼為 Base64UrlTextEncoder,然後進行區塊化。 由於加密和區塊化,大小上限 cookie 小於 4096 個位元組。 cookie 資料不會壓縮,因為壓縮加密資料可能會導致安全性問題,例如 CRIMEBREACH 攻擊。 如需以 cookie 為基礎的 TempData 提供者詳細資訊,請參閱 CookieTempDataProvider

選擇 TempData 提供者

選擇 TempData 提供者涉及數項考量,例如:

  • 應用程式已經使用工作階段狀態了嗎? 如果是的話,使用工作階段狀態 TempData 提供者對超過資料大小沒有額外應用程式成本。
  • 應用程式是否盡量只將 TempData 用於相對少量的資料,最多 500 個位元組? 如果是的話,cookie TempData 提供者將對包含 TempData 的每個要求新增少量成本。 如果不是的話,則工作階段狀態 TempData 提供者可能有助於避免在每個要求中來回傳送大量資料,直到取用 TempData 為止。
  • 應用程式在伺服器陣列中的多部伺服器上執行? 如果是的話,不需要額外設定,即可在資料保護之外使用 cookie TempData 提供者 (請參閱 ASP.NET Core 資料保護概觀金鑰儲存提供者)。

大部分的 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!";

此方法也有在程式碼中消除使用索引鍵字串的優點。

Cache

快取是儲存和擷取資料的有效方式。 應用程式可以控制快取項目的存留期。 如需詳細資訊,請參閱 ASP.NET Core 中的回應快取

快取的資料未與特定要求、使用者或工作階段建立關聯。 請勿快取可能由其他使用者要求所擷取的特定使用者資料。

若要快取整個應用程式的資料,請參閱 ASP.NET Core 中的快取記憶體內部

常見錯誤

如果工作階段中介軟體無法保存工作階段:

  • 中介軟體會記錄例外狀況,而且要求會正常繼續。
  • 這會導致無法預期的行為。

如果備份存放區無法使用,工作階段中介軟體將無法保存工作階段。 例如,使用者在工作階段中存放購物車。 使用者在購物車新增一個項目,但認可失敗。 應用程式未察覺到失敗,因此它向使用者報告項目已新增至購物車,但這並不正確。

檢查是否有錯誤的建議方法是,當應用程式完成寫入至工作階段後,呼叫 await feature.Session.CommitAsync。 備份存放區無法使用時,CommitAsync 會擲回例外狀況。 如果 CommitAsync 失敗,應用程式可以處理例外狀況。 無法使用資料存放區時的相同情況下會擲回 LoadAsync

其他資源

在 Web 伺服陣列上裝載 ASP.NET Core