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

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

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

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

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

Role Authorization and Cookie Authentification in SignalR

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

Пусть в папке Models находятся контекст данных ApplicationContext и две модели Role и User, чьи объекты хранятся в базе данных:

using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;

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; }
    }
}

И также для передачи данных из представления в контроллер в папке Models имеет модель представления LoginModel:

using System.ComponentModel.DataAnnotations;

namespace AuthSignalRApp.Models
{
    public class LoginModel
    {
        [Required(ErrorMessage = "Не указан Email")]
        public string Email { get; set; }

        [Required(ErrorMessage = "Не указан пароль")]
        [DataType(DataType.Password)]
        public string Password { get; set; }
    }
}

Контроллер AccountController в папке Controllers управляет аутентификацией:

using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using AuthSignalRApp.Models;
using Microsoft.EntityFrameworkCore;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;

namespace AuthSignalRApp.Controllers
{
    public class AccountController : Controller
    {
        private ApplicationContext _context;
        public AccountController(ApplicationContext context)
        {
            _context = context;
        }
		// тестирование SignalR
        public IActionResult Index()
        {
            return View();
        }
        [HttpGet]
        public IActionResult Login()
        {
            return View();
        }
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Login(LoginModel model)
        {
            if (ModelState.IsValid)
            {
                User user = await _context.Users
                    .Include(u => u.Role)
                    .FirstOrDefaultAsync(u => u.Email == model.Email && u.Password == model.Password);
                if (user != null)
                {
                    await Authenticate(user); // аутентификация
                    return RedirectToAction("Index", "Account");// переадресация на метод Index
                }
                ModelState.AddModelError("", "Некорректные логин и(или) пароль");
            }
            return View(model);
        }
        public async Task<IActionResult> Logout()
        {
            await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
            return RedirectToAction("Login", "Account");
        }
        private async Task Authenticate(User user)
        {
            var claims = new List<Claim>
            {
                new Claim(ClaimsIdentity.DefaultNameClaimType, user.Email),
                new Claim(ClaimsIdentity.DefaultRoleClaimType, user.Role?.Name)
            };
            ClaimsIdentity id = new ClaimsIdentity(claims, "ApplicationCookie", ClaimsIdentity.DefaultNameClaimType,
                ClaimsIdentity.DefaultRoleClaimType);
            await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(id));
        }
    }
}

Для аутентификации в папке Views/Account определим представление Login.cshtlm:

@model AuthSignalRApp.Models.LoginModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<h2>Вход на сайт</h2>

<form asp-action="Login" asp-controller="Account" asp-anti-forgery="true">
    <div class="validation" asp-validation-summary="ModelOnly"></div>
    <div>
        <div class="form-group">
            <label asp-for="Email">Введите Email</label><br />
            <input type="text" asp-for="Email" />
            <span asp-validation-for="Email" /><br />
        </div>
        <div class="form-group">
            <label asp-for="Password">Введите пароль</label><br />
            <input asp-for="Password" />
            <span asp-validation-for="Password" /><br />
        </div>
        <div class="form-group">
            <input type="submit" value="Войти" class="btn" />
        </div>
    </div>
</form>

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

Для тестирования аутентификации и авторизации в SignalR определим следующий класс хаб:

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(Roles="admin")]
        public async Task Notify(string message, string userName)
        {
            await Clients.All.SendAsync("Receive", message, userName);
        }
    }
}

Для организации доступа к хабу и его методам можно применять атрибут AuthorizeAttribute. В данном случае функциональность хаба доступна только для аутентифицировнных пользователей, то есть тех, кто условно залогинился в приложении. Таким образом, анонимные пользователи не смогут вызвать методы хаба, даже если в их распоряжении будет код javascript, который обращается к этим методам.

Кроме того, для метода Notify указано еще одно ограничение - доступ только для тех пользователей, которые принадлежат роли "admin". В остальном оба метода одинаковы - принимают два параметра и вызывают на клиенте функцию "Receive".

Для взаимодействия с хабом в папке Views/Account определим следующее представление Index.cshtml:

@if (User.Identity.IsAuthenticated)
{
    <a href="~/Account/Logout" >Выйти</a>
}
else
{
    <a href="~/Account/Login" >Войти</a>
}

<div id="userNameBlock">
    Введите ник:<br />
    <input id="userName" 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" value="Отправить" />
</div>
@if (User.IsInRole("admin"))
{
    <br /><div id="notifyForm">
        <input type="text" id="notify" />
        <input type="button" id="notifyBtn" value="Уведомление" />
    </div>
}

<div id="chatroom"></div>

<script src="~/js/signalr/dist/browser/signalr.min.js"></script>
<script>
    const hubConnection = new signalR.HubConnectionBuilder()
        .withUrl("/chat")
        .build();

    let userName = "";
    // получение сообщения от сервера
    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) {
        userName = document.getElementById("userName").value;
        document.getElementById("header").innerHTML = "<h3>Welcome " + userName + "</h3>";
    });
    // отправка сообщения от простого пользователя
    document.getElementById("sendBtn").addEventListener("click", function (e) {
        let message = document.getElementById("message").value;
        hubConnection.invoke("Send", message, userName);
    });
    // если администратор добавляем блок
    @if (User.IsInRole("admin"))
    {
        // отправка сообщения от администратора
        <text>
        document.getElementById("notifyBtn").addEventListener("click", function(e) {
            let message = document.getElementById("notify").value;
            hubConnection.invoke("Notify", message, userName);
        });
        </text>
    }
    hubConnection.start();
</script>

С помощью проверки на роль @if (User.IsInRole("admin")) добвляются блоки html и код javascript, который будет у условного администратора. Отмечу, что это делается именно в представлении, а не в статической html-странице, что позволяет нам воспользоваться преимуществами razor. Но даже если бы пользователь имел бы все эти блоки на странице, они все равно бы не работали, если бы он не принадлежал роли "admin", так как на стороне хаба его роль все равно бы проверялась.

И класс Startup выглядит следующим образом:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AuthSignalRApp.Models;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

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(CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie(options =>
                {
                    options.LoginPath = new Microsoft.AspNetCore.Http.PathString("/Account/Login");
                    options.AccessDeniedPath = new Microsoft.AspNetCore.Http.PathString("/Account/Login");
                });
            services.AddSignalR();
            services.AddControllersWithViews();
        }

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

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

            app.UseRouting();

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

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Account}/{action=Index}");
                endpoints.MapHub<ChatHub>("/chat");
            });
        }
    }
}

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

Авторизация и аутентификация в SignalR
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850