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

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«Представились, но прав нет».

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

EndpointAuthДействие
POST /registerНетСоздать пользователя
POST /loginНетПолучить JWT
GET /api/notesBearer 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);
}
}
МетодЛогика
RegisterCreateAsync сохраняет пользователя; пароль попадает в БД только как хеш.
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
  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>"

Cookie (Identity)JWT Bearer
Где «сессия»Зашифрованное cookie в браузереТокен у клиента
КлиентRazor Pages, MVCSPA, mobile, сервисы
CSRFНужен anti-forgery на формахBearer + HTTPS, короткий TTL
Быстрый старт HTMLdotnet 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
IDX10503Issuer/Audience/Key при проверке не совпадают с генерацией
Браузер «не логинится» JWTДля HTML нужны cookies, не только API-токен
Пароль отклонёнPasswordOptions (длина, цифры, регистр)

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

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

Дальше


См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).