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

ASP.NET Core+React项目基于SignalR实现用户专属通知的技术咨询

问题背景

我正在开发一个分层架构的ASP.NET Core Web API 6.0项目,搭配React前端,包含客户间转账功能。为提升用户体验,计划实现通知收件箱功能,支持客户查看单条/全部通知、单条/批量删除通知,并决定集成SignalR实现实时通信。

项目采用Identity进行认证,已配置自定义JWT令牌验证用于端点授权,JWT令牌还可提取客户标识符,这对我的场景至关重要。

但在配置SignalR发送定向通知时遇到问题:例如当客户A向客户B转账时,希望为客户B生成一条可通过SignalR Hub展示的通知,并在数据库中创建对应记录。

寻求指导的方向

  1. SignalR认证:如何配置SignalR使用ASP.NET Core Web API相同的JWT令牌?共享令牌是最佳实践还是应为SignalR单独设置认证机制?
  2. 数据库交互:如何设置SignalR读取专属通知表,实现通知的CRUD操作(包括标记已读)?
  3. 客户端通知管理:标记已读、删除等通知管理操作,通过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令牌能减少系统复杂度,保持认证逻辑一致。

具体配置步骤

  • 后端配置:

    1. 在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();
    
    1. 在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仅负责推送实时事件。

实现思路

  1. 通知表设计:确保Notification实体包含ClientId(接收用户ID)、IsRead、Content、CreatedAt、NotificationType(如转账通知)等字段。

  2. 业务服务层处理数据操作:

    • 创建通知:当转账完成时,先向数据库插入通知记录,再通过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暴露这些方法(除非需要实时同步状态到其他设备)。
  3. 初始化加载通知:用户打开收件箱时,通过API端点获取历史通知,而非SignalR(SignalR仅负责推送新通知)。

3. 客户端通知管理的方案选择

推荐方案:API端点处理管理操作,SignalR仅负责实时推送

  • 原因:
    1. 标记已读、删除等操作是状态变更操作,通过REST API能更好地遵循HTTP语义(PUT/DELETE),便于调试、日志记录和重试机制。
    2. SignalR适合处理实时事件推送,如新通知到达,而非同步状态变更请求。
    3. 分离职责后,系统架构更清晰,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。

额外优化建议

  1. 用户连接映射:如果需要处理用户多设备登录,可在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);
    }
    
  2. 通知持久化:确保所有实时推送的通知都先写入数据库,避免因用户离线导致通知丢失,用户上线后可通过API获取未接收的通知。
  3. 错误处理:在SignalR推送时添加异常捕获,记录日志,避免因单个用户推送失败影响其他操作。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 09:25:59