无法连接到 Blazor webassembly 中的 SignalR

Failed to connect to SignalR in Blazor webassembly

我正在尝试从我的 blazor webassembly 客户端连接到 SignalR 服务,但我认为这在 CORS 上失败了。这是我的剃刀文件中的代码。

m_connection = new HubConnectionBuilder()
    .WithUrl(myMircoServiceUrl, options =>
    {
       options.AccessTokenProvider = () => Task.FromResult(userService.Token);
    })
   .WithAutomaticReconnect()
   .Build();
await m_connection.StartAsync();

然后在 webassembly 日志记录中我看到以下错误:

从源“http://localhost:5010”获取 'xxxx/negotiate?negotiateVersion=1' 的访问已被 CORS 策略阻止:对预检请求的响应未通过访问控制检查:否 'Access-Control-Allow-Origin' header 出现在请求的资源上。如果不透明响应满足您的需求,请将请求的模式设置为 'no-cors' 以在禁用 CORS 的情况下获取资源。

我在 Blazor 服务器配置中添加了以下 CORS 策略,并在微服务配置中添加了类似内容:

        app.UseResponseCompression();

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseBlazorDebugging();
        }
        else
        {
            app.UseExceptionHandler(@"/Error");
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseRouting();
        app.UseCookiePolicy();
        app.UseAuthentication();
        app.UseAuthorization();

        app.UseCors(policy => policy
            .WithOrigins("http://localhost:5010")
            .AllowAnyHeader()
            .AllowAnyMethod());

        app.UseClientSideBlazorFiles<Client.Program>();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
            endpoints.MapFallbackToClientSideBlazor<Client.Program>(@"index.html");
        });

有人知道哪里出了问题吗?

更新 1

我现在在 Chrome 控制台中看到以下错误:

dotnet.js:1 到 'ws://localhost:5000/hubs/posts?id=9Jxs0DhP924zgw_eIeE9Lg' 的 WebSocket 连接失败:HTTP 身份验证失败;没有可用的有效凭据

更新 2

我从 SignalR 集线器中删除了 [Authorize] 属性,现在它可以连接了。我可以向集线器发送消息。问题是这个属性是有原因的,因为我不希望人们可以订阅不适合他们的消息

更新 3

仍然没有进展。考虑使用 IdentityServer4 将身份验证提取到单独的微服务。最后状态是我有以下启动例程:

就我而言,ASP.NET Core 2.2 我有一个 API,我希望能够使用 API 中的 SignalR 连接到我的客户端应用程序。

我有

的项目
  1. 网络API
  2. IdentityServer4
  3. MVC 客户端

以ASP.NET核心身份作为用户管理

为了让您的用户通过身份验证,您需要像这样实现一个 IUserIdProvider

 public class IdBasedUserIdProvider : IUserIdProvider
 {
      public string GetUserId(HubConnectionContext connection)
      {
           //TODO: Implement USERID Mapper Here
           //throw new NotImplementedException();
           //return whatever you want to map/identify the user by here. Either ID/Email
           return connection.User.FindFirst("sub").Value;
      }
 }

有了这个,我确保我正在将 ID/Email 推送到我从服务器或客户端调用的方法。虽然我总是可以在 HubContext 上使用 .User 并且它工作正常。

在我的网站 API Startup.cs 文件中我想出了

public void ConfigureServices(IServiceCollection services)
{
     services.AddCors(cfg =>
           {
                cfg.AddDefaultPolicy(policy =>
                {
                     policy.WithOrigins(Configuration.GetSection("AuthServer:DomainBaseUrl").Get<string[]>())
                     .AllowAnyHeader()
                     .AllowAnyMethod()
                     .AllowCredentials()
                     .SetIsOriginAllowed((_) => true)
                     .SetIsOriginAllowedToAllowWildcardSubdomains();
                });
           });
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, UserManager<AppUser> userManager,
           RoleManager<IdentityRole> roleManager){

    app.UseCors();

}

注意 Configuration.GetSection("AuthServer:DomainBaseUrl").Get() 从配置文件中检索允许 CORS 的域列表。

我在 My Client App COnfigureService Method 中做了这个配置

           services.AddCors(cfg =>
           {
                cfg.AddDefaultPolicy(policy => {
                     policy.AllowAnyHeader();
                     policy.AllowAnyMethod();
                     policy.SetIsOriginAllowed((host) => true);
                     policy.AllowAnyOrigin();
                });
           });

希望对您的情况有所帮助。

最好的解决方案确实如 Ismail Umer 所描述的那样,使用类似 IdentityServer4 的独立身份验证服务。并在所有其他服务中使用此服务。这是我将在下一次迭代中做的事情。

作为短期解决方案,我临时将 blazor 服务器部分移到了我的 api 服务中,并使用双重身份验证方法(JWT header 或 cookie)。

        var key = Encoding.UTF8.GetBytes(m_configuration[@"SecurityKey"]);
        services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    NameClaimType = @"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(key),
                    ValidateIssuer = false,
                    ValidateAudience = false,
                    ValidateLifetime = true
                };
            })
            .AddCookie();

        // TODO: For time being support dual authorization. At later stage split in various micro-services and use IdentityServer4 for Auth
        services.AddAuthorization(options =>
        {
            var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(
                CookieAuthenticationDefaults.AuthenticationScheme,
                JwtBearerDefaults.AuthenticationScheme);
            defaultAuthorizationPolicyBuilder =
                defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
            options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
        });

这是 Microsoft.AspNetCore.SignalR.Client 3.1.3 的问题。 您可以在评论中阅读 here

您可以等待更新或暂时修复此问题:

  1. 禁用协商
  2. 显式设置 WebSocket 传输
  3. 修改查询url
  4. 添加 OnMessageReceived 处理程序

客户端:

  var token = await GetAccessToken();
  var hubConnection = new HubConnectionBuilder()
        .WithUrl($"/notification?access_token={token}", options =>
        {
            options.SkipNegotiation = true;
            options.Transports = HttpTransportType.WebSockets;
            options.AccessTokenProvider = GetAccessToken;

        })
        .Build();

服务器端:

        public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(options =>
        {
            // ...
        })
        .AddJwtBearer(options =>
        {
            options.Events = new JwtBearerEvents
            {
                OnMessageReceived = context =>
                {
                    var accessToken = context.Request.Query["access_token"];

                    var path = context.HttpContext.Request.Path;
                    if (!string.IsNullOrEmpty(accessToken) &&
                        (path.StartsWithSegments("/notification", System.StringComparison.InvariantCulture)))
                    {
                        context.Token = accessToken;
                    }
                    return Task.CompletedTask;
                },
            };
        });
    }

我在使用 CORS 和之后的 Websocket 时遇到了同样的错误。
在我的例子中,后备 longPolling 被用作连接有效但控制台记录错误 HTTP Authentication failed; no valid credentials available.
的原因 如果您使用 Identity Server JWT,则以下代码解决了我的案例的错误。
(代码来自Microsoft SignalR Documentation - Authentication and authorization in ASP.NET Core SignalR - Identity Server JWT authentication

services.AddAuthentication()
    .AddIdentityServerJwt();
// insert:
 services.TryAddEnumerable(
    ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>, 
        ConfigureJwtBearerOptions>());
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Options;
public class ConfigureJwtBearerOptions : IPostConfigureOptions<JwtBearerOptions>
{
    public void PostConfigure(string name, JwtBearerOptions options)
    {
        var originalOnMessageReceived = options.Events.OnMessageReceived;
        options.Events.OnMessageReceived = async context =>
        {
            await originalOnMessageReceived(context);
                
            if (string.IsNullOrEmpty(context.Token))
            {
                var accessToken = context.Request.Query["access_token"];
                var path = context.HttpContext.Request.Path;
                
                if (!string.IsNullOrEmpty(accessToken) && 
                    path.StartsWithSegments("/hubs"))
                {
                    context.Token = accessToken;
                }
            }
        };
    }
}

重要提示:您的路线必须以 hubs 开头才能触发选项!
(参见第 path.StartsWithSegments("/hubs")) 行)

app.UseEndpoints(e =>
            {
                ...
                e.MapHub<ChatHub>("hubs/chat");
            });