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

NopCommerce插件开发:SignalR连接触发OnStarting异常求助

解决NopCommerce中SignalR OnConnectedAsync调用身份验证时的"OnStarting cannot be set because the response has already started"异常

异常详情

System.InvalidOperationException: OnStarting cannot be set because the response has already started

完整调用栈:

Microsoft.AspNetCore.SignalR.HubConnectionHandler: Error: Error when dispatching 'OnConnectedAsync' on hub.

System.InvalidOperationException: OnStarting cannot be set because the response has already started.
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ThrowResponseAlreadyStartedException(String value)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.OnStarting(Func`2 callback, Object state)
   at Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler.InitializeHandlerAsync()
   at Microsoft.AspNetCore.Authentication.AuthenticationHandler`1.InitializeAsync(AuthenticationScheme scheme, HttpContext context)
   at Microsoft.AspNetCore.Authentication.AuthenticationHandlerProvider.GetHandlerAsync(HttpContext context, String authenticationScheme)
   at Microsoft.AspNetCore.Authentication.AuthenticationService.AuthenticateAsync(HttpContext context, String scheme)
   at Nop.Services.Authentication.CookieAuthenticationService.GetAuthenticatedCustomerAsync()
   at Nop.Plugin.API.Services.BearerTokenOrCookieAuthenticationService.GetAuthenticatedCustomerAsync()
   at Nop.Web.Framework.WebWorkContext.SetCurrentCustomerAsync(Customer customer) 
   at Nop.Web.Framework.WebWorkContext.GetCurrentCustomerAsync() 
   at Nop.Plugin.Widgets.RealTimeChat.Hubs.MessageHub.IsAuthenticated() in Documents\nopCommerce\src\Plugins\Nop.Plugin.Widgets.RealTimeChat\Hubs\MessageHub.cs:line 63
   at Nop.Plugin.Widgets.RealTimeChat.Hubs.MessageHub.OnConnectedAsync() in nopCommerce\src\Plugins\Nop.Plugin.Widgets.RealTimeChat\Hubs\MessageHub.cs:line 23
   at Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher`1.OnConnectedAsync(HubConnectionContext connection)
   at Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher`1.OnConnectedAsync(HubConnectionContext connection)
   at Microsoft.AspNetCore.SignalR.HubConnectionHandler`1.RunHubAsync(HubConnectionContext connection)
Microsoft.AspNetCore.Routing.EndpointMiddleware: Information: Executed endpoint '/messageHub'

相关代码

API插件Startup文件

public void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{         
    services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, jwtBearerOptions =>
    {
        jwtBearerOptions.TokenValidationParameters = //token validation parameter
        jwtBearerOptions.Events = new JwtBearerEvents
        {
            OnChallenge = context =>
            {
                context.HandleResponse();
                context.Response.StatusCode = StatusCodes.Status401Unauthorized;
                context.Response.ContentType = "application/json";
                var result = JsonConvert.SerializeObject(new { error = "Unauthorized access" });
                return context.Response.WriteAsync(result);
            }
        };
    });
    
    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

    AddAuthorizationPipeline(services);
 
    services.Configure<KestrelServerOptions>(options =>
    {
        options.AllowSynchronousIO = true;
    });
    services.Configure<IISServerOptions>(options =>
    {
        options.AllowSynchronousIO = true;
    });
    services.Configure<MvcNewtonsoftJsonOptions>(options =>
    {
        options.SerializerSettings.Converters.Add(new StringEnumConverter());
    });

    services.AddControllers();
    services.AddSwaggerGen(options =>
    {
        options.SwaggerDoc("v1", new OpenApiInfo
        {
            Version = "v1",
            Title = "nop commerce mobile api"
        });
    });

    services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigSwaggerOptions>();

    services.AddCors(opt =>
    {
        opt.AddPolicy(name: "CorsPolicy", builder =>
        {
            builder
                .AllowAnyOrigin()
                .AllowAnyHeader()
                .AllowAnyMethod();
        });
    });
}
 
public void Configure(IApplicationBuilder app)
{
    var environment = app.ApplicationServices.GetRequiredService<IWebHostEnvironment>();

    var rewriteOptions = new RewriteOptions().AddRewrite("api/token", "/token", true);
    app.UseRewriter(rewriteOptions);

    app.UseCors("CorsPolicy"); 

    app.MapWhen(context => context.Request.Path.StartsWithSegments(new PathString("/api")), a =>
    {
        a.Use(async (context, next) =>
        {
            context.Request.EnableBuffering();
            await next();
        });

        // Authentication and authorization
        a.UseAuthentication();
        a.UseAuthorization();

        // Routing and endpoints
        a.UseRouting();
        a.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    });

    // Swagger configuration
    app.UseSwagger(options => options.RouteTemplate = "api/swagger/{documentName}/swagger.json");
    app.UseSwaggerUI(options =>
    {
        options.RoutePrefix = "api/swagger";
        options.SwaggerEndpoint("/api/swagger/v1/swagger.json", "Nop API v1");
    });
}  

实时聊天插件Startup文件

public void Configure(IApplicationBuilder application)
{
    application.UseRouting();
    application.UseAuthorization();
    application.UseEndpoints(endpoints =>
    {
        endpoints.MapHub<MessageHub>(RealTimeChatConstants.HubConnectionRoute);
    });
}

public void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
    services.AddSignalR(r =>
    {
        r.EnableDetailedErrors = true;
    });

    //register services
}

MessageHub代码

public class MessageHub : Hub
{
    // fields and ctor
    public override async Task OnConnectedAsync()
    {
        if (!await IsAuthenticated())
        {
            return;
        }

        var currentCustomer = await _workContext.GetCurrentCustomerAsync();

        var existingUser = _connectedUsers.FirstOrDefault(x => x.UserId == currentCustomer.Id);

        if (existingUser == null)
        {
            _connectedUsers.Add(new UserConnection { ConnectionId = Context.ConnectionId, UserId = currentCustomer.Id });
        }
        else
        {
            existingUser.ConnectionId = Context.ConnectionId;
        }

        await base.OnConnectedAsync();
    }
}

身份验证方法

public virtual async Task<Customer> GetAuthenticatedCustomerAsync()
{
    //try to get authenticated user identity
    var authenticateResult = await _httpContextAccessor.HttpContext.AuthenticateAsync(NopAuthenticationDefaults.AuthenticationScheme);// getting error in this line
    if (!authenticateResult.Succeeded)
        return null;
    //logic to get customer

    return customer;
}

JS连接代码

var setUpConnection = function (hubUrl) {
    connection = new signalR.HubConnectionBuilder()
      .withUrl(hubUrl)
      .configureLogging(signalR.LogLevel.Information)
      .build();

    setUpSignalRConnection();
  }

解决方案

1. 修正实时聊天插件的中间件顺序

SignalR连接流程中,响应在OnConnectedAsync执行前已启动,此时无法再修改响应头或添加认证回调。必须在路由前提前执行认证逻辑:

public void Configure(IApplicationBuilder application)
{
    // 先执行认证中间件,确保连接建立前完成身份验证
    application.UseAuthentication();
    application.UseRouting();
    application.UseAuthorization();
    application.UseEndpoints(endpoints =>
    {
        endpoints.MapHub<MessageHub>(RealTimeChatConstants.HubConnectionRoute);
    });
}

2. 直接从Hub上下文获取已认证用户

认证中间件提前执行后,无需再次调用AuthenticateAsync,直接从Context.User获取身份信息:

public override async Task OnConnectedAsync()
{
    if (!Context.User.Identity.IsAuthenticated)
    {
        await Context.AbortAsync();
        return;
    }

    // 从Claims中提取用户ID,查询Customer
    var userIdClaim = Context.User.FindFirst(ClaimTypes.NameIdentifier);
    if (userIdClaim == null)
    {
        await Context.AbortAsync();
        return;
    }
    
    var currentCustomer = await _customerService.GetCustomerByIdAsync(int.Parse(userIdClaim.Value));
    if (currentCustomer == null)
    {
        await Context.AbortAsync();
        return;
    }

    // 后续连接逻辑
    var existingUser = _connectedUsers.FirstOrDefault(x => x.UserId == currentCustomer.Id);
    if (existingUser == null)
    {
        _connectedUsers.Add(new UserConnection { ConnectionId = Context.ConnectionId, UserId = currentCustomer.Id });
    }
    else
    {
        existingUser.ConnectionId = Context.ConnectionId;
    }

    await base.OnConnectedAsync();
}

3. 适配JWT认证(如果需要)

如果聊天插件需要支持JWT认证,需在ConfigureServices中添加JWT配置,并修改客户端连接代码携带Token:

服务端配置

public void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
    services.AddSignalR(r =>
    {
        r.EnableDetailedErrors = true;
    });

    // 添加JWT认证支持
    services.AddAuthentication()
        .AddJwtBearer(options =>
        {
            // 复用API的Token验证参数
            options.TokenValidationParameters = // 和API一致的配置
            options.Events = new JwtBearerEvents
            {
                OnMessageReceived = context =>
                {
                    // 从Query参数获取SignalR连接的Token
                    var accessToken = context.Request.Query["access_token"];
                    var path = context.HttpContext.Request.Path;
                    if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments(RealTimeChatConstants.HubConnectionRoute))
                    {
                        context.Token = accessToken;
                    }
                    return Task.CompletedTask;
                }
            };
        });

    //register services
}

客户端连接代码

var setUpConnection = function (hubUrl, token) {
    connection = new signalR.HubConnectionBuilder()
      .withUrl(hubUrl, {
        accessTokenFactory: () => token
      })
      .configureLogging(signalR.LogLevel.Information)
      .build();

    setUpSignalRConnection();
  }

内容的提问来源于stack exchange,提问作者Sadikul Haque Sadi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 11:39:52