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

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();
 });

问题原因分析

  1. 默认认证方案优先级限制:当前默认认证方案为JwtBearerDefaults.AuthenticationScheme(对应realm1),认证流程只会优先触发默认方案的事件,不会自动尝试其他配置的方案
  2. SignalR Hub未绑定多认证方案:虽然控制器配置了包含多方案的默认授权策略,但SignalR Hub未明确应用该策略,导致realm2方案未参与到Hub连接的认证流程中
  3. 事件触发逻辑限制:只有当认证方案被纳入当前请求的认证流程时,其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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 11:14:55