Данное руководство устарело. Актуальное руководство: Руководство по ASP.NET Core 7
Использование токена при аутентификации и авторизации в 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()
В итоге проект может выглядеть следующим образом:
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; } };