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

ASP.NET Core SignalR自定义认证及自定义ID标记连接的实现方案

当然可以!ASP.NET Core SignalR给了我们足够的灵活性来实现这种自定义认证和连接标记的需求,我来给你详细拆解步骤:

1. 实现连接前的自定义认证逻辑

你完全可以在客户端建立SignalR连接时,手动验证userId和customId的匹配关系,不通过就直接返回未授权响应。核心是在Hub的OnConnectedAsync方法里完成验证:

首先,在你的Hub类中获取客户端传入的查询参数,再对接数据库做校验:

public class YourCustomHub : Hub
{
    private readonly IUserRepository _userRepository; // 假设这是你的数据库仓储

    public YourCustomHub(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public override async Task OnConnectedAsync()
    {
        // 从HttpContext获取查询参数
        var httpContext = Context.GetHttpContext();
        var userId = httpContext?.Request.Query["userId"].ToString();
        var customId = httpContext?.Request.Query["customId"].ToString();

        // 校验参数合法性和数据库匹配
        if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(customId))
        {
            Context.Response.StatusCode = StatusCodes.Status400BadRequest;
            await Context.AbortAsync();
            return;
        }

        var isMatch = await _userRepository.IsUserIdMatchingCustomId(userId, customId);
        if (!isMatch)
        {
            Context.Response.StatusCode = StatusCodes.Status401Unauthorized;
            await Context.AbortAsync();
            return;
        }

        // 验证通过,继续后续逻辑
        await base.OnConnectedAsync();
    }
}

这样只要参数不匹配,客户端就会收到401响应,连接直接被终止。

2. 标记连接并存储映射关系

为了后续能通过customId找到对应的客户端,你需要把customId和SignalR的ConnectionId做关联存储。推荐用线程安全的集合(因为SignalR是多线程处理连接的),如果是集群部署就换成分布式缓存(比如Redis)。

第一步:创建连接映射类

public class ConnectionMapping<T>
{
    private readonly ConcurrentDictionary<T, HashSet<string>> _connections = new();

    // 添加连接映射
    public void Add(T key, string connectionId)
    {
        _connections.AddOrUpdate(key, 
            new HashSet<string> { connectionId }, 
            (_, existingConnections) =>
            {
                lock (existingConnections)
                {
                    existingConnections.Add(connectionId);
                    return existingConnections;
                }
            });
    }

    // 根据key获取所有对应的ConnectionId
    public IEnumerable<string> GetConnections(T key)
    {
        _connections.TryGetValue(key, out var connections);
        return connections ?? Enumerable.Empty<string>();
    }

    // 移除断开的连接
    public void Remove(T key, string connectionId)
    {
        if (_connections.TryGetValue(key, out var connections))
        {
            lock (connections)
            {
                connections.Remove(connectionId);
                if (connections.Count == 0)
                {
                    _connections.TryRemove(key, out _);
                }
            }
        }
    }
}

第二步:注册为单例服务

在Program.cs里把这个映射类注册成单例,确保整个应用共享一份映射:

builder.Services.AddSingleton<ConnectionMapping<string>>();

第三步:在Hub中维护映射

修改Hub的OnConnectedAsync和OnDisconnectedAsync方法,完成映射的添加和移除:

public class YourCustomHub : Hub
{
    private readonly IUserRepository _userRepository;
    private readonly ConnectionMapping<string> _connectionMapping;

    public YourCustomHub(IUserRepository userRepository, ConnectionMapping<string> connectionMapping)
    {
        _userRepository = userRepository;
        _connectionMapping = connectionMapping;
    }

    public override async Task OnConnectedAsync()
    {
        // 省略之前的认证逻辑...

        // 认证通过后,添加customId和ConnectionId的映射
        var customId = Context.GetHttpContext()?.Request.Query["customId"].ToString();
        if (!string.IsNullOrEmpty(customId))
        {
            _connectionMapping.Add(customId, Context.ConnectionId);
        }

        await base.OnConnectedAsync();
    }

    public override async Task OnDisconnectedAsync(Exception exception)
    {
        var customId = Context.GetHttpContext()?.Request.Query["customId"].ToString();
        if (!string.IsNullOrEmpty(customId))
        {
            _connectionMapping.Remove(customId, Context.ConnectionId);
        }

        await base.OnDisconnectedAsync(exception);
    }
}
3. 通过customId给指定客户端发消息

现在你可以在任何地方(比如后台服务、API控制器、Hub的其他方法)注入ConnectionMapping和IHubContext,就能精准给对应customId的客户端发消息了:

示例1:在Hub内部发送

public async Task SendUpdateToClient(string customId, object updateData)
{
    var connectionIds = _connectionMapping.GetConnections(customId);
    foreach (var connectionId in connectionIds)
    {
        await Clients.Client(connectionId).SendAsync("ReceiveServerUpdate", updateData);
    }
}

示例2:在后台服务中发送

public class YourBackgroundService : BackgroundService
{
    private readonly IHubContext<YourCustomHub> _hubContext;
    private readonly ConnectionMapping<string> _connectionMapping;

    public YourBackgroundService(IHubContext<YourCustomHub> hubContext, ConnectionMapping<string> connectionMapping)
    {
        _hubContext = hubContext;
        _connectionMapping = connectionMapping;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            // 模拟业务逻辑,获取需要推送的customId和数据
            var targetCustomId = "xxx";
            var updateData = new { Message = "Server update" };

            var connectionIds = _connectionMapping.GetConnections(targetCustomId);
            foreach (var connectionId in connectionIds)
            {
                await _hubContext.Clients.Client(connectionId).SendAsync("ReceiveServerUpdate", updateData);
            }

            await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
        }
    }
}
4. 额外注意事项
  • 安全性:如果userId和customId是敏感信息,建议不要用查询参数传递,改用请求头或者加密后的参数,避免URL泄露。
  • 集群部署:如果你的应用是多实例集群,内存中的ConcurrentDictionary会失效,需要把连接映射存储到Redis等分布式缓存中。
  • 扩展认证:如果后续需要结合Bearer Token,你可以在查询参数或请求头里同时传递Token,先验证Token有效性,再校验userId和customId的匹配,这样安全等级更高。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.09 08:27:33