Аутентификация и авторизация с помощью токенов

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

В прошлой статье рассматривалась аутентификация и авторизация на основе кук. Другим расспространенным способом аутентификации и авторизации представляет использование токенов. Рассмотрим, как использовать токены в приложении SignalR.

Подробнее про аутентификацию и авторизацию токенов в asp.net можно посмотреть в соответствующих статьях: Аутентификация с помощью JWT-токенов и Авторизация с помощью JWT-токенов в клиенте JavaScript. В данном же случае рассмотрим непосредственное взавимодействие SignalR и механизма аутентификацию с помощью токенов. И для этого возьмем следующий проект:

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

Прежде всего для использования JWT-токенов в проект ASP.NET Core необходимо добавить Nuget-пакет Microsoft.AspNetCore.Authentication.JwtBearer.

аутентфикация и авторизация с помощью jwt-токенов в SignalR в приложении на ASP.NET и C#

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

Пусть у нас будет следующий хаб 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, то доступ к хабу установлен только для аутентифицировнных пользователей. В самом хабе метод Send получает сообщение и имя отправившего его пользователя и ретранслирует его всем подключенным клиентам.

Определение основной части

В файле Program.cs определим основную логику приложения:

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using SignalRApp;   // пространство имен класса ChatHub

// условная бд с пользователями
var people = new List<Person>
 {
    new Person("tom@gmail.com", "12345"),
    new Person("bob@gmail.com", "55555")
};

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthorization();
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = AuthOptions.ISSUER,
            ValidateAudience = true,
            ValidAudience = AuthOptions.AUDIENCE,
            ValidateLifetime = true,
            IssuerSigningKey = AuthOptions.GetSymmetricSecurityKey(),
            ValidateIssuerSigningKey = true
        };

        options.Events = new JwtBearerEvents
        {
            OnMessageReceived = context =>
            {
                var accessToken = context.Request.Query["access_token"];

                // если запрос направлен хабу
                var path = context.HttpContext.Request.Path;
                if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/chat"))
                {
                    // получаем токен из строки запроса
                    context.Token = accessToken;
                }
                return Task.CompletedTask;
            }
        };
    });

builder.Services.AddSignalR();

var app = builder.Build();


app.UseDefaultFiles();
app.UseStaticFiles();

app.UseAuthentication();   // добавление middleware аутентификации 
app.UseAuthorization();   // добавление middleware авторизации 


app.MapPost("/login",  (Person loginModel) =>
{
    // находим пользователя 
    Person? person = people.FirstOrDefault(p => p.Email == loginModel.Email && p.Password == loginModel.Password);
    // если пользователь не найден, отправляем статусный код 401
    if (person is null) return Results.Unauthorized();

    var claims = new List<Claim> { new Claim(ClaimTypes.Name, person.Email) };
    // создаем JWT-токен
    var jwt = new JwtSecurityToken(
            issuer: AuthOptions.ISSUER,
            audience: AuthOptions.AUDIENCE,
            claims: claims,
            expires: DateTime.UtcNow.Add(TimeSpan.FromMinutes(2)),
            signingCredentials: new SigningCredentials(AuthOptions.GetSymmetricSecurityKey(), SecurityAlgorithms.HmacSha256));
    var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);

    // формируем ответ
    var response = new
    {
        access_token = encodedJwt,
        username = person.Email
    };

    return Results.Json(response);
});

app.MapHub<ChatHub>("/chat");
app.Run();

record class Person(string Email, string Password);
public class AuthOptions
{
    public const string ISSUER = "MyAuthServer"; // издатель токена
    public const string AUDIENCE = "MyAuthClient"; // потребитель токена
    const string KEY = "mysupersecret_secretsecretsecretkey!123";   // ключ для шифрации
    public static SymmetricSecurityKey GetSymmetricSecurityKey() =>
        new SymmetricSecurityKey(Encoding.UTF8.GetBytes(KEY));
}

Для представления отдельного пользователя здесь определен класс Person с двумя свойствами, а для целей тестирования в приложении опреден список объектов Person. Для хранения настроек токена определен вспомогательный класс AuthOptions.

В отличие от стандартной работы с токеном в webapi здесь нам еще надо получить токен из строки запроса, если запрос обращен к хабу:

options.Events = new JwtBearerEvents
{
	OnMessageReceived = context =>
	{
		var accessToken = context.Request.Query["access_token"];

		// если запрос направлен хабу
		var path = context.HttpContext.Request.Path;
		if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/chat"))
		{
			// получаем токен из строки запроса
			context.Token = accessToken;
		}
		return Task.CompletedTask;
	}
};

То есть, если запрос идет по пути "/chat", а в строке запроса передан параметр "access_token" (который хранит токен), то токен извлекается и устанавливается для свойства context.Token. Таким образом, мы сможем идентифицировать текущего пользователя.

Для непосредственной аутентификации пользователя и выдачи токена определена конечная точка app.MapPost("/login"), которая через post-запрос получает логин и пароль пользователя и и возвращает токен и имя пользователя.

Создание клиентской части

Для тестирования определим в проекте в папке wwwroot следующую веб-страницу index.html:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>METANIT.COM</title>
</head>
<body>
    <div id="loginBlock">
        <p>
            Введите логин:<br />
            <input id="email" type="text" />
        </p>
        <p>
            Введите пароль:<br />
            <input id="password" type="password" />
        </p>
        <input id="loginBtn" type="button" value="Войти" />
    </div><br />

    <div id="inputForm">
        <input type="text" id="message" />
        <input type="button" id="sendBtn" disabled value="Отправить" />
    </div>
    <div id="chatroom"></div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/6.0.1/signalr.js"></script>
    <script>
        let token;      // токен
        let username;   // имя пользователя
        const hubConnection = new signalR.HubConnectionBuilder()
            .withUrl("/chat", { accessTokenFactory: () => token })
            .build();

        // аутентификация
        document.getElementById("loginBtn").addEventListener("click", async ()=>{

            // отправляем запрос на аутентификацию
            // посылаем запрос на адрес "/login", в ответ получим токен и имя пользователя
            const response = await fetch("/login", {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({
                    email: document.getElementById("email").value,
                    password: document.getElementById("password").value
                })
            });

            // если запрос прошел нормально
            if (response.ok === true) {
                // получаем данные
                const data = await response.json();
                token = data.access_token;
                username = data.username;
                document.getElementById("loginBtn").disabled = true;

                hubConnection.start()       // начинаем соединение с хабом
                    .then(() =>  document.getElementById("sendBtn").disabled = false )
                    .catch(err =>  console.error(err.toString()));
            }
            else {
                // если произошла ошибка, получаем код статуса
                console.log(`Status: ${response.status}`);
            }
        });

        // отправка сообщения от простого пользователя
        document.getElementById("sendBtn").addEventListener("click", () => {
            const message = document.getElementById("message").value;
            hubConnection.invoke("Send", message, username)
                .catch(error => console.error(error));
        });
        // получение сообщения от сервера
        hubConnection.on("Receive", (message, user) => {

            // создаем элемент <b> для имени пользователя
            const userNameElem = document.createElement("b");
            userNameElem.textContent = `${user}: `;

            // создает элемент <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);
        });
    </script>
</body>
</html>

Изначально токен не установлен, он будет установлен только после прохождении аутентификации на сервере. Для этого по нажатию на кнопку loginBtn посылается POST-запрос на сервер по адресу "/login" и взамен получаем токен и имя пользователя, которые сохраняются в глобальные переменные на веб-странице.

И только после получения токена запускатся соединение:

hubConnection.start()

При этом при установке соединения в SignalR в клиенте javascript необходимо дополнительно передавать токен:

const hubConnection = new signalR.HubConnectionBuilder()
            .withUrl("/chat", { accessTokenFactory: () => token })
            .build();

То есть SignalR на стороне клиент не добавляет токен в заголовки запроса. И при обращении к хабу токен фактически будет посылаться как параметр строки запроса.

Результат работы программы:

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