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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 07:17:05