Система ASP.NET Core Identity достаточно расширяемая и конфигурируемая. И, несмотря на то, что уже по умолчанию при подключении ASP.NET Core Identity мы получаем достаточно функциональный механизм аутентификации пользователей, всё же может потребоваться изменить работу этой системы, исходя из собственных требований и предпочтений. И, начиная с этой части, мы попробуем разработать свою систему аутентификации пользователей в проекте ASP.NET Core MVC, используя рассмотренные ранее базовые классы Identity.
По умолчанию, для хранения данных о пользователях системы используется база данных SQL Server. При этом, мы не ограничены в выборе собственного хранилища. Так, для демонстрации возможностей ASP.NET Core Identity, мы будем строить свою систему аутентификации на базе БД SQLite (в дальнейшем, если потребуется, можно будет задействовать любую БД, поддерживаемую в Entity Framework Core).
Подготовка проекта к использованию ASP.NET Core Identity
Создание нового проекта ASP.NET Core MVC и установка необходимых nuget-пакетов
Создадим новый проект ASP.NET Core MVC без использования проверки подлинности и назовем его, например, AspMvcAuth
Для работы нам понадобятся следующие nuget-пакеты:
- Microsoft.EntityFrameworkCore
- Microsoft.EntityFrameworkCore.Design
- Microsoft.EntityFrameworkCore.Sqlite
- Microsoft.AspNetCore.Identity.EntityFramework
- Microsoft.AspNetCore.Identity.UI
Первые три пакета необходимы для использования Entity Framework Core с SQLite, четвертый и пятый — для работы с ASP.NET Core Identity.
Создание класса пользователя системы
Вначале создадим свой класс пользователя системы. Этот класс должен быть наследником базового класса IdentityUser
. Например, расширим базовый класс следующим образом:
using Microsoft.AspNetCore.Identity; namespace AspMvcAuth.Models { public class ApplicationUser: IdentityUser<int> { public DateOnly Birthday { get; set; } public string Organization { get; set; } public string Departmenet { get; set; } } }
Здесь стоит отметить то, что в качестве первичного ключа для пользователя будет использоваться целочисленное значение:
public class ApplicationUser: IdentityUser<int>
по умолчанию, первичный ключ представляет собой строку.
Создание контекста данных
На втором шаге мы должны определить контекст данных. Контекст данных для работы с БД пользователей должен быть наследником базового класса IdentityDbContext
. При этом, стоит учитывать, что мы должны явно указать для контекста данных какой тип данных мы будем использовать для первичного ключа. Поэтому нам необходимо воспользоваться следующей версией класса IdentityDbContext
для нашего контекста данных:
public class IdentityDbContext<TUser, TRole, TKey>
здесь мы должны определить класс пользователя, класс для хранения ролей и тип первичного ключа. Вначале, определим новый класс для ролей пользователей:
using Microsoft.AspNetCore.Identity; namespace AspMvcAuth.Models { public class ApplicationRole: IdentityRole<int> { } }
Теперь добавим в приложение контекст данных:
using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using AspMvcAuth.Models; using Microsoft.EntityFrameworkCore; namespace AspMvcAuth.Data { public class ApplicationContext : IdentityDbContext<ApplicationUser, ApplicationRole, int> { public ApplicationContext(DbContextOptions<ApplicationContext> options) : base(options) { } } }
Теперь добавим в конфигурацию приложения строку подключения к БД SQLite. Для этого можем воспользоваться файлом appsettings.json или же использовать хранилище секретов пользователей. Для примера, используем хранилище секретов. Откроем хранилище секретов пользователей:
И добавим в файл secrets.json строку подключения к SQLite:
{ "ConnectionStrings": { "DefaultConnection": "DataSource=Identity.sqlite" } }
Теперь создадим новую миграцию. Для этого воспользуемся менеджером пакетов Nuget и выполним команду
PM> Add-Migration Init
и сразу обновим базу данных, выполнив команду
PM> Update-Database
После этого мы получим следующую структуру проекта:
В папке проекта появится файл Identity.sqlite с базой данных пользователей, структура которой будет выглядеть следующим образом:
Так как мы создали свой тип пользователя (ApplicationUser
), то в БД таблица AspNetUsers будет выглядеть следующим образом:
Теперь добавим контекст БД в приложение в виде сервиса. Для этого добавим в Program.cs следующие строки:
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); builder.Services.AddDbContext<ApplicationContext>(options => options.UseSqlite(connectionString));
Подключение сервисов ASP.NET Core Identity
Для подключения сервисов ASP.NET Core Identity перейдем в файл Program.cs и воспользуется методами расширения:
builder.Services.AddDefaultIdentity<ApplicationUser>() .AddEntityFrameworkStores<ApplicationContext>();
Здесь мы добавили необходимые сервисы ASP.NET Core Identity в приложение, включая и сервис хранилища данных. Теперь можно переходить к следующему шагу — добавлению необходимых моделей данных, контроллеров и представлений.
Использование Identity в проекте ASP.NET Core MVC
Регистрация пользователя
Для работы с учётными записями пользователей создадим новую область с названием Account
. Ниже представлен состав проекта при создании новой области с использованием возможностей Visual Studio:
Сразу добавим в папку Areas файлы _ViewImports.cshtml и _ViewStart.cshtml со следующим содержимым:
_ViewImports.cshtml:
@using AspMvcAuth.Areas.Account.Models @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
_ViewStart.cshtml:
@{ Layout = "/Views/Shared/_Layout.cshtml"; }
Теперь наши представления в области будут использовать общий для всего приложения макет, а в отдельные представления не придётся каждый раз подключать пространство имен с нашими моделями данных.
Теперь создадим модель, которая будет содержать информацию, необходимую для регистрации пользователя. Создадим в папке Areas/Account/Model файл RegisterModel.cs со следующим содержимым:
using System.ComponentModel; using System.ComponentModel.DataAnnotations; using AspMvcAuth.Models; namespace AspMvcAuth.Areas.Account.Models { public class RegisterModel { [Required(ErrorMessage ="Поле Логин обязательно для заполнения")] [DisplayName("Логин")] public string Login { get; set; } [Required(ErrorMessage = "Поле E-mail обязательно для заполнения")] [EmailAddress(ErrorMessage ="Введен некорректный адрес")] [DisplayName("E-mail")] public string Email { get; set; } [Required(ErrorMessage = "Поле Организация обязательно для заполнения")] [DisplayName("Организация")] public string Organization { get; set; } [Required(ErrorMessage = "Поле Отдел обязательно для заполнения")] [DisplayName("Отдел")] public string Department { get; set; } [Required(ErrorMessage = "Поле День рождения обязательно для заполнения")] [DisplayName("День рождения")] public DateTime Birthday { get; set; } [Required(ErrorMessage = "Поле Пароль обязательно для заполнения")] [DisplayName("Пароль")] public string Password { get; set; } [Required(ErrorMessage = "Поле Пароль ещё раз обязательно для заполнения")] [Compare("Password",ErrorMessage = "Пароли не совпадают")] [DisplayName("Пароль ещё раз")] public string ConfirmPassword { get; set; } public ApplicationUser GetUser() { ApplicationUser user = new() { Email = Email, UserName = Login, Organization = Organization, Departmenet = Department, Birthday = DateOnly.FromDateTime(Birthday) }; return user; } } }
Так как нам необходимо будет проверять корректность введенных пользователем данных, то сразу были использованы необходимые атрибуты валидации для модели. Также, для удобства, добавили атрибуты DisplayName
для свойств, которые мы будем отображать в форме регистрации.
Наша модель имеет всего один метод — GetUser()
, который, используя свойства класса возвращает объект класса ApplicationUser
. Теперь используем эту модель в представлении. Для этого создадим новое пустое представление с именем Register.cshtml в папке Areas/Account/Views/Account и добавим в него следующий код:
@model RegisterModel @{ ViewData["Title"] = "Регистрация"; } <h1>@ViewData["Title"]</h1> <form asp-controller="Account" asp-action="Register" method="post"> <div asp-validation-summary="All"></div> <div> <label class="form-label" asp-for="Login"></label> <input class="form-control" name='Login' asp-for="Login" /> <label class="form-label" asp-for="Email"></label> <input class="form-control" name="Email" asp-for="Email" /> <label class="form-label" asp-for="Organization"></label> <input class="form-control" name="Organization" asp-for="Organization" /> <label class="form-label" asp-for="Department"></label> <input class="form-control" name="Department" asp-for="Department" /> <label class="form-label" asp-for="Birthday"></label> <input class="form-control" name="Birthday" asp-for="Birthday" type="date" /> <label class="form-label" asp-for="Password"></label> <input class="form-control" name="Password" asp-for="Password" type="password" /> <label class="form-label" asp-for="ConfirmPassword"></label> <input class="form-control" name="ConfirmPassword" asp-for="ConfirmPassword" type="password" /> <br /> <button type="submit" class="btn btn-primary mb-3">Отправить</button> </div> </form>
Здесь мы создали форму добавления нового пользователя в систему, используя tag-хэлперы для форм. Единственный новый элемент, который появился в коде — это классы элементов формы, типа class="form-label"
и class="form-control"
. Это классы фреймворка Bootstrap и используем мы их просто для того, чтобы форма имела какой-никакой презентабельный вид.
Обработка POST-запроса будет происходить в контроллере AccountController
, который должен содержать действие Register
о чем нам говорят атрибуты тэга form
:
asp-controller="Account" asp-action="Register"
Также можно обратить внимание на то, что валидация формы будет происходить на стороне клиента, о чем нам сообщает элемент формы:
<div asp-validation-summary="All"></div>
Поэтому сразу добавим в файл Views/Shared/_Layout.cshtml необходимые скрипты для валидации:
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.3/jquery.validate.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.12/jquery.validate.unobtrusive.js"></script>
Осталось создать контроллер для обработки запросов пользователя. Создадим в папке Areas/Account/Controllers файл AccountController.cs со следующим содержимым:
using AspMvcAuth.Models; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; namespace AspMvcAuth.Areas.Account.Controllers { [Area("Account")] public class AccountController : Controller { private readonly UserManager<ApplicationUser> _userManager; private readonly SignInManager<ApplicationUser> _signInManager; public AccountController(UserManager<ApplicationUser> manager, SignInManager<ApplicationUser> signInManager) { _userManager = manager; _signInManager = signInManager; } [Route("{area}/{action}")] [HttpPost] public async Task<IActionResult> SignIn(LoginModel model) { var result = await _signInManager.PasswordSignInAsync(model.Login, model.Password, false, lockoutOnFailure: false); if (result.Succeeded) { return LocalRedirect("/"); } else { ModelState.AddModelError("", $"Задан неверный логин или пароль"); } return View("Login"); } [Route("{area}/{action}")] [HttpGet] public IActionResult SignIn() { return View("Login"); } [Route("{area}/{action}")] [HttpGet] public IActionResult Register() { return View(); } [Route("{area}/{action}")] [HttpPost] public async Task<IActionResult> Register(RegisterModel model) { var user = model.GetUser(); IdentityResult result = await _userManager.CreateAsync(user, model.Password); if (result.Succeeded) { return RedirectToAction("Index", "Home"); } else { foreach (var error in result.Errors) { ModelState.AddModelError("", error.Description); } return View(); } } } }
Для работы с объектом пользователя контроллер будет использовать один из базовых классов ASP.NET Core Identity — UserManager
, который мы запрашиваем, используя механизм DI как зависимость. Что касается действия контроллера Register()
, то для него указан маршрут:
[Route("{area}/{action}")]
то есть, доступ к странице регистрации будет происходить по пути /account/register
. При GET-запросе контроллер будет возвращать созданное нами представление:
public IActionResult Register() { return View(); }
Если же отправляется POST-запрос, то мы пробуем добавить нового пользователя в хранилище:
var result = await _userManager.CreateAsync(model.GetUser(), model.Password);
в результате выполнения этого выражения result
будет содержать объект типа IdentityResult
, который будет содержать флаг Succeeded=true
в случае успешного добавления пользователя в хранилище, либо список ошибок, который произошли при попытке добавить нового пользователя в хранилище. Если добавление нового пользователя происходит без ошибок, то контроллер просто перенаправляет нас на главную страницу приложения, иначе — вернет нас к представлению Register.cshtml в котором в сводке по ошибкам будут выданы ошибки, полученные при попытке регистрации нового пользователя.
Проверим работу нашего приложения. Запустим приложение и перейдем по пути https://localhost:[:port]/account/register
Если мы попробуем сразу нажать кнопку отправить, то увидим сводку по ошибкам валидации:
Попробуем ввести корректные данные и зарегистрировать пользователя. Если все данные будут заполнены корректно, то мы перейдем снова на главную страницу приложения, а в базе данных появится новая запись:
Также мы можем увидеть следующие ошибки при регистрации:
Это как раз тот самый объект IdentityResult
. В данном случае модель проходит валидацию в соответствии с заданными атрибутами валидации, но, при этом, настройки требований к паролям в самой ASP.NET Core Identity не позволяют зарегистрировать пользователя с заданным паролем. Чтобы настроить требования к паролю, мы можем воспользоваться опциями сервисов ASP.NET Core Identity, например, следующим образом:
builder.Services.AddDefaultIdentity<ApplicationUser>(options => { options.Password.RequireNonAlphanumeric = true;//пароль должен содержать не буквенно-цифровые символы (например, подчеркивание) options.Password.RequireDigit = true;//пароль должен содержать цифры options.Password.RequiredLength = 10;//минимальная длина пароля }) .AddEntityFrameworkStores<ApplicationContext>();
Вход/выход пользователя
После того, как пользователь зарегистрирован — он может войти в систему. Для входа пользователя в систему добавим в контроллер AccountController новый сервис:
private readonly SignInManager<ApplicationUser> _signInManager; private readonly ApplicationContext _context; public AccountController(UserManager<ApplicationUser> manager, SignInManager<ApplicationUser> signInManager) { _userManager = manager; _signInManager = signInManager; }
а также новое действие SignIn()
:
[Route("{area}/{action}")] [HttpPost] public async Task<IActionResult> SignIn(LoginModel model) { var result = await _signInManager.PasswordSignInAsync(model.Login, model.Password, false, lockoutOnFailure: false); if (result.Succeeded) { return LocalRedirect("/"); } else { ModelState.AddModelError("", $"Задан неверный логин или пароль"); } return View("Login"); } [Route("{area}/{action}")] [HttpGet] public IActionResult SignIn() { return View("Login"); }
При POST-запросе производится вход пользователя в систему по логину и паролю:
var result = await _signInManager.PasswordSignInAsync(model.Login, model.Password, false, lockoutOnFailure: false);
При удачном входе нас снова перенаправят на главную страницу, иначе — получим сообщение об ошибке в логине или пароле.
Теперь добавим новое представление с именем Login.cshtml в папку Areas/Account/Views/Account:
@model LoginModel @{ ViewData["Title"] = "Вход в приложение"; } <h1>@ViewData["Title"]</h1> <form asp-controller="Account" asp-action="SignIn" method="post"> <div asp-validation-summary="All"></div> <div> <label class="form-label" asp-for="Login"></label> <input class="form-control" name='Login' asp-for="Login" /> <label class="form-label" asp-for="Password"></label> <input class="form-control" name="Password" asp-for="Password" type="password" /> <br /> <button type="submit" class="btn btn-primary mb-3">Отправить</button> </div> </form>
и проверим работу приложения. Для этого запустим приложение и нажмем в браузере F12, чтобы открыть инструменты разработчика:
Теперь перейдем по адресу https://localhost:[:port]/account/signin
Для формы был сгенерирован antiforgery token. Теперь введем логин и пароль заданные при регистрации и нажмем кнопку «Отправить». При успешном входе, нам будет отправлена кука:
При закрытии браузера она удалится. Вход работает, осталось выполнить выход из системы. Для этого добавим в контроллер следующее действие:
public async Task<IActionResult> SignOut() { await _signInManager.SignOutAsync(); return LocalRedirect("/"); }
И создадим простенький компонент представления, который назовем AccountMenu:
using Microsoft.AspNetCore.Mvc; namespace AspMvcAuth.Components { public class AccountMenu : ViewComponent { public IViewComponentResult Invoke() { return View(HttpContext.User); } } }
Представление Default.cshtml для компонента сделаем пока таким:
@using System.Security.Claims @model ClaimsPrincipal <div> @if (Model.Identity.IsAuthenticated) { <p>Привет, @Model.Identity.Name</p> <a href="/account/signout">Выйти из приложения</a> } else { <p>Привет, Анонимус!</p> <a href="/account/signin">Войти в приложение</a> } </div>
То есть, в зависимости от состояния аутентификации пользователя, нам будет предложено либо войти в систему, либо выйти из неё. Добавим этот компонент на главную страницу приложения (Views/Home/Index.cshtml)
@addTagHelper *, AspMvcAuth @{ ViewData["Title"] = "Home Page"; } <div class="text-center"> <h1 class="display-4">Welcome</h1> <vc:account-menu></vc:account-menu> </div>
Снова запустим приложение:
Войдем в систему и получим куку аутентификации:
Нажмем ссылку «Выйти из приложения» и убедимся, что мы действительно вышли из приложения:
Как можно видеть по рисунку кука авторизации была удалена, а на главной странице вместо логина пользователя снова появился «Анонимус». Следовательно, наша простенькая система аутентификации пользователей работает.
Итого
В этой части мы разработали простую систему аутентификации пользователей в приложении ASP.NET Core MVC, используя возможности, предоставляемые библиотекой ASP.NET Core Identity. В качестве упрощения, для хранения данных пользователя использовалась база данных SQLite. Для регистрации, входа и выхода пользователя мы использовали классы, предоставляемые ASP.NET Core Identity и, при этом создали свой класс пользователя на основе базового класса IdentityUser
.