2-后端 SignalR 集成(.NET 8 WebAPI)
2026/6/12 2:29:14 网站建设 项目流程

基于现有.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.cs(SignalR 核心通信类)

using Microsoft.AspNetCore.SignalR; namespace ERP.WebAPI.Hubs { /// <summary> /// 实时通知集线器 /// </summary> public class NoticeHub : Hub { /// <summary> /// 推送全员消息 /// </summary> /// <param name="title">通知标题</param> /// <param name="content">通知内容</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); // } } }

步骤 3:Program.cs 注册 SignalR(核心配置)

修改之前优化好的Program.cs追加 SignalR 服务 + 中间件,同时修改跨域(SignalR 长连接对跨域有额外要求)

完整可直接替换的 Program.cs

using 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.AddDbContext<AppDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("SqlServer"))); // 2. Redis builder.Services.AddSingleton<IConnectionMultiplexer>(sp => ConnectionMultiplexer.Connect(builder.Configuration.GetConnectionString("Redis")!)); // 3. 控制器 + 全局过滤器 builder.Services.AddControllers(options => { options.Filters.Add<GlobalExceptionFilter>(); options.Filters.Add<ApiResultFilter>(); }); // 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.AddScoped<JwtAuthorizeFilter>(); // 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.Empty<string>() } }); }); // 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.MapHub<NoticeHub>("/noticeHub"); app.MapControllers(); app.Run();

关键改动说明

  1. builder.Services.AddSignalR():注册 SignalR 服务
  2. 跨域新增.AllowCredentials()SignalR 长连接强制要求,否则前端连接失败
  3. app.MapHub<NoticeHub>("/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 IHubContext<NoticeHub> _noticeHub; private const string UserListCacheKey = "users:all"; // 构造函数注入 Hub 上下文 public UsersController(AppDbContext db, IConnectionMultiplexer redis, IHubContext<NoticeHub> noticeHub) { _db = db; _redis = redis; _noticeHub = noticeHub; } #region 查询所有用户(Redis缓存) [HttpGet] public async Task<IActionResult> GetAllUsers() { var redisDb = _redis.GetDatabase(); var cachedData = await redisDb.StringGetAsync(UserListCacheKey); if (cachedData.HasValue) { var users = JsonSerializer.Deserialize<List<User>>(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 Task<IActionResult> 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 Task<IActionResult> 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 Task<IActionResult> 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 Task<IActionResult> 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 } }

核心点

  1. 构造函数注入IHubContext<NoticeHub>:控制器调用 SignalR 的唯一方式
  2. 增 / 改 / 删 用户后,执行Clients.All.SendAsync向前端推送消息
  3. 前端监听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 class="user-list"> <h3>用户管理列表</h3> <button @click="addUser">新增用户</button> <ul> <li v-for="item in userList" :key="item.id"> {{ item.id }} - {{ item.username }} - {{ item.role }} <button @click="editUser(item)">编辑</button> <button @click="delUser(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:路由守卫 & 鉴权补充(已兼容 Token)

SignalR 连接时通过accessTokenFactory自动携带本地 Token,和接口鉴权逻辑统一,未登录用户无法建立连接


四、整体测试流程(完整跑通)

  1. 启动 Redis、SQL Server 服务
  2. 启动后端 .NET 项目:dotnet run
    • 访问 Swagger:https://localhost:7276/swagger
  3. 启动前端 Vue 项目:npm run devhttp://localhost:5173
  4. 前端登录:账号admin/ 密码admin123
  5. 进入用户列表页:
    • 控制台打印SignalR 实时通知连接成功
    • 点击【新增 / 编辑 / 删除用户】
    • 页面自动弹出alert实时通知
  6. 多开浏览器标签页 / 多台浏览器操作,所有在线页面都会收到推送

五、扩展功能(可选进阶)

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. 单点推送(只推送给指定用户)

核心原理

  1. 每个前端建立 SignalR 连接后,都会生成唯一 ConnectionId
  2. 前端把当前ConnectionId+ 当前登录用户信息(用户名 / 用户 ID)传给后端保存。
  3. 后端维护用户ID <-> ConnectionId映射关系(用内存 / Redis 存储)。
  4. 业务触发推送时,根据目标用户 ID 查到对应ConnectionId,调用Clients.Client(连接ID)实现只推送给该用户

两种存储方案:

  • 内存存储:简单、适合单机部署,服务重启数据丢失
  • Redis 存储:支持集群、服务重启不丢失,推荐生产使用
  1. 后端NoticeHub增加方法:
    public async Task SendSingleNotice(string connId, string title, string content) { await Clients.Client(connId).SendAsync("ReceiveNotice", title, content); }
  2. 前端连接成功后,把connection.connectionId传给后端,实现定向推送。

3. 集群部署(多服务器 SignalR)

如果后端多实例部署,需配置SignalR Redis 背板,实现多节点消息同步:

dotnet add package Microsoft.AspNetCore.SignalR.StackExchangeRedis

Program.cs 追加:

builder.Services.AddSignalR().AddStackExchangeRedis(builder.Configuration["ConnectionStrings:Redis"]);

六、常见问题排错

  1. 前端连接 SignalR 401 未授权

    • 检查accessTokenFactory是否正确携带 Token
    • 检查后端跨域是否加了.AllowCredentials()
  2. 前端连接超时 / 失败

    • 确认后端路由/noticeHub可访问
    • 关闭浏览器跨域插件 / 后端核对前端域名
  3. 能连接,但收不到消息

    • 前后端方法名必须一致:后端SendAsync("ReceiveNotice")↔ 前端on("ReceiveNotice")(大小写敏感)

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询