Blazor Server+SignalR搭建中继服务的身份认证实现问题
Blazor Server + SignalR 中继服务器身份认证实现方案
需求背景
- 基于Blazor Server搭配SignalR搭建中继服务器,接入Winform/Xamarin客户端
- 以数据库存储的用户名称/ID作为消息路由依据,实现定向消息转发
- 无Blazor Server开发经验,当前需先完成身份认证链路搭建
现有实现代码
ChatHub 核心代码
[Authorize] public class ChatHub : Hub { public Task SendMessageAsync(string user, string message) { Debug.WriteLine(Context.UserIdentifier); Debug.WriteLine(Context?.User?.Claims.FirstOrDefault()); return Clients.All.SendAsync("ReceiveMessage", user, message); ; } public Task DirectMessage(string user, string message) { return Clients.User(user).SendAsync("ReceiveMessage", user, message); } }
Program.cs 服务配置代码
var builder = WebApplication.CreateBuilder(args); var services = builder.Services; services.AddTransient<IUserIdProvider, MyUserIdProvider>(); services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; }).AddJwtBearer(options => { options.Events = new JwtBearerEvents { OnMessageReceived = context => { //var accessToken = context.Request.Query["access_token"]; var accessToken = context.Request.Headers["Authorization"]; var path = context.HttpContext.Request.Path; if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/chathub")) { context.Token = accessToken; } return Task.CompletedTask; } }; }); services.AddRazorPages(); services.AddServerSideBlazor(); services.AddSignalR(); var app = builder.Build(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseAuthentication(); app.UseRouting(); app.UseAuthorization(); app.MapBlazorHub(); app.MapHub<ChatHub>("/chathub"); app.MapFallbackToPage("/_Host"); app.Run();
Winform 测试客户端连接代码
HubConnection chatHubConnection; chatHubConnection = new HubConnectionBuilder() .WithUrl("https://localhost:7109/chathub", options => { options.AccessTokenProvider = () => Task.FromResult(token); }) .WithAutomaticReconnect() .Build(); private async void HubConBtn_Click(object sender, EventArgs e) { chatHubConnection.On<string, string>("ReceiveMessage", (user, message) => { this.Invoke(() => { var newMessage = $"{user}: {message}"; MessagesLB.Items.Add(newMessage); }); }); try { await chatHubConnection.StartAsync(); MessagesLB.Items.Add("Connected!"); HubConBtn.Enabled = false; SendMessageBtn.Enabled = true; } catch (Exception ex) { MessagesLB.Items.Add(ex.Message); } }
待实现目标
- 用户连接时校验数据库中是否存在对应账号,非法用户直接拒绝连接
- 正确填充
Context.UserIdentifier字段,支持Clients.User(userId)定向推送 - 完成JWT令牌校验、用户与SignalR连接的自动关联
具体实现步骤
- 修正JWT令牌读取逻辑
现有代码直接将Authorization头内容赋值给context.Token存在问题,标准Bearer头格式为Bearer {token},需要去除前缀;同时WebSockets连接不支持自定义头传递token,需要兼容Query参数传参场景:OnMessageReceived = context => { var path = context.HttpContext.Request.Path; if (path.StartsWithSegments("/chathub")) { // 优先从Header取token,去除Bearer前缀 var accessToken = context.Request.Headers["Authorization"] .FirstOrDefault()? .Replace("Bearer ", string.Empty); // WebSockets场景从Query参数取token if (string.IsNullOrWhiteSpace(accessToken)) { accessToken = context.Request.Query["access_token"].FirstOrDefault(); } if (!string.IsNullOrWhiteSpace(accessToken)) { context.Token = accessToken; } } return Task.CompletedTask; } - 补全JWT验证配置
现有JWT配置缺少签名、有效期、签发方/接收方校验规则,合法令牌也会验证失败。需要补充TokenValidationParameters配置,且签发JWT时必须将用户唯一ID作为ClaimTypes.NameIdentifier类型的声明写入令牌:.AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = "你的服务端唯一标识", ValidAudience = "你的客户端唯一标识", IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes("你的签名密钥,长度不得低于16位") ), ClockSkew = TimeSpan.Zero }; // 此处放置上面修正后的OnMessageReceived事件逻辑 }) - 实现自定义UserIdProvider
你已经注册了MyUserIdProvider,只需要在实现类中从用户声明里取出提前写入的用户唯一ID返回,SignalR会自动将该值赋值给Context.UserIdentifier,无需手动赋值:public class MyUserIdProvider : IUserIdProvider { public string? GetUserId(HubConnectionContext connection) { // 读取JWT中写入的用户唯一ID,和签发令牌时的声明类型保持一致 return connection.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value; } } - 增加连接时数据库校验
不需要额外编写中间件,直接在Hub中重写OnConnectedAsync方法即可,连接建立时先校验用户状态,不合法直接终止连接:[Authorize] public class ChatHub : Hub { private readonly IUserRepository _userRepository; // 通过构造函数注入你的数据库访问服务 public ChatHub(IUserRepository userRepository) { _userRepository = userRepository; } public override async Task OnConnectedAsync() { var userId = Context.UserIdentifier; // 校验用户是否存在、是否处于允许连接的状态 bool userIsValid = await _userRepository.CheckUserIsValid(userId); if (!userIsValid) { Context.Abort(); return; } await base.OnConnectedAsync(); } // 保留原有的SendMessageAsync、DirectMessage方法 } - 客户端适配
现有Winform客户端通过AccessTokenProvider传递token的逻辑可正常运行,传入服务端签发的合法JWT即可;服务端已经兼容WebSockets场景的Query传参,后续切换传输模式不需要修改客户端代码。
内容的提问来源于stack exchange,提问作者Ifirit
相关产品推荐
相关产品推荐

