You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现Blazor Server端类Cookie式跨标签页会话支持?

Blazor Server 实现与Razor Pages 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 08:55:19