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
相关产品推荐
相关产品推荐

