ASP.NET Core Identity

В подавляющем большинстве случаев, для доступа пользователей приложения Web API к ресурсам используются механизмы аутентификации и авторизации пользователей. В этой главе мы разберемся с механизмом аутентификации и авторизации пользователей в приложении ASP.NET Core Web API. Но, для начала, разберемся с основными определениями

В этой статье используется приложение, которое разрабатывается на протяжении всей главы. Весь исходник приложения с последними изменениями находится по этой ссылке

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

Аутентификация (проверка подлинности) — это процесс установления личности пользователя. То есть аутентификация — это процесс, в результате которого мы получаем ответ на вопрос «Кто осуществляет вход в систему?». После того, как пользователь проходит аутентификацию и пробует получить доступ к ресурсу, запускается следующий процесс — авторизация.

Авторизация — это процесс определения, есть ли у пользователя доступ к ресурсу. Пользователь может быть аутентифицирован (определен), но, например, не может просматривать раздел с маркировкой «18+» так как его возраст менее 18 лет (не авторизован).

Таким образом, в самом простом случае, для обеспечения аутентификации и авторизации пользователей мы должны предусмотреть в своем приложении (или в отдельном сервисе) следующие возможности:

  1. создание, чтение, редактирование и удаление данных учётных записей пользователей API.
  2. получение, обновление или отзыв ключа доступа к API для конкретного пользователя

Рассмотрим по порядку весь процесс аутентификации и авторизации пользователей в приложениях Web API.

ASP.NET Core Identity

ASP.NET Core Identity — это специальный API, включающий в себя механизмы управления пользователями и предоставляющий богатый набор сервисов, которые помогают нам создавать учетные записи пользователей, хешировать их пароли, создавать модели базы данных для хранения учётных данных и аутентификации в целом.

Для работы с ASP.NET Core Identity нам потребуется также библиотека Entity Framework Core с которой мы уже работали.

Теперь нам необходимо настроить приложение для использования ASP.NET Core Identity и первое, что необходимо сделать – это определить класс пользователя системы.

Класс пользователя системы (IdentityUser)

IdentityUser – это основной класс пользователя в ASP.NET Core Identity, который содержит следующие полезные для работы свойства (см. Таблица 38)

Таблица 37 — Свойства класса IdentityUser

Имя Тип Описание
Id TKey Первичный ключ для записи пользователя в базе данных. При использовании класса IdentityUser TKey представляется типом String.
AccessFailedCount Int Количество неудачных попыток входа для текущего пользователя
ConcurrencyStamp String Случайное значение, которое должно изменяться всякий раз, когда пользователь сохраняется в хранилище
Email String Адрес электронной почты пользователя
EmailConfirmed Boolean Флаг, указывающий, подтвердил ли пользователь свой адрес электронной почты
LockoutEnabled Boolean Флаг, указывающий, может ли пользователь быть заблокирован
LockoutEnd Nullable<DateTimeOffset> Дата и время в формате UTC завершения блокировки пользователя
NormalizedEmail String Нормализованный адрес электронной почты пользователя
NormalizedUserName String Нормализованное имя пользователя
PasswordHash String Хэш-представление пароля пользователя
PhoneNumber String Номер телефона
PhoneNumberConfirmed Boolean Флаг, указывающий, подтвердил ли пользователь свой телефонный адрес
SecurityStamp String Случайное значение, которое должно изменяться при каждом изменении учетных данных пользователя
TwoFactorEnabled Boolean Флаг, указывающий, включена ли двухфакторная проверка подлинности для пользователя
UserName String Имя пользователя

Как можно видеть, IdentityUser уже содержит достаточно много свойств, идентифицирующих пользователя системы. Однако, если нам необходимо расширить перечень свойств класса пользователя, то ничего нам не мешает унаследовать собственный класс от IdentityUser и добавить таким образом необходимую нам функциональность в новый класс. Например, создадим новое приложение ASP.NET Core Web API,  добавим в проект папку Models/Identity и разместим в ней новый класс User:

public class User : IdentityUser<int>
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Таким образом, наш класс пользователя имеет два новых свойства – имя и фамилия пользователя, а запись в базе данных о пользователе будет иметь первичный ключ в виде целого числа (см. описание свойства Id в таблице выше).

Также стоит отметить, что класс IdentityUser содержит основные свойства необходимые для аутентификации пользователя, но, при этом, в целях безопасности, не хранит пароль пользователя в открытом виде, а только его хэш-представление. Теперь, определив класс пользователя мы можем настроить контекст базы данных для работы с ASP.NET Core Identity.

Для работы с ASP.NET Core Identity, обычно используется фреймворк Entity Framework Core, поэтому, если вы не знакомы с этим фреймворком, то рекомендую вам ознакомиться с его основами вот в этом руководстве.

Добавление контекста базы данных для ASP.NET Core Identity

Так как ASP.NET Core Identity использует в работе EF Core, то для работы с учётными записями пользователей необходим контекст базы данных. Поэтому вначале установим необходимые nuget-пакеты:

  1. Microsoft.AspNetCore.Identity.EntityFrameworkCore —  пакет добавит в наш проект основной функционал EF Core.
  2. Microsoft.EntityFrameworkCore.Sqlite — пакет добавит в наш проект функционал, необходимый для работы с SQLite
  3. Microsoft.EntityFrameworkCore.Design — добавляет возможности управления базой данных при разработке приложения

Теперь создадим в проекте новую папку Services и разместим в ней новый класс с названием ApplicationContext и следующим описанием:

public class ApplicationContext : IdentityDbContext<User, IdentityRole<int>, int>
{
    public ApplicationContext(DbContextOptions<ApplicationContext> options) : base(options)
    {

    }

}

здесь ApplicationContext  — это контекст базы данных. При этом, стоит обратить внимание на то, от какого класса наследуется наш контекст:

public class ApplicationContext : IdentityDbContext<User, IdentityRole<int>, int>

Так как мы в своем классе User переопределили тип первичного ключа со string на int, то при определении контекста базы данных нам необходимо наследовать наш класс от универсального класса IdentityDbContext, указав в универсальных параметрах тип пользователя, тип роли пользователей и непосредственно тип первичного ключа.

Теперь нам необходимо подключить в приложении новый сервис контекста базы данных. Для этого необходимо перейти в файл Program.cs и добавить следующий сервис:

builder.Services.AddDbContext<ApplicationContext>(builder => builder.UseSqlite("Data Source=IdentityBase.db"));

Здесь мы добавили новый сервис в контейнер DI и настроили его работу с базой SQLite, передав в параметрах метода AddDbContext() делегат вида Action<DbContextOptionsBuilder>?, а метод UseSqlite(), в свою очередь — это метод расширения для DbContextOptionsBuilder в котором мы указали строку подключения к SQLite.

Теперь нам необходимо создать миграцию и обновить базу данных. Для этого необходимо в Visual Studio в консоли диспетчера пакетов выполнить команду:

Add-Migration Initial

После выполнения этой команды в проекте появится новая папка Migrations с новой миграцией. Теперь вернемся к нашему контексту базы данных и немного изменим конструктор:

public ApplicationContext(DbContextOptions<ApplicationContext> options) : base(options)
{
    Database.Migrate();
}

Теперь новая миграция будет применяться при каждом запросе сервиса базы данных. Так как пока нам не требуется запрашивать сервис базы данных, перейдем к следующему моменту — зарегистрируем необходимые сервисы для работы с ASP.NET Core Identity.

Настройка приложения для работы с ASP.NET Core Identity

Теперь необходимо настроить наше приложение для работы с ASP.NET Core Identity, подключив и настроив необходимые сервисы. Для этого перейдем в файл Program.cs и внесем в него следующие изменения

builder.Services.AddAuthentication();
builder.Services.AddIdentity<User, IdentityRole<int>>(options =>
{
    options.Password.RequireNonAlphanumeric = true;
    options.Password.RequiredLength = 8;
    options.Password.RequireUppercase = false;
}).AddEntityFrameworkStores<ApplicationContext>()
  .AddDefaultTokenProviders();

var app = builder.Build();

app.UseAuthentication();

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

builder.Services.AddAuthentication();

и ниже, после строки var app = builder.Build(); добавили также компонент middleware для аутентификации:

app.UseAuthentication();

После этого мы добавили сервис по умолчанию для использования ASP.NET Core Identity, используя метод расширения для IServiceCollection:

builder.Services.AddIdentity<User, IdentityRole<int>>()

используя в качестве универсальных параметров метода класс пользователя и класс, используемый для ролей пользователей. Так как мы пока ещё ничего не знаем про роли пользователей и их использование в приложении, второй параметр представлен базовым классом роли – IdentityRole<int>. В качестве параметра метода мы передаем делегат типа  Action<IdentityOptions>, в котором указываем требования к паролю пользователя, а именно:

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

При необходимости можно задать и другие требования к паролю, а также настройки хранилища и т.д, в классе IdentityOptions определено довольно много различных настроек для ASP.NET Core Identity.

Метод AddIdentity() возвращает объект типа IdentityBuilder с помощью которого мы можем настраивать работу ASP.NET Core Identity. Так, в нашем случае, мы вызвали два метода расширения:

  • AddEntityFrameworkStores() с помощью которого указали контекст базы данных хранилища учётных сведений о пользователях
  • AddDefaultTokenProviders(), который добавляет провайдер по умолчанию с помощью которого мы можем работать с данными пользователей, выдавать им ключи доступа (токены) и так далее.

В результате таких изменений мы добавили в приложение поддержку ASP.NET Core Identity с настройками по умолчанию. Теперь мы можем использовать ASP.NET Core Identity для работы с учётными данными пользователей.

Добавление контроллера для управления данными пользователей

Для начала создадим в папке Models/Identity новый класс, объекты которого будут использоваться для работы с учётными записями пользователей:

public class UserDto
{
    [Required(ErrorMessage = "Поле UserName является обязательным")]
    public string UserName { get; set; }

    [Required(ErrorMessage = "Поле Password является обязательным")]
    public string Password { get; set; }
    [Compare("Password", ErrorMessage ="Пароли не совпадают")]
    public string ConfirmPassword { get; set; }

    [Required(ErrorMessage = "Поле Email является обязательным")]
    public string Email { get; set; }

    [Required(ErrorMessage = "Поле FirstName является обязательным")]
    public string FirstName { get; set; }

    [Required(ErrorMessage = "Поле LastName является обязательным")]
    public string LastName { get; set; }

    public string PhoneNumber { get; set; }
}

В этом классе мы определили необходимые атрибуты валидации для свойств. Теперь добавим в папку Controllers новый контроллер:

[Route("[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
    private readonly UserManager<User> _userManager;

    public UsersController(UserManager<User> userManager)
    {
        _userManager = userManager;
    }

    [HttpPost]
    public async Task<ActionResult> AddUser(UserDto user)
    {
        var data = await _userManager.FindByEmailAsync(user.Email);
        if (data != null) 
        {
            ModelState.AddModelError("User", $"Пользователь с Email {user.Email} уже зарегистрирован в приложении");
            return ValidationProblem(statusCode: 400, modelStateDictionary: ModelState );
        }

        var userForReg = new User() 
        { 
            Email = user.Email,
            FirstName = user.FirstName,
            LastName = user.LastName,
            PhoneNumber = user.PhoneNumber,
            UserName = user.UserName,
        };

        var result = await _userManager.CreateAsync(userForReg, user.Password);
        if (result.Succeeded)
        {
            return Created();
        }
        else
        {
            foreach (var item in result.Errors)
            {
                ModelState.AddModelError(item.Code, item.Description);
            }

            return ValidationProblem(statusCode: 400, modelStateDictionary: ModelState);
        }

    }
}

В этом контроллере мы определили пока всего одно действие – AddUser() с помощью которого мы будем регистрировать новых пользователей в нашем приложении. Для управления учетными данными пользователей мы не используем напрямую контекст базы данных, а запрашиваем сервис ASP.NET Core Identity – UserManager<User>

public UsersController(UserManager<User> userManager)
{
    _userManager = userManager;
}

Этот сервис содержит необходимые методы для управления пользователями. Теперь рассмотрим действие AddUser(). В качестве параметра это действие получает объект класса UserDto. Вначале мы проверяем имеется ли в хранилище пользователь с заданным Email и, если имеется, то отправляем пользователю сообщение об ошибке, используя метод ValidationProblem():

var data = await _userManager.FindByEmailAsync(user.Email);
if (data != null) 
{
    ModelState.AddModelError("User", $"Пользователь с Email {user.Email} уже зарегистрирован в приложении");
    return ValidationProblem(statusCode: 400, modelStateDictionary: ModelState );
}

Если проверка прошла успешно, то мы создаем новый объект типа User 

var userForReg = new User() 
{ 
    Email = user.Email,
    FirstName = user.FirstName,
    LastName = user.LastName,
    PhoneNumber = user.PhoneNumber,
    UserName = user.UserName,
};

и вместе с паролем передаем новый объект в метод CreateAsync() менеджера пользователей:

var result = await _userManager.CreateAsync(userForReg, user.Password);

Метод CreateAsync() создает в хранилище запись о новом пользователе. При этом от нас не требуется самостоятельно генерировать хэш-представление пароля – это сделается автоматически. В итоге, метод вернет нам результат в виде объекта класса IdentityResult. При этом, при регистрации нового пользователя могут произойти различные проблемы, начиная от несоответствия пароля заданным требованиям и, заканчивая тем, что в базе данных может оказаться пользователь с тем же именем (UserName). Поэтому мы проверяем свойство Succeeded объекта IdentityResult и, если оно равно false (регистрация прошла с ошибкой), то добавляем в ModelState все возникшие при регистрации пользователя ошибки и снова возвращаем пользователю сообщение с кодом 400 Bad Request:

if (result.Succeeded)
{
    return Created();
}
else
{
    foreach (var item in result.Errors)
    {
        ModelState.AddModelError(item.Code, item.Description);
    }

    return ValidationProblem(statusCode: 400, modelStateDictionary: ModelState);
}

В случае успешной регистрации действие вернет код 201 Created с пустым телом ответа. Проверим работу этого контроллера, добавив в http-файл проекта новый запрос:

POST {{WebApplication6_HostAddress}}/users/
Content-Type: application/json

{
  "UserName": "Vlad",
  "Email": "vlad@csharp.webdelphi.ru",
  "FirstName": "Vlad",
  "LastName": "Ivanov",
  "Password": "_12345q67890",
  "ConfirmPassword":"_12345q67890",
  "PhoneNumber": "1234"
}

Ответ сервера при успешной регистрации пользователя:

Если в процессе регистрации произойдет какая-либо ошибка, то мы увидим сообщение следующего вида:

Что касается базы данных SQLite для хранения данных о пользователях, то в папке с проектом появится файл IdentityBase.db

Можете открыть его с помощью любого приложения для управления БД SQLite и убедиться, что новый пользователь был действительно зарегистрирован (запись будет содержаться в таблице AspNetUsers:

Также в базе данных будут содержаться другие таблицы для работы с ASP.NET Core Identity — для хранения ролей пользователей, логинов и так далее и с некоторыми из этих таблиц мы познакомимся далее.

Итого

В этой части мы познакомились в общих чертах с ASP.NET Core Identity и подготовили приложение в котором мы можем зарегистрировать нового пользователя. Естественно, что на этом вопросы аутентификации и авторизации пользователей в приложениях ASP.NET Core Web API не заканчиваются, однако, для дальнейшего рассмотрения этих вопросов нам необходимо будет немного разобраться с тем, как именно можно авторизовать пользователя в приложении ASP.NET Core Web API.

Подписаться
Уведомить о
guest
0 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии