Как исправить проблемы аутентификации JWT в SignalR WebSocket
Узнайте, как устранить сбои соединения SignalR WebSocket при использовании аутентификации JWT: настройка CORS, передача токенов в строке запроса и логика повторного подключения, включая проверку параметров валидации токенов и включение sticky‑sessions для масштабируемых серверных конфигураций.
Как устранить сбой подключения SignalR WebSocket с использованием JWT‑аутентификации?
При попытке подключиться к SignalR‑хабу с JWT‑аутентификацией я получаю ошибку WebSocket:
[2025-11-19T14:03:33.645Z] Error: Failed to start the transport 'WebSockets': Error: WebSocket failed to connect. The connection could not be found on the server, either the endpoint may not be a SignalR endpoint, the connection ID is not present on the server, or there is a proxy blocking WebSockets. If you have multiple servers check that sticky sessions are enabled.
Конфигурация клиента выглядит так:
this.hubConnection = new signalR.HubConnectionBuilder()
.withUrl(
`${environment.serviceUrl}/hubs/notifications?userId=${userId}&applicationId=${applicationId}`,
{
accessTokenFactory: () => {
return this.authService.idToken;
},
transport: signalR.HttpTransportType.WebSockets
}
)
.withAutomaticReconnect()
.configureLogging(signalR.LogLevel.Debug)
.build();
this.hubConnection
.start()
.then(() => console.log('[SignalR] Connected'))
.catch(err => console.error('[SignalR] Connection error:', err));
На сервере уже настроен CORS:
services.AddCors(o => o.AddPolicy("AllowCors", builder =>
{
builder.SetIsOriginAllowed(origin => true)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
}));
Подключение работает без аутентификации. Что может вызывать эту проблему и как её решить?
SignalR WebSocket‑соединения с аутентификацией JWT обычно падают из‑за некорректной серверной конфигурации JWT, которая не обрабатывает токены из строки запроса для WebSocket‑соединений, проблем с CORS при работе с учетными данными или отсутствия логики повторного подключения. Основное решение состоит в настройке JWT‑а так, чтобы он считывал токены из строки запроса во время WebSocket‑соединений, и в корректной настройке CORS, поддерживающей учетные данные.
Содержание
- Понимание проблемы
- Серверная конфигурация JWT
- Проблемы с CORS
- Проверка клиента
- Расширенные решения
- Частые сценарии ошибок
Понимание проблемы
Сообщение об ошибке указывает, что WebSocket‑соединение падает во время процесса аутентификации. Ключевой момент — SignalR с WebSocket использует иной поток аутентификации по сравнению с обычными HTTP‑запросами. При использовании WebSocket SignalR автоматически помещает JWT‑токен в строку запроса (?access_token=YOUR_TOKEN), а не в заголовки, потому что браузерный API WebSocket не поддерживает пользовательские заголовки.
Это приводит к несовпадению: серверная часть JWT‑а может быть настроена на поиск токенов в заголовках (для обычных HTTP‑запросов), но не в строке запроса (для WebSocket‑соединений). Соединение работает без аутентификации, потому что middleware аутентификации не вмешивается в установку соединения.
Серверная конфигурация JWT
Самый критичный исправление — настроить JWT‑а так, чтобы он обрабатывал токены из строки запроса для WebSocket‑соединений. Согласно документации Microsoft по аутентификации SignalR, нужно подключить событие OnMessageReceived:
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
// Ваши существующие параметры проверки
};
// Это ключевая часть для SignalR с WebSockets
x.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
// Если запрос относится к нашему хабу...
if (!string.IsNullOrEmpty(accessToken))
{
// Читаем токен из строки запроса
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
Эта конфигурация гарантирует, что когда SignalR отправляет JWT‑токен в строке запроса во время WebSocket‑соединения, middleware аутентификации сможет корректно прочитать и проверить его.
Проблемы с CORS
Текущая конфигурация CORS имеет критическую ошибку: SetIsOriginAllowed(origin => true) может вызвать проблемы с учетными данными. Согласно исследованиям из документации Microsoft, приложения должны быть настроены так, чтобы проверять эти заголовки и гарантировать, что только WebSocket‑соединения из ожидаемых источников разрешены.
Обновите конфигурацию CORS, чтобы использовать конкретные источники вместо подстановочных знаков:
services.AddCors(o => o.AddPolicy("AllowCors", builder =>
{
// Замените на ваш реальный фронтенд‑домен
builder.WithOrigins("https://your-frontend-domain.com")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
}));
Исследование из GitHub issue #4457 конкретно упоминает, что «чтобы не получать ошибку CORS, вместо возвращения wildcard ‘*’ сервер должен возвращать разрешённый origin как URL, поскольку это не работает с учетными данными, которые нужны для SignalR».
Важное замечание по безопасности
Обратите внимание, что передача JWT‑токенов в строке запроса имеет последствия для безопасности. Токены могут быть залогированы серверами, прокси и браузерами. Рассмотрите возможность использования токенов с коротким сроком действия и механизма обновления токенов.
Проверка клиента
Конфигурация клиента выглядит в целом корректной, но есть несколько оптимизаций:
1. Добавьте логику повторного подключения при ошибках 401
Согласно руководству по устранению неполадок Microsoft, стоит добавить логику повторного подключения для обработки ошибок 401:
this.hubConnection.onclose(error => {
if (error && error.message.includes("401")) {
console.log('[SignalR] 401 error detected, attempting to reconnect...');
// Возможно, захотите обновить токен здесь
this.authService.refreshToken().then(() => {
// Повторить подключение с новым токеном
this.hubConnection.start();
});
}
});
2. Рассмотрите возможность пропуска этапа переговоров
В некоторых случаях можно пропустить фазу переговоров, чтобы упростить соединение:
this.hubConnection = new signalR.HubConnectionBuilder()
.withUrl(
`${environment.serviceUrl}/hubs/notifications?userId=${userId}&applicationId=${applicationId}`,
{
accessTokenFactory: () => this.authService.idToken,
transport: signalR.HttpTransportType.WebSockets,
skipNegotiation: true // Добавьте эту опцию
}
)
.withAutomaticReconnect()
.configureLogging(signalR.LogLevel.Debug)
.build();
Однако это требует, чтобы ваш сервер поддерживал WebSocket напрямую без переговоров.
Расширенные решения
1. Конфигурация нескольких серверов
Если вы используете несколько серверов, убедитесь, что sticky sessions включены, как указано в сообщении об ошибке. Это можно настроить в настройках балансировщика нагрузки:
// В конфигурации стартапа
services.AddSignalR()
.AddAzureSignalR(options =>
{
// Конфигурация Azure SignalR
options.ServerStickyMode = Microsoft.Azure.SignalR.ServerStickyMode.Required;
});
2. Проблемы с настройкой Authority
Исследования из Stack Overflow показывают, что иногда настройки Authority JWT могут вызывать проблемы. Если у вас есть x.Authority = "Authority URL" в конфигурации JWT, попробуйте отключить его или убедиться, что URL правильный.
3. Параметры проверки токена
Проверьте параметры проверки токена, чтобы убедиться, что они соответствуют издателю токена:
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = true, // Должен совпадать с издателем токена
ValidateAudience = true, // Должен совпадать с аудиторией токена
ValidIssuer = "your-issuer",
ValidAudience = "your-audience",
ClockSkew = TimeSpan.Zero
};
Частые сценарии ошибок
1. 401 Unauthorized во время рукопожатия WebSocket
Симптом: Соединение работает без аутентификации, но падает с JWT
Решение: Убедитесь, что событие OnMessageReceived корректно настроено для чтения из строки запроса
2. Политика CORS блокирует соединение
Симптом: Соединение падает с ошибками, связанными с CORS
Решение: Используйте конкретные источники вместо wildcard и убедитесь, что AllowCredentials() включен
3. Токен не отправляется в строке запроса
Симптом: Аутентификация падает, но токен не виден в URL соединения
Решение: Проверьте, что accessTokenFactory возвращает валидный токен, и посмотрите вкладку Network в браузере
4. Проблемы с несколькими серверами
Симптом: Перебрасывающиеся ошибки соединения
Решение: Включите sticky sessions в вашем балансировщике нагрузки
Заключение
Проблемы с WebSocket‑соединениями SignalR и аутентификацией JWT обычно вызваны настройками сервера, а не клиентом. Ключевые шаги по решению:
- Настройте JWT‑а на чтение токенов из строки запроса через обработчик
OnMessageReceived. - Исправьте конфигурацию CORS, используя конкретные источники вместо wildcard при работе с учетными данными.
- Добавьте логику повторного подключения для автоматического обновления токенов и обработки ошибок 401.
- Проверьте параметры проверки токена, чтобы они соответствовали издателю и аудитории.
- Рассмотрите sticky sessions, если используете несколько серверов за балансировщиком нагрузки.
Пошагово устраняя эти области, вы сможете установить безопасные WebSocket‑соединения с JWT‑аутентификацией в SignalR. Если проблемы сохраняются, проверьте сетевые запросы в браузере, чтобы точно определить, где происходит сбой аутентификации, и просмотрите журналы сервера для подробной информации об ошибках.
Источники
- Authentication and authorization in ASP.NET Core SignalR - Microsoft Learn
- Security considerations in ASP.NET Core SignalR - Microsoft Learn
- Troubleshooting guide for Azure SignalR Service - Microsoft Learn
- JWT Authentication Failed with SignalR - Stack Overflow
- SignalR JWT authentication fails - Stack Overflow
- ASP.NET Core SignalR Authentication and Authorization 401 - GitHub Issue
- Credential CORS issue - GitHub Issue
- Securing Angular SignalR client with JWT - Damien Bod