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

Blazor Server:如何从服务器端强制登出指定用户

针对单个用户强制触发Blazor认证重新验证的解决方案

核心思路

要实现管理员操作(锁定/删除用户)后目标用户立即登出,需要同时完成两点:

  1. 服务端主动失效该用户的所有有效认证票证
  2. 主动通知客户端触发重新验证(避免用户被动等待校验)

具体实现方案

1. 利用自定义ITicketStore主动失效用户票证

既然已经实现了自定义ITicketStore,可以扩展方法专门清理指定用户的所有票证:

public class CustomTicketStore : ITicketStore
{
    // 假设用字典/数据库存储票证,键为票证ID,值为AuthenticationTicket
    private readonly Dictionary<string, AuthenticationTicket> _ticketStore = new();

    // 原有ITicketStore接口实现...

    // 新增:失效指定用户的所有票证
    public async Task InvalidateUserTickets(string userId)
    {
        var ticketsToRemove = _ticketStore.Where(t => 
            t.Value.Principal.FindFirst(ClaimTypes.NameIdentifier)?.Value == userId)
            .Select(t => t.Key)
            .ToList();

        foreach (var ticketId in ticketsToRemove)
        {
            await RemoveAsync(ticketId);
        }
    }
}

管理员执行锁定/删除操作时,直接调用这个方法即可让用户现有票证失效。

2. 集成SignalR主动通知客户端即时登出

上述方法是被动生效(用户下次请求才会发现票证失效),要做到即时生效,需要用SignalR主动通知客户端:

步骤1:实现SignalR Hub

public class AuthenticationHub : Hub
{
    // 存储用户ID与连接ID的映射,生产环境建议用分布式缓存(如Redis)
    private static readonly Dictionary<string, List<string>> _userConnections = new();

    public override async Task OnConnectedAsync()
    {
        var userId = Context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        if (!string.IsNullOrEmpty(userId))
        {
            lock (_userConnections)
            {
                if (!_userConnections.ContainsKey(userId))
                {
                    _userConnections[userId] = new List<string>();
                }
                _userConnections[userId].Add(Context.ConnectionId);
            }
        }
        await base.OnConnectedAsync();
    }

    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        var userId = Context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        if (!string.IsNullOrEmpty(userId))
        {
            lock (_userConnections)
            {
                if (_userConnections.TryGetValue(userId, out var connections))
                {
                    connections.Remove(Context.ConnectionId);
                    if (connections.Count == 0)
                    {
                        _userConnections.Remove(userId);
                    }
                }
            }
        }
        await base.OnDisconnectedAsync(exception);
    }

    // 供管理员调用:通知指定用户登出
    public async Task NotifyUserLogout(string userId)
    {
        if (_userConnections.TryGetValue(userId, out var connections))
        {
            await Clients.Clients(connections).SendAsync("ForceLogout");
        }
    }
}

步骤2:客户端监听通知并处理

在Blazor客户端的全局组件(如MainLayout.razor)中注入HubConnection,监听登出通知:

@inject HubConnection HubConnection
@inject NavigationManager NavManager
@inject AuthenticationStateProvider AuthStateProvider

@code {
    protected override async Task OnInitializedAsync()
    {
        HubConnection.On("ForceLogout", async () =>
        {
            // 强制刷新认证状态
            await AuthStateProvider.GetAuthenticationStateAsync();
            // 跳转到登录页
            NavManager.NavigateTo("/login", forceLoad: true);
        });

        try
        {
            await HubConnection.StartAsync();
        }
        catch (Exception ex)
        {
            // 处理连接异常
        }
    }
}

步骤3:管理员操作时触发通知

在用户管理服务中注入IHubContext<AuthenticationHub>,执行操作后发送通知:

public class UserManagementService
{
    private readonly IHubContext<AuthenticationHub> _hubContext;
    private readonly CustomTicketStore _ticketStore;

    public UserManagementService(IHubContext<AuthenticationHub> hubContext, CustomTicketStore ticketStore)
    {
        _hubContext = hubContext;
        _ticketStore = ticketStore;
    }

    public async Task LockUser(string userId)
    {
        // 执行锁定用户的业务逻辑...
        
        // 失效用户票证
        await _ticketStore.InvalidateUserTickets(userId);
        // 通知用户客户端登出
        await _hubContext.Clients.All.SendAsync("NotifyUserLogout", userId);
    }
}

3. 优化RevalidatingServerAuthenticationStateProvider的校验逻辑

如果不想引入SignalR,也可以优化现有认证状态提供者,仅对标记为"需要重新验证"的用户触发校验:

public class CustomAuthStateProvider : RevalidatingServerAuthenticationStateProvider
{
    private readonly IMemoryCache _cache;
    private readonly CustomTicketStore _ticketStore;

    public CustomAuthStateProvider(ILoggerFactory loggerFactory, IMemoryCache cache, CustomTicketStore ticketStore) 
        : base(loggerFactory)
    {
        _cache = cache;
        _ticketStore = ticketStore;
        // 全局校验间隔设为较大值,比如1小时
        RevalidationInterval = TimeSpan.FromHours(1);
    }

    protected override async Task<bool> ValidateAuthenticationStateAsync(
        AuthenticationState authenticationState, CancellationToken cancellationToken)
    {
        var userId = authenticationState.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        if (string.IsNullOrEmpty(userId))
            return false;

        // 检查用户是否被标记为需要重新验证
        var needsRevalidation = await _cache.GetOrCreateAsync($"Revalidate:{userId}", _ => Task.FromResult(false));
        if (!needsRevalidation)
            return true;

        // 校验票证是否有效
        var ticketId = authenticationState.User.FindFirst("TicketId")?.Value;
        var ticket = await _ticketStore.RetrieveAsync(ticketId);
        var isValid = ticket != null && (!ticket.Properties.ExpiresUtc.HasValue || ticket.Properties.ExpiresUtc > DateTimeOffset.UtcNow);

        // 校验完成后清除标记
        if (isValid)
            await _cache.RemoveAsync($"Revalidate:{userId}");

        return isValid;
    }
}

管理员操作时给用户添加标记:

await _cache.SetAsync($"Revalidate:{targetUserId}", true, TimeSpan.FromMinutes(5));
await _ticketStore.InvalidateUserTickets(targetUserId);

方案对比

  • SignalR方案:即时生效,用户体验最好,适合对实时性要求高的场景
  • 优化校验间隔方案:实现简单,无额外依赖,但用户需等待下一次请求/页面刷新才会登出,实时性稍差

内容的提问来源于stack exchange,提问作者FM_AT

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 09:07:31