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

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

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

Использование токена при аутентификации и авторизации в SignalR будет несколько отличаться от использования кук. Частности, при установке соединения в SignalR в клиенте javascript необходимо дополнительно передавать токен:

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

В качестве второго параметра в метод withUrl передается объект, у которого параметр accessTokenFactory представляет функцию, которая возвращает токен. В данном случае возвращается значение переменной token. Но естественно механизмы получения токена могут быть и более сложные.

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

using Microsoft.EntityFrameworkCore;

namespace AuthSignalRApp.Models
{
    public class ApplicationContext : DbContext
    {
        public DbSet<User> Users { get; set; }
        public DbSet<Role> Roles { get; set; }
        public ApplicationContext(DbContextOptions<ApplicationContext> options)
            : base(options)
        {
            Database.EnsureCreated();
        }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            string adminRoleName = "admin";
            string userRoleName = "user";

            // добавляем роли
            Role adminRole = new Role { Id = 1, Name = adminRoleName };
            Role userRole = new Role { Id = 2, Name = userRoleName };
            User adminUser1 = new User { Id = 1, Email = "admin@mail.com", Password = "123456", RoleId = adminRole.Id };
            User adminUser2 = new User { Id = 2, Email = "tom@mail.com", Password = "123456", RoleId = adminRole.Id };
            User simpleUser1 = new User { Id = 3, Email = "bob@mail.com", Password = "123456", RoleId = userRole.Id };
            User simpleUser2 = new User { Id = 4, Email = "sam@mail.com", Password = "123456", RoleId = userRole.Id };

            modelBuilder.Entity<Role>().HasData(new Role[] { adminRole, userRole });
            modelBuilder.Entity<User>().HasData(new User[] { adminUser1, adminUser2, simpleUser1, simpleUser2 });
            base.OnModelCreating(modelBuilder);
        }
    }
	public class Role
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public List<User> Users { get; set; }
        public Role()
        {
            Users = new List<User>();
        }
    }
	public class User
    {
        public int Id { get; set; }
        public string Email { get; set; }
        public string Password { get; set; }

        public int? RoleId { get; set; }
        public Role Role { get; set; }
    }
}

Для работы с JWT-токенами установим через Nuget пакет Microsoft.AspNetCore.Authentication.JwtBearer.

Для хранения настроек токена пусть в проекте будет определен вспомогательный класс AuthOptions:

using Microsoft.IdentityModel.Tokens;
using System.Text;

namespace AuthSignalRApp
{
    public class AuthOptions
    {
        public const string ISSUER = "MyAuthServer"; // издатель токена
        public const string AUDIENCE = "MyAuthClient"; // потребитель токена
        const string KEY = "mysupersecret_secretkey!123";   // ключ для шифрации
        public const int LIFETIME = 1; // время жизни токена - 1 минута
        public static SymmetricSecurityKey GetSymmetricSecurityKey()
        {
            return new SymmetricSecurityKey(Encoding.ASCII.GetBytes(KEY));
        }
    }
}

Для аутентификации пусть будет определен контроллер AccountController:

using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using AuthSignalRApp.Models;
using Microsoft.EntityFrameworkCore;
using System.Security.Claims;
using System;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;

namespace AuthSignalRApp.Controllers
{
    public class AccountController : Controller
    {
        private ApplicationContext _context;
        public AccountController(ApplicationContext context)
        {
            _context = context;
        }
        [HttpPost("/token")]
        public async Task<IActionResult> Token(string username, string password)
        {
            var identity = await GetIdentity(username, password);
            if (identity == null)
            {
                return BadRequest("Invalid username or password.");
            }

            var now = DateTime.UtcNow;
            // создаем JWT-токен
            var jwt = new JwtSecurityToken(
                    issuer: AuthOptions.ISSUER,
                    audience: AuthOptions.AUDIENCE,
                    notBefore: now,
                    claims: identity.Claims,
                    expires: now.Add(TimeSpan.FromMinutes(AuthOptions.LIFETIME)),
                    signingCredentials: new SigningCredentials(AuthOptions.GetSymmetricSecurityKey(), SecurityAlgorithms.HmacSha256));
            var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);

            var response = new
            {
                access_token = encodedJwt,
                username = identity.Name
            };
            return Json(response);
        }

        private async Task<ClaimsIdentity> GetIdentity(string username, string password)
        {
            User person = await _context.Users.Include(r => r.Role).FirstOrDefaultAsync(x => x.Email == username && x.Password == password);
            if (person != null)
            {
                var claims = new List<Claim>
                {
                    new Claim(ClaimsIdentity.DefaultNameClaimType, person.Email),
                    new Claim(ClaimsIdentity.DefaultRoleClaimType, person.Role.Name)
                };
                ClaimsIdentity claimsIdentity =
                new ClaimsIdentity(claims, "Token", ClaimsIdentity.DefaultNameClaimType,
                    ClaimsIdentity.DefaultRoleClaimType);
                return claimsIdentity;
            }
            // если пользователь не найден
            return null;
        }
    }
}

Метод Token будет получать логин и пароль и возвращать токен и имя пользователя.

Пусть у нас будет следующий хаб ChatHub:

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 userName)
        {
            await Clients.All.SendAsync("Receive", message, userName);
        }
    }
}

Поскольку к классу применяется атрибут Authorize, то доступ к хабу установлен только для аутентифицировнных пользователей.

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

<!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="header"></div><br />

    <div id="inputForm">
        <input type="text" id="message" />
        <input type="button" id="sendBtn" disabled value="Отправить" />
    </div>
    <div id="chatroom"></div>

    <script src="js/signalr/dist/browser/signalr.min.js"></script>
    <script>
        let token;		// токен
        let username;	// имя пользователя
        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);
        });

        // аутентификация
        document.getElementById("loginBtn").addEventListener("click", function (e) {
            
            var request = new XMLHttpRequest();
			// посылаем запрос на адрес "/token", в ответ получим токен и имя пользователя
            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;
                    username = data.username;

                    document.getElementById("sendBtn").disabled = false;

                    hubConnection.start()       // начинаем соединение с хабом
                    .catch(err => {  
                        console.error(err.toString());
                        document.getElementById("loginBtn").disabled = true;
                        document.getElementById("sendBtn").disabled = true;
                    });
                }
            });
            // отправляем запрос на аутентификацию
            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;
            hubConnection.invoke("Send", message, username);
        });
    </script>
</body>
</html>

Изначально токен не установлен, он будет установлен только после прохождении аутентификации на сервере. По нажатию на кнопку loginBtn отправляется запрос, который должен вернуть токен и имя пользователя. И только после получения токена запускатся соединение:

hubConnection.start()

В итоге проект может выглядеть следующим образом:

Token-based authentication and authorization in SignalR ASP.NET Core

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

using System.Threading.Tasks;
using AuthSignalRApp.Models;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;

namespace AuthSignalRApp
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            string connection = "Server=(localdb)\\mssqllocaldb;Database=authsignalrappdb;Trusted_Connection=True;";
            services.AddDbContext<ApplicationContext>(options => options.UseSqlServer(connection));

            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                {
                    options.RequireHttpsMetadata = false;
                    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;
                        }
                    };
                });
            services.AddSignalR();
            services.AddControllers();
        }

        public void Configure(IApplicationBuilder app)
        {
            app.UseDeveloperExceptionPage();

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

            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
                endpoints.MapHub<ChatHub>("/chat");
            });
        }
    }
}

В отличие, скажем, от стандартной работы с токеном в 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;
	}
};
Токен авторизации в SignalR в ASP.NET Core
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850