У меня есть две службы aspnet.core. Один для IdentityServer 4 и один для API, используемого клиентами Angular4 +. Концентратор SignalR работает на API. Все решение работает на докере, но это не имеет значения (см. Ниже).
Я использую неявный поток аутентификации, который работает безупречно. Приложение NG перенаправляется на страницу входа в IdentityServer, где пользователь входит в систему. После этого браузер перенаправляется обратно в приложение NG с токеном доступа. Затем токен используется для вызова API и установления связи с SignalR. Думаю, я прочитал все, что доступно (см. Источники ниже).
Поскольку SignalR использует веб-сокеты, которые не поддерживают заголовки, токен следует отправлять в строке запроса. Затем на стороне API токен извлекается и устанавливается для запроса так же, как и в заголовке. Затем токен проверяется, и пользователь авторизуется.
API работает без проблем, пользователи авторизуются, а заявки могут быть получены на стороне API. Так что проблем с IdentityServer быть не должно, поскольку SignalR не требует специальной настройки. Я прав?
Когда я не использую атрибут [Authorized] на концентраторе SignalR, квитирование преуспевает. Вот почему я думаю, что в используемой мной инфраструктуре докеров и обратном прокси-сервере нет ничего плохого (прокси настроен на включение веб-сокетов).
Итак, без авторизации SignalR работает. При авторизации клиент NG во время рукопожатия получает следующий ответ:
Failed to load resource: the server responded with a status of 401
Error: Failed to complete negotiation with the server: Error
Error: Failed to start the connection: Error
Запрос
Request URL: https://publicapi.localhost/context/negotiate?signalr_token=eyJhbGciOiJSUz... (token is truncated for simplicity)
Request Method: POST
Status Code: 401
Remote Address: 127.0.0.1:443
Referrer Policy: no-referrer-when-downgrade
В ответ я получаю:
access-control-allow-credentials: true
access-control-allow-origin: http://localhost:4200
content-length: 0
date: Fri, 01 Jun 2018 09:00:41 GMT
server: nginx/1.13.10
status: 401
vary: Origin
www-authenticate: Bearer
Согласно журналам, токен успешно проверен. Я могу включить полные журналы, но подозреваю, в чем проблема. Я включу эту часть сюда:
[09:00:41:0561 Debug] Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler AuthenticationScheme: Identity.Application was not authenticated.
[09:00:41:0564 Debug] Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler AuthenticationScheme: Identity.Application was not authenticated.
Я получаю их в файле журнала и не понимаю, что это значит. Я включаю часть кода в API, где я получаю и извлекаю токен вместе с конфигурацией аутентификации.
services.AddAuthentication(options =>
{
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultForbidScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultSignInScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultSignOutScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddIdentityServerAuthentication(options =>
{
options.Authority = "http://identitysrv";
options.RequireHttpsMetadata = false;
options.ApiName = "publicAPI";
options.JwtBearerEvents.OnMessageReceived = context =>
{
if (context.Request.Query.TryGetValue("signalr_token", out StringValues token))
{
context.Options.Authority = "http://identitysrv";
context.Options.Audience = "publicAPI";
context.Token = token;
context.Options.Validate();
}
return Task.CompletedTask;
};
});
Других ошибок, исключений в системе нет. Я могу отлаживать приложение, и вроде все в порядке.
Что означают включенные строки журнала? Как я могу отладить происходящее во время авторизации?
Обновлено: Я чуть не забыл упомянуть, что думал, что проблема связана со схемами аутентификации, поэтому я установил для каждой схемы ту, которая, по моему мнению, была необходима. Однако, к сожалению, это не помогло.
Я здесь немного невежественен, поэтому ценю любое предложение. Спасибо.
Источники информации:
Передать токен аутентификации в SignalR
Защита SignalR с помощью IdentityServer
Microsoft docs по авторизации SignalR





Я должен ответить на свой вопрос, потому что у меня был крайний срок, и, как ни странно, мне удалось его решить. Поэтому я записываю его в надежде, что он кому-то поможет в будущем.
Сначала мне нужно было понять, что происходит, поэтому я заменил весь механизм авторизации на свой собственный. Я мог сделать это с помощью этого кода. Это не требуется для решения, однако, если кому-то это нужно, это способ сделать.
services.Configure<AuthenticationOptions>(options =>
{
var scheme = options.Schemes.SingleOrDefault(s => s.Name == JwtBearerDefaults.AuthenticationScheme);
scheme.HandlerType = typeof(CustomAuthenticationHandler);
});
С помощью IdentityServerAuthenticationHandler и переопределения всех возможных методов я наконец понял, что событие OnMessageRecced выполняется после проверки токена. Поэтому, если бы во время вызова HandleAuthenticateAsync не было токена, ответ был бы 401. Это помогло мне выяснить, где разместить свой собственный код.
Мне нужно было реализовать свой собственный «протокол» во время получения токена. Я заменил этот механизм.
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddIdentityServerAuthentication(JwtBearerDefaults.AuthenticationScheme,
options =>
{
options.Authority = "http://identitysrv";
options.TokenRetriever = CustomTokenRetriever.FromHeaderAndQueryString;
options.RequireHttpsMetadata = false;
options.ApiName = "publicAPI";
});
Важной частью является свойство TokenRetriever и то, что его заменяет.
public class CustomTokenRetriever
{
internal const string TokenItemsKey = "idsrv4:tokenvalidation:token";
// custom token key change it to the one you use for sending the access_token to the server
// during websocket handshake
internal const string SignalRTokenKey = "signalr_token";
static Func<HttpRequest, string> AuthHeaderTokenRetriever { get; set; }
static Func<HttpRequest, string> QueryStringTokenRetriever { get; set; }
static CustomTokenRetriever()
{
AuthHeaderTokenRetriever = TokenRetrieval.FromAuthorizationHeader();
QueryStringTokenRetriever = TokenRetrieval.FromQueryString();
}
public static string FromHeaderAndQueryString(HttpRequest request)
{
var token = AuthHeaderTokenRetriever(request);
if (string.IsNullOrEmpty(token))
{
token = QueryStringTokenRetriever(request);
}
if (string.IsNullOrEmpty(token))
{
token = request.HttpContext.Items[TokenItemsKey] as string;
}
if (string.IsNullOrEmpty(token) && request.Query.TryGetValue(SignalRTokenKey, out StringValues extract))
{
token = extract.ToString();
}
return token;
}
И это мой собственный алгоритм извлечения токенов, который сначала пробует стандартный заголовок и строку запроса для поддержки распространенных ситуаций, таких как вызовы веб-API. Но если токен все еще пуст, он пытается получить его из строки запроса, куда клиент поместил его во время рукопожатия веб-сокета.
Обновлено: я использую следующий код на стороне клиента (TypeScript), чтобы предоставить токен для рукопожатия SignalR
import { HubConnection, HubConnectionBuilder, HubConnectionState } from '@aspnet/signalr';
// ...
const url = `${apiUrl}/${hubPath}?signalr_token=${accessToken}`;
const hubConnection = new HubConnectionBuilder().withUrl(url).build();
await hubConnection.start();
Где apiUrl, hubPath и accessToken - обязательные параметры соединения.
@ChrisCa: для этой цели мы используем HttpInterceptor (TS, Angular4 +). Я могу поделиться кодом, если хотите, однако мы используем rxjs и redux, поэтому процесс распределяется между разными компонентами. Но вы можете найти пример реализации почти для каждого технологического стека.
Привет спасибо. Теперь я понимаю, что вы передаете токен аутентификации в строке запроса, верно? У меня это уже работает. Я думал, вы придумали способ использовать заголовок авторизации вместо строки запроса, но я не думаю, что вы это делаете?
Нет, к сожалению, AFAIK это невозможно в моем случае. Но технология позволяет это (см .: developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/…). Итак, если вы можете изменить заголовок для веб-сокета, это должно работать с IdentityServer4.
да, похоже, браузеры не позволяют устанавливать заголовок, хотя вы можете это сделать с других типов клиентов. к сожалению, я нахожусь в браузере, поэтому мне нужно использовать строку запроса - хотя еще раз спасибо
Большое спасибо, Даниэль :) Я преследовал ту же проблему, пока не увидел твое решение !!!
@DanielLeiszen есть ли проблемы с безопасностью, связанные с передачей токена через запрос с использованием веб-сокетов?
@AntonToshik AFAIK, строка запроса так же видна, как и заголовок запроса, так что это скорее решение протокола, чем решение безопасности. используя HTTPS, строка запроса достаточно безопасна, я думаю
@DanielLeiszen Как выглядит код подключения на стороне клиента сигнализации?
Спасибо, я видел много примеров, это единственное, что у меня сработало.
Спасибо, Даниэль :-) Он работает с asp core 3.1 и IdentityServer4. Отличный вопрос и ответ.
Позвольте мне положить на это свои два цента. Я думаю, что большинство из нас хранят токены в файлах cookie, и во время рукопожатия WebSockets они также отправляются на сервер, поэтому я предлагаю использовать извлечение токенов из cookie.
Для этого добавьте ниже последний оператор if:
if (string.IsNullOrEmpty(token) && request.Cookies.TryGetValue(SignalRCookieTokenKey, out string cookieToken))
{
token = cookieToken;
}
На самом деле мы можем вообще удалить извлечение из строки запроса, поскольку, согласно Документы Microsoft, это не совсем безопасно и может быть где-то записано.
Спасибо за обновления. В документе, на который вы ссылались, говорится: «При использовании WebSockets или событий, отправленных сервером, клиент браузера отправляет токен доступа в строке запроса. Получение токена доступа через строку запроса обычно так же безопасно, как и использование стандартного заголовка авторизации. Вы всегда должны использовать HTTPS. для обеспечения безопасного сквозного соединения между клиентом и сервером ». Об этом я тоже сказал в комментарии. При использовании аутентификации токена-носителя файлы cookie не используются.
@DanielLeiszen да, но вы почти всегда храните свой токен на предъявителя в cookie, поэтому мы можем использовать cookie для получения токена при рукопожатии. Это обычная практика.
вы имеете в виду, что сервер отправляет токен доступа / обновления, который затем сохраняется в файле cookie на стороне клиента и автоматически отправляется обратно во время запросов как часть пакета? Мне не известно о таком поведении, но я был бы рад увидеть несколько примеров. как создается cookie?
@DanielLeiszen Например, у вас есть IdentityServer4 и клиентский SPA. Вы аутентифицируетесь на ID4 и перенаправляетесь на клиентский SPA с помощью access_token. (Я рекомендую использовать поток кода авторизации в качестве последнего предложения OAuth 2.0 для SPA) Затем в SPA вы сохраняете свой токен в файле cookie. И после этого пытаешься подключиться к signalR. Cookie будет автоматически отправляться на сервер каждый раз во время рукопожатия, а затем вы перехватываете их с помощью класса поиска и получаете токен. После этого у вас будет токен так же, как через строку запроса, но чуть более красивым способом.
@DanielLeiszen Одна вещь, на которую следует обратить внимание - cookie всегда автоматически отправляет на сервер во время HTTP-запроса, вот как они работают. Для этого не нужно делать что-то дополнительно. Но вы, наверное, это уже знаете :)
Файлы cookie сначала отправляются С сервера. Если сервер не настроен на отправку файлов cookie, отправлять обратно нечего. Использование только токенов на предъявителя не требует файлов cookie, поэтому отправлять обратно нечего.
@DanielLeiszen изначально вы получаете токен не из cookie, который был получен с сервера, вы получаете токен из ответа конечной точки токена в формате json, и только после этого вы помещаете токен в cookie и сможете отправить их на сервер.
Благодарю за разъяснение. Таким образом, cookie создается на стороне клиента и отправляется на сервер. Таким образом, в отличие от запроса со строкой aproach, файлы cookie могут не регистрироваться переходами во время связи. Однако без HTTPS это так же уязвимо, поскольку содержит текстовое значение токена доступа. На HTTPS этот подход безопаснее, чем строка запроса. Я прав?
@DanielLeiszen точно :) Я надеюсь, что в наши дни никто не рассматривает возможность использования HTTP без S, так что я думаю, это можно считать правильным подходом.
Я знаю, что это старая ветка, но на случай, если кто-то наткнется на это, как я. Нашел альтернативное решение.
TL; DR: JwtBearerEvents.OnMessageReceived перехватит токен перед его проверкой при использовании, как показано ниже:
public void ConfigureServices(IServiceCollection services)
{
// Code removed for brevity
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = "https://myauthority.io";
options.ApiName = "MyApi";
options.JwtBearerEvents = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
// If the request is for our hub...
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/hubs/myhubname")))
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
}
Этот документ Microsoft подсказал мне: https://docs.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-3.1. Однако в примере Microsoft вызывается options.Events, поскольку это не пример использования IdentityServerAuthentication. Если options.JwtBearerEvents используется так же, как options.Events в примере Microsoft, IdentityServer4 счастлив!
Это должен быть правильный ответ. Мне также нужно было добавить атрибут Authorize в SignalR Hub.
В то время, когда я отвечал на этот вопрос, лучшего способа решить эту проблему не было. Тем временем, благодаря этому сообщению и проблеме с github, MS удалось решить реальную проблему, стоящую за этой проблемой. Я не знаю, какова фактическая политика, позволяющая обновлять ответ без изменения репутации. Какие-либо предложения?
Кроме того, в вопросе указывается фактическая версия SignalR, которая является версией, доступной в то время. Я думаю, что SignalR 2.0 принес много решений в этой области.
Я понимаю, о чем ты говоришь, Даниэль. Это был мой первый пост здесь, и я не пытался повлиять на вашу репутацию. Я просто пытался поделиться новым решением. Может быть, я мог бы предложить отредактировать ваш ответ, используя это как другое возможное решение, чтобы ваша репутация не пострадала?
@AlexGemma: Я тоже не знаю, какой протокол должен быть в этой ситуации. Достоверность информации во времени всегда была слабым местом Интернета. И я не вижу никаких особенностей в SO. Я не беспокоюсь о своей репутации, я думаю, что голосование за ответ может помочь. Как я вижу из комментариев, в некоторых ситуациях этот ответ не работает даже с aspnet.core 3.1, но мой исходный ответ все еще работает.
Я не вижу примера, чтобы этот ответ не работал в ASP.NET Core 3.1. В вашем вопросе ваш исходный код на самом деле был очень близок к этому ответу. Основное различие, которое я вижу, заключается в следующем: options.JwtBearerEvents = new JwtBearerEvents {OnMessagedReceived ... vs options.JwtBearerEvents.OnMessageReceived ... Я предлагаю отредактировать, чтобы включить этот параметр в выбранный ответ, но в том случае, если он не принимается, этот ответ все еще может быть здесь, как еще один вариант. Я ценю сотрудничество и голосование "за"!
fyi, не передавайте префикс Bearer с токеном
@AlexGemma какую версию сигнализатора вы используете? Я использую AspNetCore.SignalR.Core 1.1.0, но у меня это все еще не сработало. Я должен был следовать тому пути, который был предложен в этом посте. stackoverflow.com/a/55926126/6012566
@Amir Это ASP.NET Core 3.1. Да, вам также понадобится UseAuthentication ().
v. интересно. У меня аналогичная проблема. как вы добавили собственный заголовок в клиентский JS (или TS)? вы вносили изменения в пакет SignalR npm? Я не вижу ничего в api для передачи заголовка - спасибо