SignalR позволяет использовать встроенные механизмы аутентификации и авторизации ASP.NET Core для разграничения доступа к хабам и их функциональности. В данном случае рассмотрим применение аутентификации на основе куки и авторизации по ролям в приложении с SignalR. Подробнее же про аутентификацию на основе куки и авторизацию ролей в asp.net core можно посмотреть в соответствующих статьях: Аутентификация с помощью куки и Авторизация по ролям
Возьмем простейший проект, где применяется простейшая аутентификация и авторизация на основе куки со следующей структурой:
Для тестирования аутентификации и авторизации в SignalR определим следующий класс хаб ChatHub:
using Microsoft.AspNetCore.Authorization; // для атрибута Authorize using Microsoft.AspNetCore.SignalR; namespace SignalRApp { [Authorize] public class ChatHub : Hub { public async Task Send(string message, string userName) { await Clients.All.SendAsync("Receive", message, userName); } [Authorize(Roles = "admin")] public async Task Notify(string message) { await Clients.All.SendAsync("Receive", message, "Администратор"); } } }
Для организации доступа к хабу и его методам можно применять атрибут AuthorizeAttribute. В данном случае функциональность хаба доступна только для аутентифицировнных пользователей, то есть тех, кто условно залогинился в приложении. Таким образом, анонимные пользователи не смогут вызвать методы хаба, даже если в их распоряжении будет код javascript, который обращается к этим методам.
Кроме того, для метода Notify указано еще одно ограничение - доступ только для тех пользователей, которые принадлежат роли "admin". Поэтому когда у хаба будет вызван данный метод, приложение будет знать, что метод вызван администратором. В остальном оба метода одинаковы вызывают на клиенте функцию "Receive".
Для определения клиентской части создадим в проекте новую папку, которую назовем html - она будет хранить файлы html. Прежде всего добавим в нее новый файл login.html, который будет содержать форму для входа в приложение:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <h2>Вход в приложение</h2> <form method="post"> <p> <label>Email</label><br /> <input name="email" /> </p> <p> <label>Password</label><br /> <input type="password" name="password" /> </p> <input type="submit" value="Login" /> </form> </body> </html>
По нажатию на кнопку введенные email и пароль будут отправляться обратно приложению в POST-запросе.
Также добавим в папку html новую страницу index.html, которая будет предназначена для обычных пользователей:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <h2>Личный кабинет пользователя</h2> <p><a href="logout">Выйти</a></p> <div id="userForm"> <p> Введите ник:<br /> <input id="username" type="text" /> </p> <p> Введите сообщение:<br /> <input type="text" id="message" /> </p> <input type="button" id="sendBtn" value="Отправить" /> </div> <div id="chatroom"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/6.0.1/signalr.js"></script> <script> const hubConnection = new signalR.HubConnectionBuilder() .withUrl("/chat") .build(); // отправка сообщения от простого пользователя document.getElementById("sendBtn").addEventListener("click", () => { const message = document.getElementById("message").value; const username = document.getElementById("username").value; hubConnection.invoke("Send", message, username) .catch(error => console.error(error)); }); // получение сообщения от сервера hubConnection.on("Receive", (message, userName) => { // создаем элемент <b> для имени пользователя const userNameElem = document.createElement("b"); userNameElem.textContent = `${userName}: `; // создает элемент <p> для сообщения пользователя const 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.start() .then(() => document.getElementById("sendBtn").disabled = false) .catch((err) => console.error(err)); </script> </body> </html>
По нажатию на кнопку на странице хабу будет отправлено сообщение с ником пользователя.
И также добавим в папку html новую страницу admin.html, которая будет предназначена для администратора:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <h2>Личный кабинет администратора</h2> <p><a href="logout">Выйти</a></p> <div id="userForm"> <p> Введите ник:<br /> <input id="username" type="text" /> </p> <p> Введите сообшение:<br /> <input type="text" id="message" /> </p> <input type="button" id="sendBtn" value="Отправить" /> </div> <br /> <div id="adminForm"> <input type="text" id="notify" /> <input type="button" id="notifyBtn" value="Уведомление" /> </div> <div id="chatroom"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/6.0.1/signalr.js"></script> <script> const hubConnection = new signalR.HubConnectionBuilder() .withUrl("/chat") .build(); // отправка сообщения от простого пользователя document.getElementById("sendBtn").addEventListener("click", () => { const message = document.getElementById("message").value; const username = document.getElementById("username").value; hubConnection.invoke("Send", message, username) .catch(error => console.error(error)); }); // отправка сообщения от администратора document.getElementById("notifyBtn").addEventListener("click", () => { const message = document.getElementById("notify").value; hubConnection.invoke("Notify", message); }); // получение сообщения от сервера hubConnection.on("Receive", (message, userName) => { // создаем элемент <b> для имени пользователя const userNameElem = document.createElement("b"); userNameElem.textContent = `${userName}: `; // создает элемент <p> для сообщения пользователя const 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.start() .then(() => document.getElementById("sendBtn").disabled = false) .catch((err) => console.error(err)); </script> </body> </html>
По сути тут тот же код, что и на странице index.html за тем исключением, что администратор также может отправить некоторое специальное уведомление, которое будет отправлено в хаб методу Notify.
Тепеь определим в файле Program.cs основную логику приложения:
using Microsoft.AspNetCore.Authentication.Cookies; using System.Security.Claims; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using SignalRApp; // пространство имен класса ChatHub var adminRole = new Role("admin"); var userRole = new Role("user"); var people = new List<Person> { new Person("tom@gmail.com", "12345", adminRole), new Person("bob@gmail.com", "55555", userRole), }; var builder = WebApplication.CreateBuilder(args); builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => options.LoginPath = "/login"); builder.Services.AddAuthorization(); builder.Services.AddSignalR(); var app = builder.Build(); app.UseAuthentication(); // добавление middleware аутентификации app.UseAuthorization(); // добавление middleware авторизации app.MapGet("/login", async context => await SendHtmlAsync(context, "html/login.html")); app.MapPost("/login", async (string? returnUrl, HttpContext context) => { // получаем из формы email и пароль var form = context.Request.Form; // если email и/или пароль не установлены, посылаем статусный код ошибки 400 if (!form.ContainsKey("email") || !form.ContainsKey("password")) return Results.BadRequest("Email и/или пароль не установлены"); string email = form["email"]; string password = form["password"]; // находим пользователя Person? person = people.FirstOrDefault(p => p.Email == email && p.Password == password); // если пользователь не найден, отправляем статусный код 401 if (person is null) return Results.Unauthorized(); var claims = new List<Claim> { new Claim(ClaimsIdentity.DefaultNameClaimType, person.Email), new Claim(ClaimsIdentity.DefaultRoleClaimType, person.Role.Name) }; var claimsIdentity = new ClaimsIdentity(claims, "Cookies"); var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); await context.SignInAsync(claimsPrincipal); return Results.Redirect(returnUrl ?? "/"); }); app.MapGet("/", [Authorize] async (HttpContext context) => await SendHtmlAsync(context, "html/index.html")); app.MapGet("/admin", [Authorize(Roles = "admin")] async (HttpContext context) => await SendHtmlAsync(context, "html/admin.html")); app.MapGet("/logout", async (HttpContext context) => { await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); return Results.Redirect("/login"); }); app.MapHub<ChatHub>("/chat"); app.Run(); async Task SendHtmlAsync(HttpContext context, string path) { context.Response.ContentType = "text/html; charset=utf-8"; await context.Response.SendFileAsync(path); } record class Person(string Email, string Password, Role Role); record class Role(string Name);
Для упрощения примера здесь учетные данные пользователей определены и хранятся непосредственно в коде. Пользователь представлен классом Person, а роль пользователя - классом Role.
При обращении по адресу "/login" сработает конечная точка app.MapGet("/login")
, который отправит пользователю страницу login.html для ввода логина и пароля. После отправки этих данных
их получит конечная точка app.MapPost("/login")
, который, в случае если пользователь найден, установит аутентификацирнные куки.
Конечная точка app.MapGet("/")
возвращает пользователям страницу index.html. Если же пользователь имеет роль администратора, то ему также будет доступен функционал
конечной точки app.MapGet("/admin")
, которая отправит страницу для администратора.
Пример работы хаба: