Перейти к основному содержимому

Identity — JWT, cookie и MVC

Разработчику

Play ITЗагрузка интерактивного демо…


Два трека на одной Identity

ТрекКлиентСхемаРаздел
ASPA, mobile, другой сервисJWT Bearerдо раздела MVC и cookie
BБраузер, Razor, MVCCookieMVC и 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"Представились, но прав нет".

Что получится

EndpointAuthДействие
POST /registerНетСоздать пользователя
POST /loginНетПолучить JWT
GET /api/notesBearer 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() на неверные данные не раскрывает, где именно ошибка, и улучшает безопасность.
МетодЛогика
RegisterCreateAsync сохраняет пользователя; пароль попадает в БД только как хеш.
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 и маршруты настроены согласованно.
  1. POST /register{ "email": "a@test.com", "password": "Passw0rd!" }.
  2. POST /login — скопируйте token.
  3. Swagger → Authorize → введите Bearer <токен>.
  4. GET /api/notes200.

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 и внешних клиентов.

В 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-секреты.
Seed только для разработки

В продакшене роли и учётки заводят через миграции данных, админ-панель или 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, RazorCookie (default)AddDefaultIdentity + UseAuthentication
/api/*JWTAddAuthentication().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 (Identity)JWT Bearer
Где "сессия"Зашифрованное cookie в браузереТокен у клиента
КлиентRazor Pages, MVCSPA, mobile, сервисы
CSRFAnti-forgery на POST-формахBearer + HTTPS, короткий TTL
Быстрый старт HTMLdotnet 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
IDX10503Issuer/Audience/Key при проверке не совпадают с генерацией
Браузер "не логинится" JWTДля HTML нужны cookies, не только API-токен
Пароль отклонёнPasswordOptions (длина, цифры, регистр)

Что попробовать

  1. Сократите ExpireMinutes и убедитесь, что после часа приходит 401.
  2. Храните заметки с UserId из токена.
  3. Защитите Razor Pages: [Authorize] на папке Notes.

Дальше


Основа по протоколу

Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.