为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

