Аутентификация и авторизация на основе куки

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

SignalR позволяет использовать встроенные механизмы аутентификации и авторизации ASP.NET Core для разграничения доступа к хабам и их функциональности. В данном случае рассмотрим применение аутентификации на основе куки и авторизации по ролям в приложении с SignalR. Подробнее же про аутентификацию на основе куки и авторизацию ролей в asp.net core можно посмотреть в соответствующих статьях: Аутентификация с помощью куки и Авторизация по ролям

Возьмем простейший проект, где применяется простейшая аутентификация и авторизация на основе куки со следующей структурой:

Авторизация по ролям и аутентификация на основе куки в SignalR в ASP.NET Core и C#

Определение хаба SignalR

Для тестирования аутентификации и авторизации в 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"), которая отправит страницу для администратора.

Пример работы хаба:

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