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

为SignalR Hub添加JWT授权后WebSocket连接失败求助

问题:SignalR Hub添加JWT授权后WebSocket无法连接

我在开发在线棋类应用,使用SignalR Hub处理匹配逻辑,尝试通过JWT授权限制Hub的访问,但添加授权后WebSocket无法正常连接。移除[Authorize]标签后所有功能恢复正常,已确认IIS服务器中WebSocket功能已启用。

相关代码

Hub代码

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class GameHub : Hub
{
    private readonly ApplicationDbContext context;
    public static List<Player> _Players = new List<Player>();

    public GameHub(ApplicationDbContext context)
    {
        this.context = context;
    }

    public async Task SendMessage(string user, string message)
    {   
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
    public async Task JoinQueue(string userEmail)
    {
        Player player = context.Players.Where(x => x.Email == userEmail).FirstOrDefault();
        Player player2 = null;  
        if (player != null)
        {
            _Players.Add(player);
        }
        else
            return;
        await Clients.All.SendAsync("QueuePlayerCountUpdate", _Players.Count);
        
        bool foundMatch = false;
        int eloRange = 50;
        while (!foundMatch) {
            if (_Players.Where(x => player.Elo - eloRange <= x.Elo && 
            x.Elo <= player.Elo + eloRange && 
            x.PlayerId != player.PlayerId).Any())
                player2 = _Players.Where(x => player.Elo - eloRange <= x.Elo 
&& x.Elo <= player.Elo + eloRange).FirstOrDefault();
            if(player2 == null)
            {
                Thread.Sleep(5000);
                eloRange += 50;
            }
            else
            {
                _Players.Remove(player);
                _Players.Remove(player2);
                foundMatch = true;
                Clients.Caller.SendAsync("MatchFound",player2.Email);
                Clients.User(player2.AspNetUserId).SendAsync("MatchFound", player.Email);
            }
        }
    }
}

Startup代码

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using System.Text;

namespace BackEnd_ASP.NET
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
            services.AddControllers();
            services.AddEndpointsApiExplorer();
            services.AddSwaggerGen();
            services.AddSignalR();
            services.AddCors(options =>
            {
                var frontendURL = Configuration.GetValue<string>("frontend_url");
                options.AddDefaultPolicy(builder =>
                {
                    builder.WithOrigins("http://localhost:3000").AllowAnyMethod().AllowAnyHeader().AllowCredentials();
                });
            });

            services.AddIdentity<IdentityUser, IdentityRole>().
                AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(
                options =>
                {
                    options.Events = new JwtBearerEvents
                    {
                        OnMessageReceived = context =>
                        {
                            var accessToken = context.Request.Query["access_token"];

                            if (!string.IsNullOrEmpty(accessToken))
                            {
                                context.Token = accessToken;
                            }
                            return Task.CompletedTask;
                        }
                    };
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuer = false,
                        ValidateAudience = false,
                        ValidateLifetime = false,
                        ValidateIssuerSigningKey = true,
                        IssuerSigningKey = new SymmetricSecurityKey
                        (Encoding.UTF8.GetBytes(Configuration["keyjwt"])),
                        ClockSkew = TimeSpan.Zero
                    };
                    
                });
        }
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseSwagger();
                app.UseSwaggerUI();
            }

            app.UseRouting();

            app.UseCors();  
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
                endpoints.MapHub<GameHub>("/GameHub");
            });
            app.UseAuthorization();
        }
    }
}

客户端连接代码

const connection = new HubConnectionBuilder().
            withUrl("https://localhost:7244/GameHub",{
                accessTokenFactory: ()=> {return `bearer ${token}`},  
                skipNegotiation: true,
                transport: HttpTransportType.WebSockets
              }).
            configureLogging(LogLevel.Information).build(); 

已尝试的无效方法

  • 将[Authorize]标签移到端点配置上
  • 同时在端点和Hub上添加授权
  • 移除token中的bearer前缀
  • 关闭skipNegotiation选项
  • 修改OnMessageReceived逻辑

解决方案

1. 修正中间件顺序

Startup的Configure方法中,中间件执行顺序是核心问题。app.UseAuthentication()和app.UseAuthorization()必须放在app.UseRouting()之后,app.UseEndpoints()之前,否则授权逻辑无法在端点请求前触发。修正后的代码:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseSwagger();
        app.UseSwaggerUI();
    }

    app.UseRouting();

    app.UseCors();
    app.UseAuthentication(); // 新增:认证中间件必须在授权之前
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
        endpoints.MapHub<GameHub>("/GameHub");
    });
}

2. 修正客户端Token传递

使用skipNegotiation: true和WebSocket传输时,SignalR的accessTokenFactory返回值会直接作为查询参数access_token的值,不需要添加bearer前缀。修正客户端代码:

const connection = new HubConnectionBuilder()
    .withUrl("https://localhost:7244/GameHub", {
        accessTokenFactory: () => token, // 移除bearer前缀
        skipNegotiation: true,
        transport: HttpTransportType.WebSockets
    })
    .configureLogging(LogLevel.Information)
    .build();

3. 优化JWT验证配置

当前TokenValidationParameters中ValidateLifetime = false,建议改为true以确保token有效性(开发测试阶段可暂时关闭,生产环境必须开启):

options.TokenValidationParameters = new TokenValidationParameters
{
    ValidateIssuer = false,
    ValidateAudience = false,
    ValidateLifetime = true, // 修改为true
    ValidateIssuerSigningKey = true,
    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["keyjwt"])),
    ClockSkew = TimeSpan.Zero
};

4. 可选:端点层面添加授权

如果需要强化端点级别的授权,可以在MapHub时添加RequireAuthorization,与Hub上的[Authorize]形成双重验证:

endpoints.MapHub<GameHub>("/GameHub").RequireAuthorization(JwtBearerDefaults.AuthenticationScheme);

额外检查

确保JWT中包含用户的唯一标识(如sub声明对应IdentityUser的Id),因为Clients.User(player2.AspNetUserId)依赖用户Id定位连接,若JWT中没有正确的用户标识,后续消息推送也会出现问题。

内容的提问来源于stack exchange,提问作者Gunal S

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 18:14:57