ASP.NET Core 8.0 SignalR多JWT方案OnMessageReceived未触发问题
ASP.NET Core 8.0 SignalR多JWT认证方案(Keycloak多Realm)问题排查
问题现象
- 使用SignalR WebSocket Hub时无法获取用户Claims,连接Hub阶段用户未完成认证
- 已按要求将token放在查询字符串中,配置了两个Keycloak Realm的JWT认证方案,但第二个Realm(realm2)的OnMessageReceived事件未触发
- 仅默认Bearer方案(对应realm1)工作正常:OnMessageReceived事件触发,认证功能正常
现有配置代码
多Realm认证配置
var authBuilder = services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }); authBuilder.AddJwtBearer(o => { o.Authority = "https://keycloak.example.com/realms/realm1"; o.Audience = "frontend-client"; o.Events = new JwtBearerEvents { OnMessageReceived = context => { var accessToken = context.Request.Query["access_token"]; var path = context.HttpContext.Request.Path; if (!string.IsNullOrEmpty(accessToken) && (path.StartsWithSegments("/events"))) { context.Token = accessToken; context.Request.Headers.Authorization = "Bearer " + accessToken; } return Task.CompletedTask; }, }; }).AddJwtBearer("realm2", o => { o.Authority = "https://keycloak.example.com/realms/realm2"; o.Audience = "frontend-client"; o.Events = new JwtBearerEvents { OnMessageReceived = context => { var accessToken = context.Request.Query["access_token"]; var path = context.HttpContext.Request.Path; if (!string.IsNullOrEmpty(accessToken) && (path.StartsWithSegments("/events"))) { context.Token = accessToken; context.Request.Headers.Authorization = "Bearer " + accessToken; } return Task.CompletedTask; }, }; });
单Realm正常工作的配置(对比用)
var authBuilder = services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }); authBuilder.AddJwtBearer(o => { o.Authority = "https://keycloak.example.com/realms/realm1"; o.Audience = "frontend-client"; o.Events = new JwtBearerEvents { OnMessageReceived = context => { var accessToken = context.Request.Query["access_token"]; var path = context.HttpContext.Request.Path; if (!string.IsNullOrEmpty(accessToken) && (path.StartsWithSegments("/events"))) { context.Token = accessToken; context.Request.Headers.Authorization = "Bearer " + accessToken; } return Task.CompletedTask; }, }; });
Hub获取Claims代码
var caller = _principal as ClaimsPrincipal; // principal 是 IIPrincipal var claimValue = caller?.FindFirst("sub")?.Value;
授权策略配置
services.AddAuthorization(options => { options.DefaultPolicy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .AddAuthenticationSchemes(schemes.ToArray()) .Build(); });
问题原因分析
- 默认认证方案优先级限制:当前默认认证方案为
JwtBearerDefaults.AuthenticationScheme(对应realm1),认证流程只会优先触发默认方案的事件,不会自动尝试其他配置的方案 - SignalR Hub未绑定多认证方案:虽然控制器配置了包含多方案的默认授权策略,但SignalR Hub未明确应用该策略,导致realm2方案未参与到Hub连接的认证流程中
- 事件触发逻辑限制:只有当认证方案被纳入当前请求的认证流程时,其OnMessageReceived事件才会触发,realm2事件未触发说明它未被调用
解决方案
方案1:给SignalR Hub指定多认证方案
给Hub添加[Authorize]特性并明确指定所有需要的认证方案,确保流程会尝试所有配置的JWT方案:
[Authorize(AuthenticationSchemes = "Bearer,realm2")] public class EventsHub : Hub { // Hub方法实现 }
方案2:全局配置多方案认证代理
通过PolicyScheme代理多个JWT方案,让全局请求(包括SignalR)自动尝试所有认证方案:
services.AddAuthentication() .AddJwtBearer("realm1", o => { o.Authority = "https://keycloak.example.com/realms/realm1"; o.Audience = "frontend-client"; o.Events = new JwtBearerEvents { OnMessageReceived = context => { var accessToken = context.Request.Query["access_token"]; var path = context.HttpContext.Request.Path; if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/events")) { context.Token = accessToken; } return Task.CompletedTask; }, }; }) .AddJwtBearer("realm2", o => { o.Authority = "https://keycloak.example.com/realms/realm2"; o.Audience = "frontend-client"; o.Events = new JwtBearerEvents { OnMessageReceived = context => { var accessToken = context.Request.Query["access_token"]; var path = context.HttpContext.Request.Path; if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/events")) { context.Token = accessToken; } return Task.CompletedTask; }, }; }) .AddPolicyScheme("MultiRealmBearer", "MultiRealmBearer", options => { options.ForwardDefaultSelector = context => null; // 让授权策略处理多方案尝试 }); // 设置默认授权策略使用代理方案 services.AddAuthorization(options => { options.DefaultPolicy = new AuthorizationPolicyBuilder("MultiRealmBearer") .RequireAuthenticatedUser() .AddAuthenticationSchemes("realm1", "realm2") .Build(); });
方案3:优化OnMessageReceived逻辑
无需在事件中修改Authorization请求头,直接设置context.Token即可,SignalR认证流程会自动使用该Token验证,避免头修改干扰后续逻辑。
额外排查建议
- 确认
schemes.ToArray()确实包含"Bearer"和"realm2"两个方案名称 - 使用对应Realm的有效token测试,确保token的Audience、Issuer参数与配置匹配
- 启用认证日志排查流程:在
appsettings.json中添加
{ "Logging": { "LogLevel": { "Microsoft.AspNetCore.Authentication": "Debug" } } }
内容的提问来源于stack exchange,提问作者Ghazanfar Khan
相关产品推荐
相关产品推荐

