Blazor Server中如何处理多会话下已认证用户的状态变更?
1. Codewrinkles的自定义IdentityValidationProvider是不是最佳方案?
不是最优,但属于可行的折中方案。微软的A&A体系本身基于无状态令牌/ cookie设计,原生没有内置跨会话实时同步状态的机制——默认场景下,认证状态由客户端携带的凭证决定,后端不会主动追踪会话状态。
这个自定义Provider的核心逻辑是每次获取AuthenticationState时都去验证用户的最新状态(比如查询数据库的IsEnabled、是否被删除等字段),确保组件拿到的是最新状态。但缺点也很明显:
- 性能开销:每次组件请求认证状态都要查库,高并发场景下可能有压力,可通过短时间缓存优化。
- 非实时:只有当组件主动请求状态时才会更新,无法做到即时同步。
2. AuthenticationStateChanged事件是否仍可正常使用?
完全可以正常使用,但要明确它的触发条件:只有当AuthenticationStateProvider内部调用NotifyAuthenticationStateChanged方法时,这个事件才会触发。
所谓“不应通过AuthenticationStateProvider获取用户信息”的说法,指的是默认的ServerAuthenticationStateProvider不会自动感知后端的状态变更——它只会在用户登录/登出时更新状态,其他会话的操作不会触发它的状态更新。但事件本身没有限制,只要你在检测到用户状态变化时主动调用NotifyAuthenticationStateChanged,事件就会正常触发,所有订阅了该事件的组件都会收到通知。
3. 还有哪些实现方式?
(1)SignalR实时推送(最优实时方案)
当用户在后端发生状态变更(登出/禁用/删除)时,通过SignalR主动向该用户的所有在线会话推送状态变更消息。前端收到消息后,通知AuthenticationStateProvider更新状态:
// 前端SignalR客户端监听 hubConnection.On("UserStateChanged", async () => { var authState = await _authenticationStateProvider.GetAuthenticationStateAsync(); if (!authState.User.Identity.IsAuthenticated) { _authenticationStateProvider.NotifyAuthenticationStateChanged( Task.FromResult(new AuthenticationState(new ClaimsPrincipal())) ); } });
这种方式能做到毫秒级实时同步,用户体验最好,但需要额外搭建SignalR服务。
(2)前端定期轮询
在前端设置定时器,定期调用后端API检查当前用户的状态,发现状态变更时触发认证状态更新:
// 示例:在MainLayout中实现 protected override async Task OnInitializedAsync() { var timer = new PeriodicTimer(TimeSpan.FromSeconds(30)); while (await timer.WaitForNextTickAsync()) { var isUserValid = await _userService.CheckUserValidityAsync(); if (!isUserValid) { _authenticationStateProvider.NotifyAuthenticationStateChanged( Task.FromResult(new AuthenticationState(new ClaimsPrincipal())) ); timer.Dispose(); break; } } }
优点是实现简单,缺点是有延迟,且会增加后端请求量。
(3)优化版自定义AuthenticationStateProvider
在Codewrinkles方案的基础上,加入缓存机制减少数据库查询:
public class CustomAuthStateProvider : ServerAuthenticationStateProvider { private readonly IUserService _userService; private readonly IMemoryCache _cache; public CustomAuthStateProvider(IUserService userService, IMemoryCache cache) { _userService = userService; _cache = cache; } public override async Task<AuthenticationState> GetAuthenticationStateAsync() { var authState = await base.GetAuthenticationStateAsync(); var userId = authState.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; if (!string.IsNullOrEmpty(userId)) { // 缓存10秒,减少查库频率 var isValid = await _cache.GetOrCreateAsync($"UserValid_{userId}", async entry => { entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(10); return await _userService.IsUserValidAsync(userId); }); if (!isValid) { return new AuthenticationState(new ClaimsPrincipal()); } } return authState; } }
这种方式平衡了实时性和性能,适合对实时性要求不高的场景。
4. 状态变更后:通知组件还是重定向?
不需要单独通知每个子组件,也不建议用重定向的方式。
正确的做法是通过AuthenticationStateProvider触发状态变更通知:调用NotifyAuthenticationStateChanged后,<CascadingAuthenticationState>会自动更新级联参数,所有依赖AuthenticationState的组件(比如使用AuthorizeView、@inject AuthenticationStateProvider的组件)都会自动重新渲染,无需手动调用StateHasChanged()。
重定向会导致页面刷新,丢失当前页面的状态(比如表单输入、滚动位置),用户体验较差,只适合极端场景(比如用户被强制登出且需要回到登录页)。
5. 关于作用域服务+MainLayout订阅的方案
你的方案是可行的,但可以优化:直接在自定义AuthenticationStateProvider中集成状态变更的监听逻辑(比如SignalR消息、轮询结果),当检测到状态变化时直接调用NotifyAuthenticationStateChanged,这样所有依赖认证状态的组件都会自动更新,不需要在MainLayout中手动处理。
这种方式更符合Blazor的认证体系设计,代码更简洁,也能避免手动管理组件状态的繁琐。
内容的提问来源于stack exchange,提问作者David Thielen

