ASP.NET Core+React项目基于SignalR实现用户专属通知的技术咨询
问题背景
我正在开发一个分层架构的ASP.NET Core Web API 6.0项目,搭配React前端,包含客户间转账功能。为提升用户体验,计划实现通知收件箱功能,支持客户查看单条/全部通知、单条/批量删除通知,并决定集成SignalR实现实时通信。
项目采用Identity进行认证,已配置自定义JWT令牌验证用于端点授权,JWT令牌还可提取客户标识符,这对我的场景至关重要。
但在配置SignalR发送定向通知时遇到问题:例如当客户A向客户B转账时,希望为客户B生成一条可通过SignalR Hub展示的通知,并在数据库中创建对应记录。
寻求指导的方向
- SignalR认证:如何配置SignalR使用ASP.NET Core Web API相同的JWT令牌?共享令牌是最佳实践还是应为SignalR单独设置认证机制?
- 数据库交互:如何设置SignalR读取专属通知表,实现通知的CRUD操作(包括标记已读)?
- 客户端通知管理:标记已读、删除等通知管理操作,通过SignalR Hub处理更高效,还是应通过传统控制器和API端点处理,仅用SignalR负责通知新事件?
已实现的部分代码
SignalR Hub代码
public sealed class TestNotificationHub : Hub { private readonly ITestNotificationService _testNotificationService; public NotificationHub(ITestNotificationService testNotificationService) { _testNotificationService = testNotificationService; } public async Task GetUnreadNotificationsForUser(string userId) { var notifications = await _testNotificationService.GetUnreadNotifications(userId); await Clients.User(userId).SendAsync("TakeNotifications", notifications); } }
通知服务代码
public class TestNotificationService : ITestNotificationService { private readonly IHubContext<NotificationHub> _hubContext; private readonly PersonalDbContext _dbContext; public TestNotificationService(IHubContext<NotificationHub> hubContext, DbContext dbContext) { _hubContext = hubContext; _dbContext = dbContext; } public async Task NotifyUser(string userId, string message) { await _hubContext.Clients.User(userId).SendAsync("TakeNotification", message); } public async Task MarkAllNotificationsAsRead(string userId) { await _dbContext.Notifications .Where(un => un.ClientId == userId && !un.IsRead) .UpdateAsync(un => new Notification { IsRead = true }); } // Additional methods for marking notifications as read and deleting notifications... }
我感觉自己在项目中对SignalR的整合与使用还存在关键缺失,希望获得相关见解、示例或最佳实践指导。
解决方案与最佳实践
1. SignalR认证配置与最佳实践
共享JWT令牌是最佳实践
无需为SignalR单独设置认证机制,复用现有JWT令牌能减少系统复杂度,保持认证逻辑一致。
具体配置步骤
后端配置:
- 在
Program.cs中确保JWT认证中间件在SignalR之前注册:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = builder.Configuration["Jwt:Issuer"], ValidAudience = builder.Configuration["Jwt:Audience"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])), NameClaimType = ClaimTypes.NameIdentifier // 确保用户ID的Claim对应正确 }; // 处理SignalR的WebSocket连接中的令牌 options.Events = new JwtBearerEvents { OnMessageReceived = context => { var accessToken = context.Request.Query["access_token"]; var path = context.HttpContext.Request.Path; if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs/notification")) { context.Token = accessToken; } return Task.CompletedTask; } }; }); // 注册SignalR builder.Services.AddSignalR();- 在Hub中通过
Context.User获取认证后的用户信息,无需手动传入userId:
public async Task GetUnreadNotificationsForUser() { var userId = Context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; if (string.IsNullOrEmpty(userId)) throw new UnauthorizedAccessException(); var notifications = await _testNotificationService.GetUnreadNotifications(userId); await Clients.Caller.SendAsync("TakeNotifications", notifications); }- 在
前端配置:
连接SignalR时在URL中附加JWT令牌:const connection = new signalR.HubConnectionBuilder() .withUrl("/hubs/notification", { accessTokenFactory: () => localStorage.getItem("jwtToken") }) .build();
2. 数据库交互与SignalR的整合
SignalR本身不负责直接操作数据库,应该通过业务服务层处理数据CRUD,SignalR仅负责推送实时事件。
实现思路
通知表设计:确保
Notification实体包含ClientId(接收用户ID)、IsRead、Content、CreatedAt、NotificationType(如转账通知)等字段。业务服务层处理数据操作:
- 创建通知:当转账完成时,先向数据库插入通知记录,再通过
IHubContext推送给目标用户:public async Task CreateTransferNotification(string targetUserId, string transferMessage) { var notification = new Notification { ClientId = targetUserId, Content = transferMessage, IsRead = false, CreatedAt = DateTime.UtcNow, NotificationType = NotificationType.Transfer }; _dbContext.Notifications.Add(notification); await _dbContext.SaveChangesAsync(); // 推送实时通知 await _hubContext.Clients.User(targetUserId).SendAsync("ReceiveNewNotification", notification); } - 标记已读/删除:在服务层实现对应的数据库操作,无需通过SignalR Hub暴露这些方法(除非需要实时同步状态到其他设备)。
- 创建通知:当转账完成时,先向数据库插入通知记录,再通过
初始化加载通知:用户打开收件箱时,通过API端点获取历史通知,而非SignalR(SignalR仅负责推送新通知)。
3. 客户端通知管理的方案选择
推荐方案:API端点处理管理操作,SignalR仅负责实时推送
- 原因:
- 标记已读、删除等操作是状态变更操作,通过REST API能更好地遵循HTTP语义(PUT/DELETE),便于调试、日志记录和重试机制。
- SignalR适合处理实时事件推送,如新通知到达,而非同步状态变更请求。
- 分离职责后,系统架构更清晰,API端点可复用在其他场景(如移动端)。
具体实现
- 创建API控制器:
[ApiController] [Route("api/notifications")] [Authorize] public class NotificationsController : ControllerBase { private readonly ITestNotificationService _notificationService; public NotificationsController(ITestNotificationService notificationService) { _notificationService = notificationService; } [HttpGet("unread")] public async Task<IActionResult> GetUnreadNotifications() { var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; var notifications = await _notificationService.GetUnreadNotifications(userId); return Ok(notifications); } [HttpPut("mark-all-read")] public async Task<IActionResult> MarkAllAsRead() { var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; await _notificationService.MarkAllNotificationsAsRead(userId); return NoContent(); } [HttpDelete("{id}")] public async Task<IActionResult> DeleteNotification(Guid id) { var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; await _notificationService.DeleteNotification(id, userId); return NoContent(); } } - 前端处理:
- 页面加载时调用API获取历史通知。
- 通过SignalR监听
ReceiveNewNotification事件,将新通知添加到列表中。 - 执行标记已读/删除操作时调用API,成功后更新本地UI。
额外优化建议
- 用户连接映射:如果需要处理用户多设备登录,可在Hub的
OnConnectedAsync中记录用户的连接ID,便于精准推送:public override async Task OnConnectedAsync() { var userId = Context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; if (!string.IsNullOrEmpty(userId)) { // 将用户ID与连接ID存储(可使用内存缓存或分布式缓存) await _connectionMappingService.AddUserConnection(userId, Context.ConnectionId); } await base.OnConnectedAsync(); } public override async Task OnDisconnectedAsync(Exception exception) { var userId = Context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; if (!string.IsNullOrEmpty(userId)) { await _connectionMappingService.RemoveUserConnection(userId, Context.ConnectionId); } await base.OnDisconnectedAsync(exception); } - 通知持久化:确保所有实时推送的通知都先写入数据库,避免因用户离线导致通知丢失,用户上线后可通过API获取未接收的通知。
- 错误处理:在SignalR推送时添加异常捕获,记录日志,避免因单个用户推送失败影响其他操作。
内容的提问来源于stack exchange,提问作者Levan Amashukeli
相关产品推荐
相关产品推荐

