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

使用C#客户端连接SignalR Hub时Context.User为null的原因排查

问题:SignalR C#客户端调用时Hub中Context.User为null,JavaScript客户端正常

场景说明

我有一个ASP.NET MVC应用,使用SignalR实现实时竞价功能,需要同时支持JavaScript和C#(控制器中调用)客户端。使用JavaScript客户端连接Hub时,Hub中的Context.User能正确获取到已认证用户;但通过控制器内的C#客户端调用时,Context.User始终为null。

相关代码

Startup.cs

namespace DesignAndBuilding.Web
{
    using System.Reflection;

    using AutoMapper;
    using DesignAndBuilding.Data;
    using DesignAndBuilding.Data.Common;
    using DesignAndBuilding.Data.Common.Repositories;
    using DesignAndBuilding.Data.Models;
    using DesignAndBuilding.Data.Repositories;
    using DesignAndBuilding.Data.Seeding;
    using DesignAndBuilding.Services;
    using DesignAndBuilding.Services.Mapping;
    using DesignAndBuilding.Services.Messaging;
    using DesignAndBuilding.Web.Hubs;
    using DesignAndBuilding.Web.ViewModels;
    using Microsoft.AspNetCore.Authentication;
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.Http;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.EntityFrameworkCore;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;

    public class Startup
    {
        private readonly IConfiguration configuration;

        public Startup(IConfiguration configuration)
        {
            this.configuration = configuration;
        }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            services.AddDbContext<ApplicationDbContext>(
                options => options.UseSqlServer(this.configuration.GetConnectionString("DefaultConnection")));

            services.AddDefaultIdentity<ApplicationUser>(IdentityOptionsProvider.GetIdentityOptions)
                .AddRoles<ApplicationRole>().AddEntityFrameworkStores<ApplicationDbContext>();

            services.AddSignalR(options =>
            {
                options.EnableDetailedErrors = true;
            })
            .AddMessagePackProtocol();

            services.Configure<CookiePolicyOptions>(
                options =>
                    {
                        options.CheckConsentNeeded = context => true;
                        options.MinimumSameSitePolicy = SameSiteMode.None;
                    });

            services.AddAutoMapper(typeof(Startup), typeof(MappingProfile));

            services.AddMemoryCache();

            services.AddControllersWithViews(
                options =>
                    {
                        options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
                    }).AddRazorRuntimeCompilation();
            services.AddRazorPages();
            services.AddDatabaseDeveloperPageExceptionFilter();

            services.AddSingleton(this.configuration);

            // Data repositories
            services.AddScoped(typeof(IDeletableEntityRepository<>), typeof(EfDeletableEntityRepository<>));
            services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>));
            services.AddScoped<IDbQueryRunner, DbQueryRunner>();

            // Application services
            services.AddTransient<IEmailSender, NullMessageSender>();
            services.AddTransient<IBuildingsService, BuildingsService>();
            services.AddTransient<IUsersService, UsersService>();
            services.AddTransient<IAssignmentsService, AssignmentsService>();
            services.AddTransient<IBidsService, BidsService>();
            services.AddTransient<INotificationsService, NotificationsService>();
            services.AddTransient<AuthenticationService>();

            // Auto Mapper Configurations
            services.AddSingleton(provider => new MapperConfiguration(mc =>
            {
                mc.AddProfile(new MappingProfile());
            }).CreateMapper());
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            AutoMapperConfig.RegisterMappings(typeof(ErrorViewModel).GetTypeInfo().Assembly);

            // Seed data on application startup
            using (var serviceScope = app.ApplicationServices.CreateScope())
            {
                var dbContext = serviceScope.ServiceProvider.GetRequiredService<ApplicationDbContext>();

                dbContext.Database.Migrate();

                new ApplicationDbContextSeeder().SeedAsync(dbContext, serviceScope.ServiceProvider).GetAwaiter().GetResult();
            }

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

            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseCookiePolicy();

            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(
                endpoints =>
                    {
                        endpoints.MapHub<NotificationsHub>("/notificationshub");
                        endpoints.MapHub<BidsHub>("/bidshub");
                        endpoints.MapControllerRoute("areaRoute", "{area:exists}/{controller=Home}/{action=Index}/{id?}");
                        endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
                        endpoints.MapRazorPages();
                    });
        }
    }
}

JavaScript客户端代码

console.log('Bids script loaded successfully!');

setupConnection = async function start() {
    let connection = null;
    connection = await new signalR.HubConnectionBuilder()
        .withUrl("/bidshub")
        .build();

    await connection.on('newbidplaced', (obj) => {
        console.log(obj);
    })

    await connection.start()
        .catch(err => console.error(err.toString()))
        .then(() => {
            connection.invoke('NewBid', '1', 'testUser', 5.4);
        });
}

setupConnection();

控制器中的C#客户端代码

var connection = new HubConnectionBuilder()
    .AddMessagePackProtocol()
    .WithUrl($"https://{this.Request.Host}/bidshub", options =>
    {
        options.UseDefaultCredentials = true;
    })
    .WithAutomaticReconnect()
    .Build();
await connection.StartAsync();
await connection.InvokeAsync("NewBid", strId, user.Id, bidViewModel.BidPrice);
await connection.DisposeAsync();

差异与问题原因

  • JavaScript客户端的认证逻辑:浏览器发起SignalR连接时,会自动携带当前用户的ASP.NET Identity认证Cookie(同域请求下默认携带),SignalR服务端能通过Cookie解析出用户身份,因此Context.User正常赋值。
  • C#客户端的问题根源:控制器内创建的SignalR客户端是一个全新的独立连接,UseDefaultCredentials = true仅适配Windows身份验证场景(如域环境),无法自动传递当前请求的ASP.NET Identity认证Cookie。没有认证凭据,服务端无法识别用户,导致Context.User为null。

解决方案

推荐方案:使用IHubContext直接调用(无需创建C#客户端)

在控制器中通过依赖注入获取IHubContext<BidsHub>,直接调用Hub方法,这样会复用当前请求的用户身份,无需额外处理认证:

// 控制器构造函数注入IHubContext
private readonly IHubContext<BidsHub> _hubContext;
private readonly UserManager<ApplicationUser> _userManager;

public YourController(IHubContext<BidsHub> hubContext, UserManager<ApplicationUser> userManager)
{
    _hubContext = hubContext;
    _userManager = userManager;
}

// 在Action中调用Hub逻辑
public async Task<IActionResult> PlaceBid(string strId, BidViewModel bidViewModel)
{
    var currentUser = await _userManager.GetUserAsync(User);
    
    // 调用Hub的客户端方法推送消息
    await _hubContext.Clients.All.SendAsync("newbidplaced", new 
    {
        Id = strId,
        UserId = currentUser.Id,
        Price = bidViewModel.BidPrice
    });

    // 如果需要执行Hub中NewBid方法的业务逻辑,建议抽离到服务类中,再通过IHubContext推送结果
    // 避免直接在Hub中写复杂业务逻辑
    
    return RedirectToAction("YourAction");
}

备选方案:手动传递认证Cookie(不推荐)

如果必须使用C#客户端(比如非控制器场景),可以手动从当前请求中获取认证Cookie并添加到SignalR请求头:

// 获取当前请求的Identity认证Cookie
var authCookie = Request.Cookies[".AspNetCore.Identity.Application"];

var connection = new HubConnectionBuilder()
    .AddMessagePackProtocol()
    .WithUrl($"https://{Request.Host}/bidshub", options =>
    {
        // 手动添加Cookie到请求头
        if (!string.IsNullOrEmpty(authCookie))
        {
            options.Headers.Add("Cookie", $".AspNetCore.Identity.Application={authCookie}");
        }
    })
    .Build();

await connection.StartAsync();
await connection.InvokeAsync("NewBid", strId, user.Id, bidViewModel.BidPrice);
await connection.DisposeAsync();

注意:这种方式存在Cookie过期、跨域限制等问题,仅在特殊场景下使用。


内容的提问来源于stack exchange,提问作者Georgi Kolev

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 14:24:54