Пользователи

Данное руководство устарело. Актуальное руководство: Руководство по ASP.NET Core 7

Последнее обновление: 28.12.2019

Пользователь в 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>

На веб-странице вначале осуществляем вход в приложение, как было описано в прошлой статье, затем отправляем сообщение определенному пользователю.

Таким образом, пользователи смогут отправлять сообщения только определенным пользователям:

Управление пользователями в SignalR в ASP.NET Core
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850