Перейти к содержанию

5. JWT и роли

Цель

Добавить вход пользователя, генерацию JWT и разграничение доступа по ролям.

Объяснение

После входа клиент получает токен. Каждый защищённый запрос отправляет заголовок:

Authorization: Bearer <token>

До этого этапа API было учебно открытым. Теперь оно становится ближе к реальному: пользователь должен войти, а сервер должен проверить, какие действия ему разрешены.

Роль влияет на поведение системы:

  • Admin управляет данными и может удалять события;
  • Analyst читает данные и отчёты, но не меняет журнал событий;
  • Operator создаёт события, но не имеет доступа к отчётам и удалению.

Такое разграничение нужно не только для безопасности, но и для понятной ответственности пользователей в производственной системе.

Код

Авторизация начинается с DTO. Эти типы фиксируют минимальный контракт входа: клиент отправляет логин и пароль, а сервер при успехе возвращает токен, имя пользователя и его роль.

ActivityMonitoring.Application/Auth/AuthDtos.cs:

namespace ActivityMonitoring.Application.Auth;

public record LoginRequest(string UserName, string Password);
public record LoginResponse(string Token, string UserName, string Role);

Далее задаётся интерфейс сервиса авторизации. Контроллеру важно знать только одно действие — попытку входа, а детали поиска пользователя и генерации токена остаются за реализацией.

ActivityMonitoring.Application/Auth/IAuthService.cs:

namespace ActivityMonitoring.Application.Auth;

public interface IAuthService
{
    Task<LoginResponse?> LoginAsync(LoginRequest request, CancellationToken ct);
}

Фабрика JWT отвечает за создание подписанного токена. В токен помещаются только данные, которые нужны серверу при последующих запросах: идентификатор пользователя, имя и роль.

ActivityMonitoring.Infrastructure/Auth/JwtTokenFactory.cs:

using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using ActivityMonitoring.Domain.Entities;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;

namespace ActivityMonitoring.Infrastructure.Auth;

public class JwtTokenFactory
{
    private readonly IConfiguration _configuration;

    public JwtTokenFactory(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public string Create(AppUser user)
    {
        var claims = new[]
        {
            new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
            new Claim(ClaimTypes.Name, user.UserName),
            new Claim(ClaimTypes.Role, user.Role.Name)
        };

        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]!));
        var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var token = new JwtSecurityToken(
            issuer: _configuration["Jwt:Issuer"],
            audience: _configuration["Jwt:Audience"],
            claims: claims,
            expires: DateTime.UtcNow.AddHours(4),
            signingCredentials: credentials);

        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}

Сервис авторизации связывает базу данных и фабрику токенов. Он находит активного пользователя, проверяет пароль в учебном формате и выдаёт JWT, если вход разрешён.

ActivityMonitoring.Infrastructure/Auth/AuthService.cs:

using ActivityMonitoring.Application.Auth;
using ActivityMonitoring.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;

namespace ActivityMonitoring.Infrastructure.Auth;

public class AuthService : IAuthService
{
    private readonly AppDbContext _db;
    private readonly JwtTokenFactory _tokenFactory;

    public AuthService(AppDbContext db, JwtTokenFactory tokenFactory)
    {
        _db = db;
        _tokenFactory = tokenFactory;
    }

    public async Task<LoginResponse?> LoginAsync(LoginRequest request, CancellationToken ct)
    {
        var user = await _db.Users
            .Include(x => x.Role)
            .FirstOrDefaultAsync(x => x.UserName == request.UserName && x.IsActive, ct);

        if (user is null)
            return null;

        // Учебная проверка. Для реального проекта используйте PasswordHasher или BCrypt.
        if (user.PasswordHash != request.Password)
            return null;

        var token = _tokenFactory.Create(user);
        return new LoginResponse(token, user.UserName, user.Role.Name);
    }
}

Контроллер авторизации открывает этот сценарий наружу через HTTP. Он не создаёт токен сам, а делегирует вход сервису и возвращает либо 200 OK с токеном, либо 401 Unauthorized.

ActivityMonitoring.Api/Controllers/AuthController.cs:

using ActivityMonitoring.Application.Auth;
using Microsoft.AspNetCore.Mvc;

namespace ActivityMonitoring.Api.Controllers;

[ApiController]
[Route("api/auth")]
public class AuthController : ControllerBase
{
    private readonly IAuthService _authService;

    public AuthController(IAuthService authService)
    {
        _authService = authService;
    }

    [HttpPost("login")]
    public async Task<ActionResult<LoginResponse>> Login(LoginRequest request, CancellationToken ct)
    {
        var response = await _authService.LoginAsync(request, ct);
        return response is null ? Unauthorized() : Ok(response);
    }
}

После добавления классов подключите их в Program.cs. Этот блок регистрирует сервисы авторизации и настраивает правила проверки JWT: издателя, аудиторию, срок действия и ключ подписи.

using System.Text;
using ActivityMonitoring.Application.Auth;
using ActivityMonitoring.Infrastructure.Auth;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;

builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddSingleton<JwtTokenFactory>();

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
        };
    });

builder.Services.AddAuthorization();

Middleware авторизации должен стоять перед app.MapControllers(), потому что проверка пользователя выполняется до вызова защищённого action-метода контроллера.

app.UseAuthentication();
app.UseAuthorization();

Теперь примените роли к действиям EventsController. Этот фрагмент показывает не новый контроллер целиком, а смысловое изменение: разные операции получают разные требования к роли пользователя.

[Authorize(Roles = "Admin,Analyst,Operator")]
[HttpGet]
public async Task<ActionResult<IReadOnlyList<ActivityEventDto>>> Get(...)

[Authorize(Roles = "Admin,Operator")]
[HttpPost]
public async Task<ActionResult<ActivityEventDto>> Create(...)

[Authorize(Roles = "Admin")]
[HttpDelete("{id:int}")]
public async Task<IActionResult> Delete(...)

Проверка

Сначала получите токен через endpoint входа. Этот запрос проверяет, что пользователь найден в базе, роль загружена, а JWT успешно сформирован и возвращён клиенту.

POST /api/auth/login
Content-Type: application/json

{
  "userName": "admin",
  "password": "Admin123!"
}

Используйте токен в Swagger через кнопку Authorize.

Проверяйте не только успешный вход, но и запрет доступа. Например, войдите под operator и попробуйте удалить событие. Правильный результат — 403 Forbidden, потому что пользователь авторизован, но его роль не имеет права на удаление.

Результат

Admin может всё, Analyst может смотреть события, Operator может создавать события, но не может удалять их.

После этапа проект перестаёт быть просто CRUD API. Теперь это система с контролем доступа, где разные пользователи видят разные возможности.