Пользователи и Identity
У курса есть ID, название и число уроков. У редактора есть учётная запись, способ входа и права. Эти две модели связаны, но решают разные задачи. Если добавить пароль прямо в CourseEntity, приложение смешает хранение учебного материала с управлением доступом. В этом уроке разделим аккаунт и доменные данные, определим роль ASP.NET Core Identity и подготовим договорённость для следующих уроков.
Материал продолжает предметную модель урока 16 и идеи версии из урока 19, но не подключает готовое хранилище Identity. В lesson-20 архива продолжения находятся полные модели, схема границы и таблица сценариев. Регистрация, база аккаунтов, отправка писем и вход здесь отсутствуют. Это осознанный урок проектирования: его результат — согласованная модель, а не работающая система пользователей. Программы и проверки при подготовке не выполнялись.
Учётная запись отвечает на отдельные вопросы
ASP.NET Core Identity предоставляет компоненты управления пользователями, паролями, ролями, claims, подтверждением адреса и связанными токенами. Он не определяет автоматически, какой курс разрешено менять конкретному редактору. Правило собственности остаётся частью приложения, даже если вход полностью обслуживает стандартная библиотека.
В учебном решении аккаунт имеет непрозрачный ID. Человек может изменить адрес почты или отображаемое имя, но связь курса с владельцем не должна из-за этого потеряться. Поэтому доменная модель сохраняет стабильный OwnerUserId, а не email, имя или текст роли. Пароль и его hash наружу в курс не попадают.
Модель будущего аккаунта может расширять стандартный тип:
using Microsoft.AspNetCore.Identity;
namespace CatalogBoundary;
public sealed class CatalogUser : IdentityUser
{
public string DisplayName { get; set; } = "";
}
public sealed record OwnedCourse(
string Id, string OwnerUserId, string Title, long Version);
Это полный набор типов примера, но он не регистрирует Identity в DI и не создаёт таблицы. Наследование от IdentityUser само по себе не заставляет UserManager сохранять пользователя. Для этого нужен store, выбранная схема данных и согласованная регистрация сервисов. Архив специально не изображает этот отсутствующий слой выполненным.
Рассмотрим два аккаунта: редактор editor-01 и редактор editor-02. JavaScript принадлежит первому, Markdown — второму. Названия курсов остаются знакомыми. Если первый переименует своё отображаемое имя, JavaScript всё ещё принадлежит editor-01. Если второй присвоит своему имени текст editor-01, право на JavaScript не изменится: совпадение текста не является идентичностью.
Домен хранит ссылку, а не механизм входа
Вариант предметной схемы описан отдельно:
CREATE TABLE course_owners (
course_id text PRIMARY KEY REFERENCES courses(id),
owner_user_id text NOT NULL
);
Это проектный фрагмент, а не миграция, уже применённая к базе. Внешний ключ здесь гарантирует существование курса. Для аккаунта ключ пока не задан, потому что схема и provider Identity ещё не выбраны. Нельзя утверждать, что база проверяет существование owner_user_id, когда такого ограничения в SQL нет. После выбора общего хранилища потребуется соответствующая связь либо явно контролируемая прикладная граница.
Существование пользователя и право редактирования тоже различаются. Заблокированный аккаунт может оставаться владельцем курса в истории, но больше не получать активный вход. Удаление пользователя не должно автоматически удалять учебные URL. Нужно выбрать передачу собственности, архивирование профиля или системного владельца. Без этого случайная каскадная операция способна разрушить каталог.
Не добавляйте владельца из входного JSON без проверки. Если сервер разрешит создать курс с любым ownerUserId, клиент назначит себе или другому человеку права произвольно. Для обычного создания владелец берётся из удостоверенной идентичности текущего пользователя. Административное переназначение становится отдельной привилегированной командой с собственным аудитом.
Store и managers имеют разные обязанности
UserManager выполняет операции над аккаунтом через store и проверяет согласованные правила. SignInManager участвует в процессе входа. Хранилище отвечает за данные и нужные интерфейсы, а не заменяется словарём с заранее известными именами. Для учебной модели важно видеть этот путь, прежде чем копировать большую регистрацию сервисов из шаблона.
Существующий Npgsql repository курсов не является store Identity. Он умеет читать и изменять курс, но не реализует управление паролями, security stamp, lockout и другими свойствами аккаунта. Вариант с Entity Framework потребует отдельного provider и версий пакетов; вариант с собственным store — полного согласованного набора интерфейсов. В этой серии ни один из них не выбран молча за читателя.
Пароль не сохраняется обычным текстом и не сравнивается оператором равенства с колонкой. Стандартный password hasher хранит представление, позволяющее проверить ввод, а не восстановить исходный пароль. Однако использование hasher отдельно ещё не даёт всю систему Identity: остаются блокировки, восстановление, подтверждение, аудит и обновление хеша. Не объявляйте набор из одного hash готовой защитой аккаунтов.
В следующем уроке будет маленькая контролируемая HTTPS-песочница с одним учебным аккаунтом. Она покажет именно протокол cookies и проверку учётных данных, не выдавая себя за подключённый Identity store. Такое разделение помогает изучить механизм входа независимо от выбора долговременного хранилища. Для реального приложения этот лабораторный источник личности нужно заменить завершённым решением аккаунтов.
Claims не являются редактируемой анкетой
После успешного входа сервер формирует claims — утверждения о пользователе, которыми пользуется обработка запроса. ID должен приходить от удостоверенного источника, а не из произвольного заголовка браузера. Право course:edit может обозначать допустимый тип действия, но не доказывает владение конкретным JavaScript. Следующий ресурсный шаг сравнит claim ID с владельцем найденного курса.
Claims внутри защищённого cookie могут устаревать. Если редактор потерял право, ранее выданный билет сам по себе не обязательно исчезает немедленно. Полная реализация должна определить срок жизни, переоценку аккаунта и отзыв сессий. Security stamp и проверка principal помогают стандартной интеграции, но их наличие зависит от реально подключённого механизма, а не от слова Identity в названии статьи.
Для будущей проверки модели перечислим ожидаемые случаи. Анонимный запрос не имеет удостоверенного владельца. editor-01 совпадает с владельцем JavaScript, editor-02 — не совпадает. Изменение отображаемого имени не меняет эту пару. Входной ownerUserId, даже совпадающий с разрешённым именем, не должен участвовать в принятии права обычного редактора.
Результат урока можно оценить без регистрации настоящих людей: проследить источник каждого ID и поле, где оно хранится. Если путь от браузерного ввода к разрешению проходит напрямую, граница нарушена. Мы получили модель для дальнейшей аутентификации и политики; полноценное внедрение Identity потребует выбранного store и отдельного этапа исполнения.
Возможности и компоненты платформы описаны в введении в ASP.NET Core Identity 10, разграничение ресурса и пользователя — в документации ресурсной авторизации.