Данное руководство устарело. Актуальное руководство: Руководство по ASP.NET Core 7
Пользователь в SignalR представляет по умолчанию объект ClaimsPrincipal. По сути это аутентифицированный пользователь,
который осуществил вход в приложение. И посредством аутентификационного тикета в куки или jwt-токена хаб может узнать, что это за
пользователь. Используя идентификатор ClaimTypes.NameIdentifier
из ClaimsPrincipal, можно отправлять сообщение определенным пользователям.
Концепция пользователя и концепция клиента(подключения) в SignalR отличаются. Пользователь - это учетная запись, под которой осуществлен вход в приложение. Можно одновременно выполнить вход в приложение сразу на нескольких устройствах, допустим, на ПК в нескольких браузерах и на мобильном устройстве. И если отправить этому пользователю сообщение, то этот пользователь увидит отправленное сообщение во всех браузерах и устройствах, в которых он зашел в приложение.
Подключение, в отличие от пользователя, создается даже на каждую отдельную вкладку браузера. Например, если в одном браузере открыть две вкладки и на обеих вкладках подключиться к хабу, то на каждой вкладке будут разные подключения с разными идентификаторами.
Если пользователь авторизован, то внутри хаба мы можем получить этого пользователя через свойство Context.User:
public class ChatHub : Hub { [Authorize] public async Task Send(string message, string to) { var user = Context.User; var userName = user.Identity.Name; // получаем роль var userRole = user.FindFirst(ClaimTypes.Role)?.Value; // принадлежит ли пользователь роли "admins" var isAdmin = user.IsInRole("admin"); //.......... } }
Это свойство представляет объект ClaimsPrincipal, поэтому мы можем также, как и в представлениях или контроллерах, получить из него формальное
имя пользователя (user.Identity.Name
), получить роль или значения других клеймов, если они сохранены в аутентификационных куках или токене, узнать, принадлежит ли
пользователь определенной роли, а также получить другую сопутствующую информацию.
Но важно учитывать, что некоторые пользователи, которые обращаются к хабу, могут быть аутентифицированы, некоторые могут быть анонимными. Поэтому если мы предоставляем доступ вообще всем пользователям, то при обращении к функционала в Context.User необходимо проверять этот объект на null. Либо применять атрибут авторизации, который исключает обращения анонимных пользователей.
В хабе мы можем не только получить текущего пользователя, но и отправить сообщения определенным пользователям. Для этого можно использовать ряд методов:
Clients.User(string userId): вызывает метод у пользователя по id
Clients.Users(IReadOnlyList<string> userIds): вызывает метод у пользователей, id которых передаются в метод
В оба метода передается некий id пользователя. Что это за id? В реальности мы сами можем определить, что будет использоваться в качестве id. Для этого добавим в проект новый класс CustomUserIdProvider:
using Microsoft.AspNetCore.SignalR; using System.Security.Claims; namespace AuthSignalRApp { public class CustomUserIdProvider : IUserIdProvider { public virtual string GetUserId(HubConnectionContext connection) { return connection.User?.Identity.Name; // или так //return connection.User?.FindFirst(ClaimTypes.Name)?.Value; } } }
Данный класс реализует интерфейс IUserIdProvider, который определяет метод GetUserId(). Этот метод как раз и возвращает id пользователя.
В данном случае он возвращает значение User.Identity.Name
. Здесь connection.User
- это текущий пользователь. Здесь
мы можем возвратить любое другое значение, например, роль пользователя, все зависит от конкретного приложения.
Далее этот метод необходимо добавить в сервисы в методе ConfigureServices класса Startup:
public void ConfigureServices(IServiceCollection services) { services.AddSingleton<IUserIdProvider, CustomUserIdProvider>(); // остальное содержимое }
Определим следующий класс хаба:
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; using System.Threading.Tasks; namespace AuthSignalRApp { [Authorize] public class ChatHub : Hub { public async Task Send(string message, string to) { var userName = Context.User.Identity.Name; if(Context.UserIdentifier!=to) // если получатель и текущий пользователь не совпадают await Clients.User(Context.UserIdentifier).SendAsync("Receive", message, userName); await Clients.User(to).SendAsync("Receive", message, userName); } public override async Task OnConnectedAsync() { await Clients.All.SendAsync("Notify", $"Приветствуем {Context.UserIdentifier}"); await base.OnConnectedAsync(); } } }
При подключение пользователя у всех подключененных клиентов будет вызываться функция Notify, которой передается сообщение общего характера.
В методе Send получаем сообщение и идентификатор пользователя, которому предназначено это сообщение. В самом методе получаем идентификатор текущего пользователя.
Поскольку в данном случае идентификатор представляет свойство User.Identity.Name, то здесь не будет разницы, как получить идентификатор текущего пользователя:
или так Context.User.Identity.Name
или так Context.UserIdentifier
. Далее сообщение отправляется
получателю, которому предназначено сообщение, и текущему пользователю, которой собственно и отправляет сообщение.
Для тестирования определим следующую веб-страницу:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>SignalR Chat</title> </head> <body> <div id="loginBlock"> Введите логин:<br /> <input id="userName" type="text" /> <input id="userPassword" type="text" /> <input id="loginBtn" type="button" value="Войти" /> </div><br /> <div id="inputForm"> <input type="text" id="message" placeholder="Введите сообщение" /> <input type="text" id="receiver" placeholder="Введите получателя" /> <input type="button" id="sendBtn" disabled value="Отправить" /> </div> <div id="chatroom"></div> <script src="https://unpkg.com/@microsoft/signalr@3.1.0/dist/browser/signalr.min.js"></script> <script> let token; const hubConnection = new signalR.HubConnectionBuilder() .withUrl("/chat", { accessTokenFactory: () => token}) .build(); hubConnection.on("Receive", function (message, userName) { // создаем элемент <b> для имени пользователя let userNameElem = document.createElement("b"); userNameElem.appendChild(document.createTextNode(userName + ": ")); // создает элемент <p> для сообщения пользователя let elem = document.createElement("p"); elem.appendChild(userNameElem); elem.appendChild(document.createTextNode(message)); var firstElem = document.getElementById("chatroom").firstChild; document.getElementById("chatroom").insertBefore(elem, firstElem); }); hubConnection.on("Notify", function (message) { // создает элемент <p> для сообщения пользователя let elem = document.createElement("p"); elem.appendChild(document.createTextNode(message)); var firstElem = document.getElementById("chatroom").firstChild; document.getElementById("chatroom").insertBefore(elem, firstElem); }); // аутентификация document.getElementById("loginBtn").addEventListener("click", function (e) { var request = new XMLHttpRequest(); request.open("POST", "/token", true); request.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); request.addEventListener("load", function () { if (request.status < 400) { // если запрос успешный let data = JSON.parse(request.response); token = data.access_token; document.getElementById("sendBtn").disabled = false; hubConnection.start() // начинаем соединение с хабом .catch(err => { console.error(err.toString()); document.getElementById("loginBtn").disabled = true; document.getElementById("sendBtn").disabled = true; }); } else { console.log("Status", request.status); console.log(request.responseText); } }); // отправляем запрос на аутентификацию request.send("username=" + document.getElementById("userName").value + "&password=" + document.getElementById("userPassword").value); }); // отправка сообщения на сервер document.getElementById("sendBtn").addEventListener("click", function (e) { let message = document.getElementById("message").value; let to = document.getElementById("receiver").value; hubConnection.invoke("Send", message, to); }); </script> </body> </html>
На веб-странице вначале осуществляем вход в приложение, как было описано в прошлой статье, затем отправляем сообщение определенному пользователю.
Таким образом, пользователи смогут отправлять сообщения только определенным пользователям: