Identity — JWT, cookie и MVC
Play ITЗагрузка интерактивного демо…
Identity — JWT, cookie и MVC
Два трека на одной Identity
| Трек | Клиент | Схема | Раздел |
|---|---|---|---|
| A | SPA, mobile, другой сервис | JWT Bearer | до раздела MVC и cookie |
| B | Браузер, Razor, MVC | Cookie | MVC и cookie Identity |
Сначала доведите один трек до рабочего входа, затем роли и политики.
- Трек A: регистрация →
loginотдаёт JWT →[Authorize]на контроллере. - Трек B: шаблон Individual →
/Identity/Account/Login→[Authorize(Roles = "...")]на админке.
Сквозной магазин — в обзоре ASP.NET; здесь минимальный каркас B.
Identity и JWT — аутентификация API
Открытый API из Первая программа на ASP.NET Core отвечает всем подряд. Реальное приложение разделяет пользователей — у каждого свои заметки, роли администратора, запрет гостям на запись.
| Вопрос | Термин |
|---|---|
| Кто вы? | Аутентификация (authentication) |
| Что вам можно? | Авторизация (authorization) |
ASP.NET Core Identity — готовые таблицы пользователей, хеш паролей, сброс пароля, роли. Для браузера часто используют cookie (зашифрованная "пропускная" в cookie). Для мобильного приложения, SPA и других сервисов — JWT (JSON Web Token): строка в заголовке Authorization: Bearer ....
Здесь соберём Web API — регистрация, вход, защищённый GET /api/notes. База — SQLite + EF Core (Entity Framework Core — первая программа). HTML-формы: Razor Pages — первая программа. Обзор безопасности: Безопасность приложений на C#.
Словарь
| Термин | Простыми словами |
|---|---|
| Identity | Подсистема пользователей, паролей, ролей в ASP.NET Core. |
| JWT | Подписанный JSON с claims (id, email, роли); клиент хранит и шлёт с каждым запросом. |
| Claim | Пара "тип — значение" внутри токена (sub, email, role). |
| Bearer | Схема HTTP: "предъявитель токена" в заголовке Authorization. |
| UserManager | Сервис создания пользователя, проверки пароля. |
| 401 Unauthorized | "Не представились" — нет или плохой токен. |
| 403 Forbidden | "Представились, но прав нет". |
Что получится
| Endpoint | Auth | Действие |
|---|---|---|
POST /register | Нет | Создать пользователя |
POST /login | Нет | Получить JWT |
GET /api/notes | Bearer JWT | Список (только вошедшие) |
Разбор:
- Сценарий показывает полный путь пользователя от регистрации до доступа к защищённому API.
POST /registerсоздаёт запись пользователя в таблицах Identity.POST /loginпроверяет пароль и возвращает подписанный JWT.- Клиент добавляет токен в
Authorization: Bearer ...и вызывает закрытый endpoint. - При валидном токене API возвращает
200, иначе middleware завершает запрос с401.
Требования
- .NET SDK 8+
- Swagger в Dev — удобно нажимать "Authorize"
Проект и пакеты
dotnet new webapi -n SecureNotesApi -o SecureNotesApi
cd SecureNotesApi
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
Разбор:
dotnet new webapiсоздаёт базовый HTTP API-проект сProgram.csи контроллерами.- Пакет
Identity.EntityFrameworkCoreдобавляет таблицы пользователей, ролей и сервисыUserManager. EntityFrameworkCore.Sqliteподключает SQLite как простую локальную БД для практики.EntityFrameworkCore.Designнужен для миграций черезdotnet ef.JwtBearerдобавляет middleware, которое читает и проверяет токены из заголовкаAuthorization.
Удалите sample WeatherForecast — оставьте только наш код.
Пользователь и контекст
Models/ApplicationUser.cs:
using Microsoft.AspNetCore.Identity;
namespace SecureNotesApi.Models;
public class ApplicationUser : IdentityUser
{
// Сюда позже: DisplayName, AvatarUrl
}
Разбор:
ApplicationUser : IdentityUserрасширяет стандартную модель пользователя Identity.- Базовый класс уже содержит поля
Id,Email,UserName,PasswordHashи служебные флаги. - Такой наследник нужен, чтобы позже безопасно добавить бизнес-поля профиля.
- В текущем состоянии класс минимальный, но архитектурно готов к расширению.
IdentityUser уже содержит Id, Email, UserName, хеш пароля и т.д.
Data/AppIdentityDbContext.cs:
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using SecureNotesApi.Models;
namespace SecureNotesApi.Data;
public class AppIdentityDbContext : IdentityDbContext<ApplicationUser>
{
public AppIdentityDbContext(DbContextOptions<AppIdentityDbContext> options)
: base(options) { }
}
Разбор:
IdentityDbContext<ApplicationUser>автоматически включает набор сущностей и таблиц Identity.- Конструктор принимает
DbContextOptions, которые приходят из DI при старте приложения. - Наследование от готового контекста избавляет от ручного описания
AspNetUsers,AspNetRolesи связей. - Контекст становится единой точкой подключения EF Core к БД для auth-функциональности.
Миграции:
dotnet ef migrations add IdentityInit
dotnet ef database update
Разбор:
migrations addфиксирует текущее состояние модели в C#-миграции.- Имя
IdentityInitпомогает понять, что это начальная схема Identity. database updateприменяет миграцию к реальной SQLite-базе.- После выполнения в БД появляются физические таблицы пользователей, ролей и токенов.
Появятся identity.db и таблицы AspNetUsers, AspNetRoles, связи пользователь–роль.
Настройка JWT в appsettings.json
{
"ConnectionStrings": {
"Default": "Data Source=identity.db"
},
"Jwt": {
"Key": "DEV_ONLY_ChangeMe_To_LongRandomSecret_Key_AtLeast32Chars!",
"Issuer": "SecureNotesApi",
"Audience": "SecureNotesApi",
"ExpireMinutes": 60
}
}
Разбор:
- Секция
ConnectionStringsхранит строку подключения к локальной SQLite-базе. - Блок
Jwtзадаёт параметры подписи и проверки токена в middleware. Keyиспользуется как секрет HMAC при выпуске и валидации JWT.IssuerиAudienceзащищают от принятия токенов, выпущенных другим сервисом.ExpireMinutesограничивает срок жизни access-токена и снижает окно риска при утечке.
| Поле | Смысл |
|---|---|
Key | Секрет подписи HMAC; в продакшене — длинная случайная строка из переменных окружения. |
Issuer / Audience | Кто выдал токен и для кого; при проверке должны совпасть. |
ExpireMinutes | Срок жизни токена; после истечения — снова login. |
Ключ Jwt:Key храните в User Secrets, переменных окружения или Key Vault. Реальный секрет в git не коммитьте.
Program.cs — Identity + Bearer
Код ITЗагрузка примера кода…
Разбор:
- Код настраивает сразу три контура: БД Identity, аутентификацию JWT и авторизацию endpoint'ов.
AddIdentityзадаёт парольную политику и подключает хранилище пользователей через EF Core.AddAuthentication().AddJwtBearer(...)включает проверку подписи, срока и issuer/audience у токена.AddAuthorization()активирует обработку[Authorize]и политик доступа.- В пайплайне
UseAuthentication()идёт раньшеUseAuthorization(), чтобы сначала построитьHttpContext.User. MapControllers()публикует маршруты контроллеров, включаяAuthControllerи защищённые API.
Порядок middleware: сначала UseAuthentication (разобрать токен, заполнить HttpContext.User), затем UseAuthorization (проверить [Authorize]), затем контроллеры.
AddJwtBearer настраивает проверку — подпись, срок, issuer/audience. При ошибке — 401.
DTO и контроллер аутентификации
Contracts/AuthContracts.cs:
namespace SecureNotesApi.Contracts;
public record RegisterRequest(string Email, string Password);
public record LoginRequest(string Email, string Password);
public record AuthResponse(string Token, DateTime ExpiresAt);
Разбор:
recordсоздаёт компактные immutable DTO для входа и выхода API.RegisterRequestиLoginRequestописывают контракт тела запроса для auth-методов.AuthResponseвозвращает клиенту сам токен и момент истечения для UX-логики обновления.- Выделение DTO в
Contractsуменьшает связность между web-слоем и внутренними моделями.
Controllers/AuthController.cs:
Код ITЗагрузка примера кода…
Разбор:
Registerсоздаёт пользователя черезUserManager, который сам хеширует пароль и пишет данные в Identity-таблицы.Loginпроверяет email/пароль и при успехе формирует ответ с JWT.CreateTokenAsyncсобирает claims (NameIdentifier,Email,Role) и подписывает токенHmacSha256.JwtSecurityTokenHandler().WriteToken(token)сериализует объект токена в строку для HTTP-ответа.- Возврат
Unauthorized()на неверные данные не раскрывает, где именно ошибка, и улучшает безопасность.
| Метод | Логика |
|---|---|
Register | CreateAsync сохраняет пользователя; пароль попадает в БД только как хеш. |
Login | При неверном email/пароле — 401 Unauthorized без подсказки, какой именно поле неверно (безопаснее). |
CreateTokenAsync | Собирает JWT: claims + подпись тем же Key, что в AddJwtBearer. |
Клиент сохраняет Token и шлёт:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Разбор:
- Заголовок
Authorizationпередаёт токен в стандартном формате Bearer-аутентификации. - Ключевое слово
Bearerсообщает middleware, какую схему проверки применять. - После пробела размещается строка JWT из
login, включая подпись. - Токен отправляется в каждом защищённом запросе, пока не истечёт
exp.
Слово Bearer и пробел обязательны.
Защищённый API
Код ITЗагрузка примера кода…
Разбор:
- Атрибут
[Authorize]блокирует весь контроллер для неаутентифицированных запросов. GETвозвращает текущий набор заметок, если middleware успешно валидировал токен.POSTпринимает текст из body и добавляет его в память.- Проверка токена происходит до входа в метод, поэтому бизнес-код работает только с доверенным пользователем.
[Authorize] — без валидного JWT middleware вернёт 401 до входа в метод.
В продакшене список заметок привяжите к User.FindFirstValue(ClaimTypes.NameIdentifier) — у каждого пользователя свой список.
Мини-пример доступа к идентификатору пользователя
using System.Security.Claims;
[HttpPost]
public IActionResult Add([FromBody] string text)
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (string.IsNullOrEmpty(userId)) return Unauthorized();
// Сохранение заметки вместе с userId
return Ok(new { owner = userId, text });
}
Разбор:
User.FindFirstValue(ClaimTypes.NameIdentifier)извлекает ID пользователя из claims JWT.- Проверка
string.IsNullOrEmpty(userId)страхует код от некорректного auth-контекста. - Значение
userIdдальше используют как внешний ключ владельца данных. - Такой приём переводит пример из "общих данных" в многопользовательскую модель с изоляцией записей.
Этот шаг переводит пример от "общий список" к реальной многопользовательской логике.
Проверка через Swagger
dotnet run
Разбор:
- Команда запускает API локально для ручной проверки auth-сценариев.
- После старта можно открыть Swagger UI и пройти цепочку register/login/secured call.
- Тест в рантайме подтверждает, что middleware и маршруты настроены согласованно.
POST /register—{ "email": "a@test.com", "password": "Passw0rd!" }.POST /login— скопируйтеtoken.- Swagger → Authorize → введите
Bearer <токен>. GET /api/notes→ 200.
curl:
curl -k -X POST https://localhost:7xxx/login \
-H "Content-Type: application/json" \
-d '{"email":"a@test.com","password":"Passw0rd!"}'
curl -k https://localhost:7xxx/api/notes \
-H "Authorization: Bearer <TOKEN>"
Разбор:
- Первый
curlотправляет JSON вloginи получает токен в ответе. - Флаг
-H "Content-Type: application/json"обязателен для корректного разбора тела. - Второй
curlпоказывает защищённый вызов с заголовкомAuthorization. - Пара команд удобна для smoke-проверки без Swagger и внешних клиентов.
MVC и cookie Identity
В server-rendered MVC браузер после входа хранит аутентификационное cookie. Сервер на каждом запросе расшифровывает его, восстанавливает ClaimsPrincipal в HttpContext.User и проверяет [Authorize]. Отдельный JWT в JavaScript для каталога товаров не нужен.
Типичный сценарий интернет-магазина:
- публичный каталог;
- корзина для всех;
- админка только для роли Administrator.
Проект с готовой Identity UI
dotnet new mvc -n StoreMvc -o StoreMvc --auth Individual
cd StoreMvc
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Design
Разбор:
--auth Individualдобавляет Razor Pages/Identity/Account/*,ApplicationDbContextи cookie-схему по умолчанию.- MVC-контроллеры и представления лежат рядом с Identity в одном
Program.cs. - SQLite удобен для локальной практики; в продакшене — PostgreSQL или SQL Server.
В appsettings.json:
{
"ConnectionStrings": {
"DefaultConnection": "Data Source=app.db"
}
}
Миграции:
dotnet ef migrations add Init
dotnet ef database update
Роли при старте приложения
Создайте роль и первого администратора один раз при запуске (распространённый seed при старте):
Код ITЗагрузка примера кода…
Разбор:
AddRoles<IdentityRole>()включает таблицы ролей и связь пользователь–роль.- Seed в
CreateScopeвыполняется доapp.Run()и не блокирует обработку запросов в продакшене, если вынести в отдельный hosted service. - Пароль админа в примере только для dev; в репозиторий не коммитьте production-секреты.
В продакшене роли и учётки заводят через миграции данных, админ-панель или IaC. Жёстко прошитый пароль в коде — утечка при публикации репозитория.
Зона Admin через Areas
Areas/Admin/Controllers/HomeController.cs:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace StoreMvc.Areas.Admin.Controllers;
[Area("Admin")]
[Authorize(Roles = "Administrator")]
public class HomeController : Controller
{
public IActionResult Index() => View();
}
Areas/Admin/Views/Home/Index.cshtml:
@{
ViewData["Title"] = "Администрирование";
}
<h1>Заказы и каталог</h1>
<p>Доступ только для роли Administrator.</p>
Ссылка из публичного layout (видна после входа):
@if (User.IsInRole("Administrator"))
{
<a asp-area="Admin" asp-controller="Home" asp-action="Index">Админка</a>
}
Разбор:
[Area("Admin")]отделяет URL вида/Admin/Home/Indexот публичных контроллеров.[Authorize(Roles = "Administrator")]возвращает 403, если cookie есть, но роли нет; 401 — если пользователь не вошёл.asp-areaв Tag Helper строит корректный маршрут без ручной склейки строк.
Формы входа и CSRF
Страницы /Identity/Account/Login и Register генерируются шаблоном. Свои POST-формы в MVC защищайте токеном:
<form asp-controller="Cart" asp-action="Add" method="post">
@Html.AntiForgeryToken()
<input type="hidden" name="productId" value="@Model.Id" />
<button type="submit">В корзину</button>
</form>
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Add(int productId) { /* ... */ }
Разбор:
- Cookie-аутентификация уязвима к CSRF: чужой сайт может отправить запрос от имени залогиненного браузера.
ValidateAntiForgeryTokenсверяет скрытое поле формы с cookie antiforgery.- Для API с JWT CSRF на Bearer обычно не распространяется; там важны HTTPS и короткий TTL токена.
Гибрид — один Identity, два входа
Один проект может обслуживать и браузер, и API:
| Часть | Схема | Настройка |
|---|---|---|
| MVC, Razor | Cookie (default) | AddDefaultIdentity + UseAuthentication |
/api/* | JWT | AddAuthentication().AddJwtBearer(...) как в треке A |
На API-контроллерах укажите схему явно: [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]. Пользователь из cookie и клиент с Bearer используют одни таблицы AspNetUsers — удобно для SPA + существующего MVC.
Внешний вход
Внешний вход через Google или Microsoft настраивают через AddAuthentication().AddGoogle(...). Поток тот же: провайдер подтверждает email, Identity создаёт или связывает локального пользователя, дальше работает cookie. Детали — документация Identity и Документация и практика ASP.NET (Microsoft Learn).
Cookie и JWT — когда что
| Cookie (Identity) | JWT Bearer | |
|---|---|---|
| Где "сессия" | Зашифрованное cookie в браузере | Токен у клиента |
| Клиент | Razor Pages, MVC | SPA, mobile, сервисы |
| CSRF | Anti-forgery на POST-формах | Bearer + HTTPS, короткий TTL |
| Быстрый старт HTML | dotnet new mvc -au Individual | трек A в этой статье |
| Админка, корзина | Естественный выбор | API + отдельный фронт |
Razor и MVC с формами — cookie и /Identity/Account/Login. Чистый JSON API без браузера — JWT из трека A.
Роли и политики
await roleManager.CreateAsync(new IdentityRole("Admin"));
await userManager.AddToRoleAsync(user, "Admin");
Разбор:
CreateAsync(new IdentityRole("Admin"))добавляет роль в таблицу ролей Identity.AddToRoleAsyncсвязывает конкретного пользователя с этой ролью.- После привязки роль попадает в claims и может участвовать в проверке доступа.
- Обычно эти операции выполняют в seed-скрипте или админском bootstrap-процессе.
[Authorize(Roles = "Administrator")]
public class OrdersController : Controller { }
Разбор:
- Атрибут ограничивает доступ только пользователям с указанной ролью в cookie.
- Проверка происходит на этапе авторизации до вызова метода контроллера.
- Если токен валиден, но роли нет, клиент получает
403 Forbidden. - Подход удобен для быстрых role-based ограничений на endpoint или контроллер.
Политики по claim — Безопасность приложений на C#.
Практика для production
| Задача | Базовый шаг |
|---|---|
| Секреты | Хранить Jwt:Key вне репозитория |
| Отзыв токенов | Короткий TTL + refresh token |
| Разделение прав | Политики по claim и ролям |
| Аудит | Логи входов, неудачных попыток и блокировок |
Вместе с интеграционными тестами из Тесты ASP.NET Core — юнит и интеграция это даёт надёжный базовый контур безопасности.
Частые ошибки
| Симптом | Решение |
|---|---|
| 401 всегда | Нет UseAuthentication; в Swagger забыли Bearer ; токен истёк |
| 500 при login | Нет миграции БД; пустой Jwt:Key |
IDX10503 | Issuer/Audience/Key при проверке не совпадают с генерацией |
| Браузер "не логинится" JWT | Для HTML нужны cookies, не только API-токен |
| Пароль отклонён | PasswordOptions (длина, цифры, регистр) |
Что попробовать
- Сократите
ExpireMinutesи убедитесь, что после часа приходит 401. - Храните заметки с
UserIdиз токена. - Защитите Razor Pages:
[Authorize]на папкеNotes.
Дальше
- Юнит- и интеграционные тесты · Minimal API и OpenAPI
- Razor Pages · Безопасность приложений · ASP.NET — обзор
- Web API — первая программа · EF Core
Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.