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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 12:45:26