.NET Tutorial
Middle6 мин чтенияВопрос 10 из 19

Что такое ClaimsPrincipal, Identity, Roles vs Policy-based auth?

Модель claims

ClaimsPrincipal
HttpContext.User

ClaimsIdentity
AuthenticationType = Bearer

ClaimsIdentity
AuthenticationType = Windows
(редко: несколько схем)

sub = 42

name = ann

role = Manager

department = Sales

permission = orders.refund

ClaimsPrincipal
HttpContext.User

ClaimsIdentity
AuthenticationType = Bearer

ClaimsIdentity
AuthenticationType = Windows
(редко: несколько схем)

sub = 42

name = ann

role = Manager

department = Sales

permission = orders.refund

Структура пользователя в ASP.NET Core
Чтение claims
ClaimsPrincipal user = HttpContext.User;

bool isAuthenticated = user.Identity?.IsAuthenticated == true;
string? userId = user.FindFirstValue(ClaimTypes.NameIdentifier);   // или "sub" — зависит от маппинга
string? email = user.FindFirstValue(ClaimTypes.Email);
bool isManager = user.IsInRole("Manager");
IEnumerable<string> permissions = user.FindAll("permission").Select(c => c.Value);

Удобно спрятать работу с claims за абстракцией:

C#
public interface ICurrentUser { Guid Id { get; } bool HasPermission(string p); }

public sealed class HttpCurrentUser(IHttpContextAccessor accessor) : ICurrentUser
{
    private ClaimsPrincipal User => accessor.HttpContext?.User ?? new ClaimsPrincipal();
    public Guid Id => Guid.Parse(User.FindFirstValue("sub") ?? throw new UnauthorizedAccessException());
    public bool HasPermission(string p) => User.HasClaim("permission", p);
}

Роли

C#
[Authorize(Roles = "Admin")]                         // только Admin
[Authorize(Roles = "Admin,Manager")]                 // Admin ИЛИ Manager
[Authorize(Roles = "Admin")] [Authorize(Roles = "Auditor")]   // Admin И Auditor
public class ReportsController : ControllerBase { }

Проблемы ролевой модели в растущем проекте:

  • Проверки if (User.IsInRole("Admin") || User.IsInRole("Manager") && ...) расползаются по коду.
  • Появление новой роли («Старший менеджер») требует правок во многих местах.
  • Нельзя выразить «менеджер своего филиала», «рабочее время», «сумма возврата до 1000».

Политики

context.Succeed

ничего / Fail

[Authorize(Policy = 'RefundUpTo1000')]

Политика
RefundUpTo1000

Требование:
аутентифицирован

Требование:
MaxRefundRequirement(1000)

MaxRefundHandler
(может читать БД через DI)

Разрешено

403

context.Succeed

ничего / Fail

[Authorize(Policy = 'RefundUpTo1000')]

Политика
RefundUpTo1000

Требование:
аутентифицирован

Требование:
MaxRefundRequirement(1000)

MaxRefundHandler
(может читать БД через DI)

Разрешено

403

Как работает политика
Объявление политик
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("AdultsOnly", p => p.RequireAssertion(ctx =>
        int.TryParse(ctx.User.FindFirstValue("age"), out var age) && age >= 18))
    .AddPolicy("SalesDepartment", p => p.RequireClaim("department", "Sales"))
    .AddPolicy("CanRefund", p => p
        .RequireAuthenticatedUser()
        .RequireClaim("permission", "orders.refund")
        .AddRequirements(new WorkingHoursRequirement(9, 18)));

builder.Services.AddSingleton<IAuthorizationHandler, WorkingHoursHandler>();
Своё требование и обработчик
public sealed record WorkingHoursRequirement(int From, int To) : IAuthorizationRequirement;

public sealed class WorkingHoursHandler(TimeProvider clock) : AuthorizationHandler<WorkingHoursRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext ctx, WorkingHoursRequirement req)
    {
        var hour = clock.GetLocalNow().Hour;
        if (hour >= req.From && hour < req.To)
            ctx.Succeed(req);                  // требование выполнено
        return Task.CompletedTask;             // не вызвали Succeed — требование не выполнено
    }
}

Семантика: политика пройдена, если все требования выполнены. Одно требование может иметь несколько обработчиков — достаточно, чтобы один вызвал Succeed (логическое ИЛИ), если никто не вызвал Fail.

Resource-based авторизация

Атрибут выполняется до загрузки данных, поэтому не может проверить, что документ принадлежит пользователю. Для этого — императивная проверка:

C#
public sealed class DocumentOwnerHandler : AuthorizationHandler<OperationAuthorizationRequirement, Document>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext ctx,
        OperationAuthorizationRequirement op, Document doc)
    {
        var userId = ctx.User.FindFirstValue("sub");
        if (op.Name == "Edit" && doc.AuthorId.ToString() == userId) ctx.Succeed(op);
        if (ctx.User.IsInRole("Admin")) ctx.Succeed(op);
        return Task.CompletedTask;
    }
}

// В обработчике запроса
app.MapPut("/documents/{id}", async (Guid id, UpdateDoc body, IAuthorizationService auth, ClaimsPrincipal user, AppDbContext db) =>
{
    var doc = await db.Documents.FindAsync(id);
    if (doc is null) return Results.NotFound();

    var result = await auth.AuthorizeAsync(user, doc, new OperationAuthorizationRequirement { Name = "Edit" });
    if (!result.Succeeded) return Results.Forbid();

    doc.Update(body.Title, body.Content);
    await db.SaveChangesAsync();
    return Results.NoContent();
});

Роли vs политики vs permissions

Подход Плюсы Минусы Когда
Роли Просто, встроено в IdP Жёстко, взрыв ролей Небольшие системы, грубое разделение
Политики на claims Централизованно, тестируемо, DI Нужно проектировать Большинство API
Permissions (права как claims) Гибко: роль = набор прав, код проверяет права Токен растёт, нужен механизм выдачи Сложные доменные права
Resource-based Учитывает конкретный объект Проверка в коде обработчика Владение, мультиарендность

Claims transformation

C#
// Обогатить пользователя правами из своей БД после аутентификации
public sealed class PermissionsTransformation(IPermissionStore store) : IClaimsTransformation
{
    public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        if (principal.HasClaim(c => c.Type == "permission")) return principal;   // может вызываться несколько раз
        var id = principal.FindFirstValue("sub");
        if (id is null) return principal;
        var identity = new ClaimsIdentity();
        foreach (var p in await store.GetPermissionsAsync(id))
            identity.AddClaim(new Claim("permission", p));
        principal.AddIdentity(identity);
        return principal;
    }
}

Результат стоит кэшировать — трансформация вызывается на каждый запрос.

Вопросы на засыпку

Что такое ASP.NET Core Identity и как оно связано с ClaimsPrincipal?

Identity — библиотека управления пользователями: хранение в БД (EF Core), хэширование паролей, подтверждение email, 2FA, блокировка, роли. При входе она создаёт ClaimsPrincipal и выдаёт cookie (или токен). Claims-модель — общая инфраструктура, Identity — одна из реализаций хранилища пользователей.

Где проверять авторизацию: в контроллере или в домене?

Грубую (роль, право на эндпоинт) — атрибутами/политиками на эндпоинте. Resource-based — в прикладном слое (обработчике команды), после загрузки ресурса. Домен остаётся независимым от ClaimsPrincipal: ему передают идентификатор пользователя как данные.

Как протестировать политику авторизации?

Создать AuthorizationHandlerContext с нужным ClaimsPrincipal и требованиями, вызвать handler.HandleAsync(ctx) и проверить ctx.HasSucceeded. Или интеграционно через WebApplicationFactory с тестовой схемой аутентификации, подставляющей нужные claims.

Что делает FallbackPolicy?

Применяется к эндпоинтам без атрибутов [Authorize]/[AllowAnonymous]. Установив RequireAuthenticatedUser(), вы делаете всё закрытым по умолчанию — забытый атрибут не откроет эндпоинт наружу.

Итог

Пользователь в ASP.NET Core — это ClaimsPrincipal с набором claims. Роли подходят для простых случаев, политики с требованиями и обработчиками — для централизованных и тестируемых правил, resource-based авторизация — когда решение зависит от конкретного объекта. Проверяйте права, а не названия ролей.