如何实现Blazor Server端类Cookie式跨标签页会话支持?
这个需求完全可以实现,核心是通过Cookie标识会话,结合自定义中间件、Circuit Handler、内存缓存和全局通知机制,实现不同标签页/Blazor Circuits间的状态即时同步,作用范围与Cookie会话完全匹配。
核心思路
用Cookie作为会话唯一标识,在HTTP管道和SignalR Circuit间传递该标识;基于会话ID将状态存储在共享缓存中,同时通过全局通知服务在状态变更时通知所有关联的Circuit实例,触发UI更新。
具体实现步骤
自定义SessionID中间件:在HTTP管道早期处理,检查并生成SessionID Cookie,确保所有请求(包括SignalR握手)都携带该标识。
public class SessionIdMiddleware { private readonly RequestDelegate _next; private const string SessionIdCookieKey = "MyAppSessionId"; public SessionIdMiddleware(RequestDelegate next) => _next = next; public async Task InvokeAsync(HttpContext context) { if (!context.Request.Cookies.TryGetValue(SessionIdCookieKey, out var sessionId)) { sessionId = Guid.NewGuid().ToString("N"); context.Response.Cookies.Append(SessionIdCookieKey, sessionId, new CookieOptions { HttpOnly = true, SameSite = SameSiteMode.Lax, Expires = DateTimeOffset.UtcNow.AddDays(30), Path = "/" }); } context.Items[SessionIdCookieKey] = sessionId; await _next(context); } }在
Program.cs中注册,放在Blazor Hub之前:app.UseMiddleware<SessionIdMiddleware>(); app.MapBlazorHub();自定义Circuit Handler传递SessionID:将SessionID从HttpContext传递到SignalR Circuit的
Items集合,让Blazor组件能通过Circuit访问会话标识。public class SessionIdCircuitHandler : CircuitHandler { private const string SessionIdCookieKey = "MyAppSessionId"; public override Task OnCircuitOpenedAsync(Circuit circuit, CancellationToken cancellationToken) { if (circuit.HttpContext?.Items.TryGetValue(SessionIdCookieKey, out var sessionId) == true) { circuit.Items[SessionIdCookieKey] = sessionId.ToString(); } return base.OnCircuitOpenedAsync(circuit, cancellationToken); } }注册为单例服务:
builder.Services.AddSingleton<CircuitHandler, SessionIdCircuitHandler>();定义会话状态接口:创建可注入的接口,暴露需要共享的状态属性和变更事件:
public interface IAppSessionState { bool IsDarkMode { get; set; } event Action OnStateUpdated; }实现会话状态类:从Circuit或HttpContext获取SessionID,基于该ID从内存缓存读写状态,同时订阅全局通知同步状态:
public class AppSessionState : IAppSessionState { private readonly IMemoryCache _cache; private readonly Circuit _circuit; private readonly IHttpContextAccessor _httpContextAccessor; private const string SessionIdCookieKey = "MyAppSessionId"; private const string CacheKeyPrefix = "AppSession_"; public bool IsDarkMode { get => GetSessionState().IsDarkMode; set { var state = GetSessionState(); if (state.IsDarkMode == value) return; state.IsDarkMode = value; _cache.Set(GetCacheKey(), state); OnStateUpdated?.Invoke(); // 通知其他Circuit状态变更 SessionStateBroadcaster.NotifySessionChanged(GetSessionId()); } } public event Action OnStateUpdated; public AppSessionState(IMemoryCache cache, Circuit circuit, IHttpContextAccessor httpContextAccessor) { _cache = cache; _circuit = circuit; _httpContextAccessor = httpContextAccessor; // 订阅全局广播,同步状态 SessionStateBroadcaster.SessionStateChanged += (sessionId) => { if (sessionId == GetSessionId()) { OnStateUpdated?.Invoke(); } }; } private string GetSessionId() { if (_circuit.Items.TryGetValue(SessionIdCookieKey, out var circuitSessionId)) { return circuitSessionId.ToString(); } return _httpContextAccessor.HttpContext?.Items[SessionIdCookieKey]?.ToString() ?? string.Empty; } private SessionState GetSessionState() { var cacheKey = GetCacheKey(); return _cache.GetOrCreate(cacheKey, entry => { entry.SlidingExpiration = TimeSpan.FromDays(30); return new SessionState(); }); } private string GetCacheKey() => $"{CacheKeyPrefix}{GetSessionId()}"; // 内部状态容器 private class SessionState { public bool IsDarkMode { get; set; } = false; } }注册为Scoped服务(每个Circuit对应一个实例):
builder.Services.AddScoped<IAppSessionState, AppSessionState>(); builder.Services.AddHttpContextAccessor();全局状态广播服务:实现静态广播类,用于在状态变更时通知所有会话实例:
public static class SessionStateBroadcaster { public static event Action<string> SessionStateChanged; public static void NotifySessionChanged(string sessionId) { SessionStateChanged?.Invoke(sessionId); } }
组件使用示例
在Blazor组件中注入状态服务,绑定属性并监听变更刷新UI:
@inject IAppSessionState SessionState @implements IDisposable <button @onclick="ToggleDarkMode" class="btn">切换深色模式</button> <div class="@(SessionState.IsDarkMode ? "bg-gray-900 text-white" : "bg-white text-gray-900") min-h-screen p-4"> 当前主题:@(SessionState.IsDarkMode ? "深色" : "浅色") </div> @code { protected override void OnInitialized() { SessionState.OnStateUpdated += StateHasChanged; } private void ToggleDarkMode() { SessionState.IsDarkMode = !SessionState.IsDarkMode; } public void Dispose() { SessionState.OnStateUpdated -= StateHasChanged; } }
注意事项
- 本方案基于内存缓存,仅支持单服务器部署;后续横向扩展时需替换为分布式缓存(如Redis),并将广播机制改为Redis Pub/Sub。
- 可根据需求调整SessionID Cookie的过期时间、SameSite、Secure等参数,确保与Razor Pages的Cookie会话行为一致。
- 中间件必须在
MapBlazorHub之前注册,否则SignalR握手请求无法获取SessionID。
内容的提问来源于stack exchange,提问作者user3386507
相关产品推荐
相关产品推荐

