Angular与ASP.NET Core集成SignalR时出现WebSocket连接错误求助
Angular + ASP.NET Core SignalR 通知推送:WebSocket未授权错误排查
我用Angular和ASP.NET Core结合SignalR实现通知推送功能,目前连接能成功建立,通知也能正常收发,但控制台持续出现WebSocket相关的未授权错误。已确认Angular端传入的x-api-key和JWT Token均正确,不清楚错误原因和解决方法,附上相关代码请求排查:
Angular端SignalR服务代码
import { Injectable } from "@angular/core"; import * as signalR from "@microsoft/signalr"; import { environment } from "../../../environments/environment"; import { AuthService } from "../../core/auth/services/auth.service"; @Injectable({ providedIn: "root", }) export class SignalRService { private hubConnection: signalR.HubConnection; constructor(private authService: AuthService) { const token = this.authService.getToken(); this.hubConnection = new signalR.HubConnectionBuilder() .withUrl("https://localhost:48348/notificationHub", { headers: { "x-api-key": environment.apiKey, Authorization: `Bearer ${token}`, }, }) .build(); this.hubConnection.on("ReceiveNotification", (message: string) => { console.log("Notification received:", message); // You can update the UI or perform any action here }); this.startConnection(); } private startConnection() { this.hubConnection .start() .then(() => console.log("Connection started")) .catch((err) => console.log("Error while starting connection: " + err)); } }
ASP.NET Core端Program.cs代码
using ELM.Application; using ELM.Infrastructure.Data; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; using Serilog; using System.Text; using EML.WebApi.Middlewares; using Microsoft.Extensions.Options; using ELM.Application.Hub; using ELM.Application.Hub.Service; var builder = WebApplication.CreateBuilder(args); // Load configuration from appsettings.json var appsettingsconfiguration = new ConfigurationBuilder() .AddJsonFile("appsettings.json") // Optionally, if you have a JSON configuration file .Build(); // Configure Serilog from appsettings.json Log.Logger = new LoggerConfiguration() .ReadFrom.Configuration(appsettingsconfiguration) .MinimumLevel.Error() // Set the global minimum logging level to Error .Enrich.FromLogContext() .CreateLogger(); builder.Host.UseSerilog(); // Add services to the container. builder.Services.AddControllers(); builder.Services.AddAutoMapper(typeof(MappingConfig)); builder.Services.AddCors(); builder.Services.AddHttpContextAccessor(); builder.Services.RegisterRepositoryServices(); builder.Services.AddCors(options => { options.AddPolicy("AllowSpecificOrigin", builder => { builder.WithOrigins("http://localhost:4200") .AllowAnyMethod() .AllowAnyHeader().AllowCredentials(); }); }); var configuration = builder.Configuration; builder.Services.AddAuthentication(cfg => { cfg.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; cfg.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; cfg.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; }).AddJwtBearer(x => { x.RequireHttpsMetadata = false; x.SaveToken = false; x.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8 .GetBytes(configuration["ApplicationSettings:JWT_Secret"]) ), ValidateIssuer = false, ValidateAudience = false, ClockSkew = TimeSpan.Zero }; }); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(options => { options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { Description = "JWT Authorization header using the Bearer scheme. \r\n\r\n " + "Enter 'Bearer' [space] and then your token in the text input below.\r\n\r\n" + "Example: \"Bearer 12345abcdef\"", Name = "Authorization", In = ParameterLocation.Header, Scheme = "Bearer" }); options.AddSecurityRequirement(new OpenApiSecurityRequirement() { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" }, Scheme = "oauth2", Name = "Bearer", In = ParameterLocation.Header }, new List<string>() } }); options.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); // Define x-api-key authorization scheme options.AddSecurityDefinition("apiKey", new OpenApiSecurityScheme { Description = "Please enter your API key", Name = "x-api-key", In = ParameterLocation.Header, Type = SecuritySchemeType.ApiKey }); // Require API key for all endpoints options.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "apiKey" } }, Array.Empty<string>() } }); }); // Configure database context builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseNpgsql(builder.Configuration.GetConnectionString("ELMDB")) ); builder.Services.AddSignalR(); var app = builder.Build(); app.UseSerilogRequestLogging(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment() || app.Environment.IsProduction()) { app.UseSwagger(); app.UseSwaggerUI(); } // Configure CORS app.UseCors("AllowSpecificOrigin"); // Add custom middleware for API key validation app.UseMiddleware<ApiKeyMiddleware>(); // Enable HTTPS redirection app.UseHttpsRedirection(); app.UseAuthentication(); app.UseAuthorization(); app.UseWebSockets(); app.MapHub<NotificationHub>("/notificationHub"); // Map controllers app.MapControllers(); // Run the application app.Run();
排查与解决步骤
- 调整中间件顺序:当前
ApiKeyMiddleware在UseAuthentication之前执行,导致WebSocket握手请求还没完成JWT认证就被校验API Key,可能引发未授权问题。把UseMiddleware<ApiKeyMiddleware>移到UseAuthentication之后、UseAuthorization之前:app.UseAuthentication(); app.UseMiddleware<ApiKeyMiddleware>(); app.UseAuthorization(); - 添加SignalR的JWT事件支持:默认JWT Bearer认证不会处理WebSocket握手请求的Token,需要在AddJwtBearer中配置
OnMessageReceived事件,确保Hub路径的请求能正确获取Token:.AddJwtBearer(x => { // 原有配置... x.Events = new JwtBearerEvents { OnMessageReceived = context => { var accessToken = context.Request.Query["access_token"]; var path = context.HttpContext.Request.Path; if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/notificationHub")) { context.Token = accessToken; } return Task.CompletedTask; } }; }); - 显式开启SignalR的Credentials支持:在Angular端的SignalR配置中添加
withCredentials: true,确保请求携带认证信息:this.hubConnection = new signalR.HubConnectionBuilder() .withUrl("https://localhost:48348/notificationHub", { headers: { "x-api-key": environment.apiKey, Authorization: `Bearer ${token}`, }, withCredentials: true }) .build(); - 修正WebSocket中间件位置:把
UseWebSockets()移到UseCors之后、UseAuthentication之前,确保WebSocket请求先通过CORS校验:app.UseCors("AllowSpecificOrigin"); app.UseWebSockets(); app.UseAuthentication(); - 检查ApiKeyMiddleware的OPTIONS请求处理:WebSocket握手前会发送OPTIONS预检请求,该请求不需要校验API Key,要在中间件中放过:
public async Task InvokeAsync(HttpContext context, RequestDelegate next) { if (context.Request.Method.Equals("OPTIONS", StringComparison.OrdinalIgnoreCase)) { await next(context); return; } // 原有API Key校验逻辑... }
内容的提问来源于stack exchange,提问作者Sahil Jarsaniya
相关产品推荐
相关产品推荐

