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
相关产品推荐
相关产品推荐

