Identity и JWT — практика на ASP.NET Core
Identity и JWT — практика
Identity и JWT - аутентификация API
Открытый API из 4511 отвечает всем подряд. Реальное приложение разделяет пользователей: у каждого свои заметки, роли администратора, запрет гостям на запись.
| Вопрос | Термин |
|---|---|
| Кто вы? | Аутентификация (authentication) |
| Что вам можно? | Авторизация (authorization) |
ASP.NET Core Identity — готовые таблицы пользователей, хеш паролей, сброс пароля, роли. Для браузера часто используют cookie (зашифрованная «пропускная» в cookie). Для мобильного приложения, SPA и других сервисов — JWT (JSON Web Token): строка в заголовке Authorization: Bearer ....
Здесь соберём Web API: регистрация, вход, защищённый GET /api/notes. База — SQLite + EF Core (441). HTML-формы: 4514. Обзор безопасности: 43.
Словарь
| Термин | Простыми словами |
|---|---|
| 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 | Список (только вошедшие) |
Требования
- .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
Удалите sample WeatherForecast — оставьте только наш код.
Пользователь и контекст
Models/ApplicationUser.cs:
using Microsoft.AspNetCore.Identity;
namespace SecureNotesApi.Models;
public class ApplicationUser : IdentityUser
{
// Сюда позже: DisplayName, AvatarUrl
}
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) { }
}
Миграции:
dotnet ef migrations add IdentityInit
dotnet ef database update
Появятся 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
}
}
| Поле | Смысл |
|---|---|
Key | Секрет подписи HMAC; в продакшене — длинная случайная строка из переменных окружения. |
Issuer / Audience | Кто выдал токен и для кого; при проверке должны совпасть. |
ExpireMinutes | Срок жизни токена; после истечения — снова login. |
:::warning Секрет в продакшене
Ключ Jwt:Key храните в User Secrets, переменных окружения или Key Vault. Реальный секрет в git не коммитьте.
:::
Program.cs — Identity + Bearer
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using SecureNotesApi.Data;
using SecureNotesApi.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppIdentityDbContext>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("Default")));
builder.Services
.AddIdentity<ApplicationUser, IdentityRole>(opt =>
{
opt.Password.RequiredLength = 8;
opt.User.RequireUniqueEmail = true;
})
.AddEntityFrameworkStores<AppIdentityDbContext>()
.AddDefaultTokenProviders();
var jwt = builder.Configuration.GetSection("Jwt");
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwt["Key"]!));
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwt["Issuer"],
ValidAudience = jwt["Audience"],
IssuerSigningKey = key
};
});
builder.Services.AddAuthorization();
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Порядок 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);
Controllers/AuthController.cs:
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using SecureNotesApi.Contracts;
using SecureNotesApi.Models;
namespace SecureNotesApi.Controllers;
[ApiController]
[Route("[controller]")]
public class AuthController : ControllerBase
{
private readonly UserManager<ApplicationUser> _users;
private readonly IConfiguration _config;
public AuthController(UserManager<ApplicationUser> users, IConfiguration config)
{
_users = users;
_config = config;
}
[HttpPost("register")]
public async Task<IActionResult> Register(RegisterRequest req)
{
var user = new ApplicationUser { UserName = req.Email, Email = req.Email };
var result = await _users.CreateAsync(user, req.Password);
if (!result.Succeeded)
return BadRequest(result.Errors);
return Ok(new { message = "Пользователь создан" });
}
[HttpPost("login")]
public async Task<ActionResult<AuthResponse>> Login(LoginRequest req)
{
var user = await _users.FindByEmailAsync(req.Email);
if (user is null || !await _users.CheckPasswordAsync(user, req.Password))
return Unauthorized();
var token = await CreateTokenAsync(user);
var jwt = _config.GetSection("Jwt");
var expire = DateTime.UtcNow.AddMinutes(int.Parse(jwt["ExpireMinutes"]!));
return new AuthResponse(token, expire);
}
private async Task<string> CreateTokenAsync(ApplicationUser user)
{
var jwt = _config.GetSection("Jwt");
var roles = await _users.GetRolesAsync(user);
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, user.Id),
new(ClaimTypes.Email, user.Email ?? user.UserName ?? "")
};
claims.AddRange(roles.Select(r => new Claim(ClaimTypes.Role, r)));
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwt["Key"]!));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: jwt["Issuer"],
audience: jwt["Audience"],
claims: claims,
expires: DateTime.UtcNow.AddMinutes(int.Parse(jwt["ExpireMinutes"]!)),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
| Метод | Логика |
|---|---|
Register | CreateAsync сохраняет пользователя; пароль попадает в БД только как хеш. |
Login | При неверном email/пароле — 401 Unauthorized без подсказки, какой именно поле неверно (безопаснее). |
CreateTokenAsync | Собирает JWT: claims + подпись тем же Key, что в AddJwtBearer. |
Клиент сохраняет Token и шлёт:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Слово Bearer и пробел обязательны.
Защищённый API
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class NotesController : ControllerBase
{
private static readonly List<string> Notes = new();
[HttpGet]
public IActionResult GetAll() => Ok(Notes);
[HttpPost]
public IActionResult Add([FromBody] string text)
{
Notes.Add(text);
return Ok();
}
}
[Authorize] — без валидного JWT middleware вернёт 401 до входа в метод.
В продакшене список заметок привяжите к User.FindFirstValue(ClaimTypes.NameIdentifier) — у каждого пользователя свой список.
Проверка через Swagger
dotnet run
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>"
Cookie и JWT — когда что
| Cookie (Identity) | JWT Bearer | |
|---|---|---|
| Где «сессия» | Зашифрованное cookie в браузере | Токен у клиента |
| Клиент | Razor Pages, MVC | SPA, mobile, сервисы |
| CSRF | Нужен anti-forgery на формах | Bearer + HTTPS, короткий TTL |
| Быстрый старт HTML | dotnet new webapp -au Individual | Эта статья |
Razor с формами логина — cookie и страницы /Identity/Account/Login. JWT — для API без браузера.
Роли и политики (кратко)
await roleManager.CreateAsync(new IdentityRole("Admin"));
await userManager.AddToRoleAsync(user, "Admin");
[Authorize(Roles = "Admin")]
Политики по claim — 43.
Частые ошибки
| Симптом | Решение |
|---|---|
| 401 всегда | Нет UseAuthentication; в Swagger забыли Bearer ; токен истёк |
| 500 при login | Нет миграции БД; пустой Jwt:Key |
IDX10503 | Issuer/Audience/Key при проверке не совпадают с генерацией |
| Браузер «не логинится» JWT | Для HTML нужны cookies, не только API-токен |
| Пароль отклонён | PasswordOptions (длина, цифры, регистр) |
Что попробовать
- Сократите
ExpireMinutesи убедитесь, что после часа приходит 401. - Храните заметки с
UserIdиз токена. - Защитите Razor Pages:
[Authorize]на папкеNotes.
Дальше
- Интеграционные тесты с JWT · Minimal API и OpenAPI
- Безопасность приложений · ASP.NET — обзор
- Web API — первая программа · EF Core
См. также
Другие статьи этого же раздела в боковом меню (как на странице «О разделе»). C# как язык платформы .NET - устройство проекта, роль `.cs`-файлов и базовые принципы организации кода. C# — это современный, типизированный язык программирования общего назначения, разработанный корпорацией Microsoft. Справочник-шпаргалка по конфигурациям в C — типы, синтаксис, стандартная библиотека, типовые паттерны. Не заменяет пошаговое обучение. Учебный курс — раздел. Набор советов, правил, принципов и обычаев в разработке на этом языке. Кавычки, точки, запятые, скобки и прочие знаки препинания. Ключевые слова C# - назначение базовых конструкций языка и примеры их применения в типичном коде. Набор функций, которые включены в стандартную библиотеку языка. Пространства имён в C# - организация модулей, `file-scoped namespace` и поддержание чистой структуры кода. манипулировать данными (арифметические, логические, сравнительные операторы). Самый базовый способ ветвления — оператор if. Он проверяет условие и, если оно истинно (true), выполняет блок кода. Обработка исключений в C# - типы исключений, `try/catch/finally` и практики надежного кода. Платформо-зависимые исключения — например, PlatformNotSupportedException используется в кроссплатформенных API, когда функция недоступна на текущей ОС.C# - язык программирования платформы .NET
Что требуется знать перед началом изучения языка программирования C#
Справочник по конфигурациям в C#
Рекомендации по разработке на C#
Синтаксис и пунктуация в C#
Ключевые слова языка C#
Встроенные функции и методы C#
Пространства имён в C#
Управляющие конструкции и логические операторы
Условные выражения и ветвления
Обработка исключений в C#
Иерархия классов исключений в C#