2-后端 SignalR 集成(.NET 8 WebAPI)
基于现有.NET 8 WebAPI Vue3项目接入SignalR实现后端实时消息推送、前端实时通知全程分步实现、补全代码、兼容现有架构。现有代码一、整体方案说明后端集成Microsoft.AspNetCore.SignalR新建消息集线器 (Hub)实现推送接口、在线用户管理前端使用microsoft/signalr客户端建立长连接接收后端推送消息业务场景用户新增 / 编辑 / 删除时全员在线客户端收到实时通知也支持单点推送、群组推送兼容现有JWT 鉴权、跨域、Redis、过滤器、中间件全部保留二、后端 SignalR 集成.NET 8 WebAPI步骤 1安装 SignalR NuGet 包项目根目录执行命令dotnet add package Microsoft.AspNetCore.SignalR步骤 2新建 Hub 文件夹 消息集线器新建文件夹Hubs创建NoticeHub.csSignalR 核心通信类using Microsoft.AspNetCore.SignalR; namespace ERP.WebAPI.Hubs { /// summary /// 实时通知集线器 /// /summary public class NoticeHub : Hub { /// summary /// 推送全员消息 /// /summary /// param nametitle通知标题/param /// param namecontent通知内容/param public async Task SendAllNotice(string title, string content) { // 推送给所有在线客户端 await Clients.All.SendAsync(ReceiveNotice, title, content); } // 可选推送指定用户 / 群组扩展用 // public async Task SendUserNotice(string connectionId, string title, string content) // { // await Clients.Client(connectionId).SendAsync(ReceiveNotice, title, content); // } } }步骤 3Program.cs 注册 SignalR核心配置修改之前优化好的Program.cs追加 SignalR 服务 中间件同时修改跨域SignalR 长连接对跨域有额外要求完整可直接替换的 Program.csusing ERP.WebAPI.Data; using ERP.WebAPI.Filters; using ERP.WebAPI.Hubs; using ERP.WebAPI.Middleware; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; using StackExchange.Redis; using System.Text; var builder WebApplication.CreateBuilder(args); // 1. 数据库 builder.Services.AddDbContextAppDbContext(options options.UseSqlServer(builder.Configuration.GetConnectionString(SqlServer))); // 2. Redis builder.Services.AddSingletonIConnectionMultiplexer(sp ConnectionMultiplexer.Connect(builder.Configuration.GetConnectionString(Redis)!)); // 3. 控制器 全局过滤器 builder.Services.AddControllers(options { options.Filters.AddGlobalExceptionFilter(); options.Filters.AddApiResultFilter(); }); // 4. JWT 认证 builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options { options.TokenValidationParameters new TokenValidationParameters { ValidateIssuerSigningKey true, IssuerSigningKey new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration[Jwt:Key]!)), ValidateIssuer true, ValidIssuer builder.Configuration[Jwt:Issuer], ValidateAudience true, ValidAudience builder.Configuration[Jwt:Audience], ValidateLifetime true }; }); // 5. 注册自定义JWT过滤器 builder.Services.AddScopedJwtAuthorizeFilter(); // 6. 注册 SignalR 服务 builder.Services.AddSignalR(); // 7. Swagger 带授权按钮 builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(c { c.SwaggerDoc(v1, new OpenApiInfo { Title ERP WebAPI, Version v1 }); var scheme new OpenApiSecurityScheme { Name Authorization, Type SecuritySchemeType.Http, Scheme Bearer, BearerFormat JWT, In ParameterLocation.Header, Description 输入格式: Bearer {Token} }; c.AddSecurityDefinition(Bearer, scheme); c.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference new OpenApiReference { Type ReferenceType.SecurityScheme, Id Bearer } }, Array.Emptystring() } }); }); // 8. 跨域【重点】SignalR 必须允许 凭据、AllowCredentials builder.Services.AddCors(options { options.AddPolicy(AllowVue, policy { policy.WithOrigins(http://localhost:5173) .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); // SignalR 长连接必备 }); }); var app builder.Build(); // 中间件顺序严格遵守 if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseRequestLogging(); app.UseHttpsRedirection(); app.UseCors(AllowVue); app.UseAuthentication(); app.UseAuthorization(); // 映射 SignalR 集线器路由 app.MapHubNoticeHub(/noticeHub); app.MapControllers(); app.Run();关键改动说明builder.Services.AddSignalR()注册 SignalR 服务跨域新增.AllowCredentials()SignalR 长连接强制要求否则前端连接失败app.MapHubNoticeHub(/noticeHub)指定 Hub 访问地址前端通过该地址建立连接步骤 4改造 UsersController操作用户时触发实时推送在新增 / 修改 / 删除用户接口中调用 SignalR 推送消息实现「数据变更 → 全员通知」修改后的 UsersController 核心代码using ERP.WebAPI.Data; using ERP.WebAPI.Filters; using ERP.WebAPI.Hubs; using ERP.WebAPI.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.SignalR; using StackExchange.Redis; using System.Text.Json; namespace ERP.WebAPI.Controllers { [ApiController] [Route(api/[controller])] [ServiceFilter(typeof(JwtAuthorizeFilter))] public class UsersController : ControllerBase { private readonly AppDbContext _db; private readonly IConnectionMultiplexer _redis; // 注入 SignalR 集线器上下文 private readonly IHubContextNoticeHub _noticeHub; private const string UserListCacheKey users:all; // 构造函数注入 Hub 上下文 public UsersController(AppDbContext db, IConnectionMultiplexer redis, IHubContextNoticeHub noticeHub) { _db db; _redis redis; _noticeHub noticeHub; } #region 查询所有用户Redis缓存 [HttpGet] public async TaskIActionResult GetAllUsers() { var redisDb _redis.GetDatabase(); var cachedData await redisDb.StringGetAsync(UserListCacheKey); if (cachedData.HasValue) { var users JsonSerializer.DeserializeListUser(cachedData.ToString()!); return Ok(users); } var usersFromDb await _db.Users.ToListAsync(); await redisDb.StringSetAsync(UserListCacheKey, JsonSerializer.Serialize(usersFromDb), TimeSpan.FromMinutes(10)); return Ok(usersFromDb); } #endregion #region 新增用户 推送通知 [HttpPost] public async TaskIActionResult CreateUser([FromBody] User model) { if (string.IsNullOrWhiteSpace(model.Username) || string.IsNullOrWhiteSpace(model.PasswordHash)) { return BadRequest(new { Message 用户名和密码不能为空 }); } var existUser await _db.Users.FirstOrDefaultAsync(u u.Username model.Username); if (existUser ! null) { return BadRequest(new { Message 用户名已存在 }); } // 密码加密 model.PasswordHash BCrypt.Net.BCrypt.HashPassword(model.PasswordHash); model.CreatedAt DateTime.Now; _db.Users.Add(model); await _db.SaveChangesAsync(); // 清空缓存 var redisDb _redis.GetDatabase(); await redisDb.KeyDeleteAsync(UserListCacheKey); // SignalR 全员推送通知 await _noticeHub.Clients.All.SendAsync(ReceiveNotice, 用户新增通知, $新用户【{model.Username}】已创建); return Created(string.Empty, model); } #endregion #region 根据ID查询单个用户 [HttpGet({id})] public async TaskIActionResult GetUserById(int id) { var user await _db.Users.FindAsync(id); if (user null) { return NotFound(new { Message 用户不存在 }); } return Ok(user); } #endregion #region 修改用户 推送通知 [HttpPut({id})] public async TaskIActionResult UpdateUser(int id, [FromBody] User model) { var user await _db.Users.FindAsync(id); if (user null) { return NotFound(new { Message 用户不存在 }); } user.Username model.Username; user.PasswordHash BCrypt.Net.BCrypt.HashPassword(model.PasswordHash); user.Role model.Role; await _db.SaveChangesAsync(); await _redis.GetDatabase().KeyDeleteAsync(UserListCacheKey); // SignalR 全员推送通知 await _noticeHub.Clients.All.SendAsync(ReceiveNotice, 用户修改通知, $用户【{user.Username}】信息已更新); return Ok(new { Message 修改成功, Data user }); } #endregion #region 删除用户 推送通知 [HttpDelete({id})] public async TaskIActionResult DeleteUser(int id) { var user await _db.Users.FindAsync(id); if (user null) { return NotFound(new { Message 用户不存在 }); } string userName user.Username; _db.Users.Remove(user); await _db.SaveChangesAsync(); await _redis.GetDatabase().KeyDeleteAsync(UserListCacheKey); // SignalR 全员推送通知 await _noticeHub.Clients.All.SendAsync(ReceiveNotice, 用户删除通知, $用户【{userName}】已被移除); return Ok(new { Message 删除成功 }); } #endregion } }核心点构造函数注入IHubContextNoticeHub控制器调用 SignalR 的唯一方式增 / 改 / 删 用户后执行Clients.All.SendAsync向前端推送消息前端监听ReceiveNotice方法即可实时接收三、前端 Vue3 集成 SignalR步骤 1安装 SignalR 客户端前端项目根目录执行npm install microsoft/signalr步骤 2封装 SignalR 连接工具新建src/utils/signalr.js统一管理连接、监听、重连逻辑import * as signalR from microsoft/signalr // 创建 SignalR 连接实例 let connection null /** * 初始化 SignalR 连接 */ export function initSignalR() { // 后端 Hub 地址 const hubUrl https://localhost:7276/noticeHub connection new signalR.HubConnectionBuilder() .withUrl(hubUrl, { // 携带 JWT Token和接口鉴权保持一致 accessTokenFactory: () localStorage.getItem(token) }) .withAutomaticReconnect() // 自动重连网络断开自动恢复 .build() // 监听后端推送的 ReceiveNotice 消息 connection.on(ReceiveNotice, (title, content) { // 这里可以弹窗、消息提示、右上角通知 alert(【${title}】\n${content}) // 也可以搭配 Element Plus / Ant Design 做美观通知 }) // 启动连接 startConnection() // 连接断开监听 connection.onclose(() { console.log(SignalR 连接已断开) }) } /** * 启动连接容错处理 */ async function startConnection() { if (connection.state signalR.HubConnectionState.Disconnected) { try { await connection.start() console.log(SignalR 实时通知连接成功) } catch (err) { console.error(SignalR 连接失败, err) // 3秒后重试 setTimeout(startConnection, 3000) } } } /** * 关闭连接页面销毁时调用 */ export function stopSignalR() { if (connection) { connection.stop() } }步骤 3页面中引入并使用 SignalR场景 1用户列表页UserList.vue登录后进入该页面建立长连接template div classuser-list h3用户管理列表/h3 button clickaddUser新增用户/button ul li v-foritem in userList :keyitem.id {{ item.id }} - {{ item.username }} - {{ item.role }} button clickeditUser(item)编辑/button button clickdelUser(item.id)删除/button /li /ul /div /template script setup import { ref, onMounted, onUnmounted } from vue import request from ./../utils/request import { initSignalR, stopSignalR } from ./../utils/signalr const userList ref([]) // 页面挂载初始化 SignalR 请求用户列表 onMounted(() { initSignalR() getUserList() }) // 页面销毁关闭 SignalR 连接 onUnmounted(() { stopSignalR() }) // 获取用户列表 const getUserList async () { const res await request.get(/users) userList.value res } // 新增用户 const addUser async () { const username prompt(请输入用户名) const pwd prompt(请输入密码) if (!username || !pwd) return await request.post(/users, { username: username, passwordHash: pwd, role: User }) getUserList() } // 编辑用户 const editUser async (row) { const newName prompt(修改用户名, row.username) if (!newName) return await request.put(/users/${row.id}, { username: newName, passwordHash: admin123, role: row.role }) getUserList() } // 删除用户 const delUser async (id) { if (!confirm(确定删除)) return await request.delete(/users/${id}) getUserList() } /script步骤 4路由守卫 鉴权补充已兼容 TokenSignalR 连接时通过accessTokenFactory自动携带本地 Token和接口鉴权逻辑统一未登录用户无法建立连接。四、整体测试流程完整跑通启动 Redis、SQL Server 服务启动后端 .NET 项目dotnet run访问 Swaggerhttps://localhost:7276/swagger启动前端 Vue 项目npm run devhttp://localhost:5173前端登录账号admin/ 密码admin123进入用户列表页控制台打印SignalR 实时通知连接成功点击【新增 / 编辑 / 删除用户】页面自动弹出alert实时通知多开浏览器标签页 / 多台浏览器操作所有在线页面都会收到推送五、扩展功能可选进阶1. 替换 alert 为美观通知推荐 Element Plus安装组件库后使用通知组件替代原生弹窗js运行// 替换 signalr.js 内的 on 监听 import { ElNotification } from element-plus connection.on(ReceiveNotice, (title, content) { ElNotification({ title: title, message: content, type: info }) })2. 单点推送只推送给指定用户核心原理每个前端建立 SignalR 连接后都会生成唯一 ConnectionId。前端把当前ConnectionId 当前登录用户信息用户名 / 用户 ID传给后端保存。后端维护用户ID - ConnectionId映射关系用内存 / Redis 存储。业务触发推送时根据目标用户 ID 查到对应ConnectionId调用Clients.Client(连接ID)实现只推送给该用户。两种存储方案内存存储简单、适合单机部署服务重启数据丢失Redis 存储支持集群、服务重启不丢失推荐生产使用后端NoticeHub增加方法public async Task SendSingleNotice(string connId, string title, string content) { await Clients.Client(connId).SendAsync(ReceiveNotice, title, content); }前端连接成功后把connection.connectionId传给后端实现定向推送。3. 集群部署多服务器 SignalR如果后端多实例部署需配置SignalR Redis 背板实现多节点消息同步dotnet add package Microsoft.AspNetCore.SignalR.StackExchangeRedisProgram.cs 追加builder.Services.AddSignalR().AddStackExchangeRedis(builder.Configuration[ConnectionStrings:Redis]);六、常见问题排错前端连接 SignalR 401 未授权检查accessTokenFactory是否正确携带 Token检查后端跨域是否加了.AllowCredentials()前端连接超时 / 失败确认后端路由/noticeHub可访问关闭浏览器跨域插件 / 后端核对前端域名能连接但收不到消息前后端方法名必须一致后端SendAsync(ReceiveNotice)↔ 前端on(ReceiveNotice)大小写敏感