Защита эндпоинтов: роли и политики

Защита эндпоинтов: [Authorize], роли и политики авторизации.

Суть: атрибут [Authorize] закрывает эндпоинт для неаутентифицированных, а с параметрами — ограничивает по ролям или политикам. Политики — гибкий способ выразить сложные правила доступа в одном месте.

Имея аутентификацию, нужно решить, кто к чему допущен. ASP.NET Core даёт декларативную защиту: помечаете контроллер или метод атрибутом — и фреймворк сам проверяет права перед вызовом.

От простого к сложному

[Authorize]                       // только аутентифицированные
public class OrdersController : ControllerBase
{
    [HttpGet]
    public IActionResult GetMine() => Ok();

    [HttpDelete("{id}")]
    [Authorize(Roles = "Admin")]  // только роль Admin
    public IActionResult Delete(int id) => NoContent();

    [HttpGet("public")]
    [AllowAnonymous]              // исключение: открыто всем
    public IActionResult Public() => Ok();
}

[Authorize] на классе закрывает все методы. [Authorize(Roles="Admin")] сужает до роли. [AllowAnonymous] делает точечное исключение даже внутри защищённого контроллера.

Политики

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("Adult", policy =>
        policy.RequireClaim("age", "18", "19", "20")); // упрощённо
});

// применение
[Authorize(Policy = "Adult")]
public IActionResult Restricted() => Ok();

Как работает под капотом

Когда middleware UseAuthorization доходит до эндпоинта, оно читает его метаданные — атрибуты [Authorize]. Для каждого требования (роль, политика) запускается обработчик, который проверяет ClaimsPrincipal (того самого пользователя из аутентификации). Если хоть одно требование не выполнено — запрос отклоняется с 403 (или 401, если пользователь вообще не аутентифицирован). Политики — это именованные наборы требований, заданные централизованно; их переиспользуют на разных эндпоинтах, не разбрасывая логику.

# Авторизация по политике: набор требований к claims пользователя
user_claims = {"role": "Editor", "age": 25}

policies = {
    "AdminOnly": lambda c: c.get("role") == "Admin",
    "Adult":     lambda c: c.get("age", 0) >= 18,
    "Staff":     lambda c: c.get("role") in {"Admin", "Editor"},
}

def authorize(claims, policy_name):
    rule = policies[policy_name]
    return "разрешено" if rule(claims) else "403 Forbidden"

for name in policies:
    print(name, "->", authorize(user_claims, name))

Попробуй сам ▶ — Editor 25 лет проходит «Adult» и «Staff», но не «AdminOnly». Это и есть проверка claims против политик, как в ASP.NET Core.

Частые ошибки

  • Защищать «по желанию». Забыть [Authorize] на чувствительном методе — дыра. Лучше глобальная политика «по умолчанию закрыто».
  • Логика доступа в теле метода. Россыпь условий по ролям трудно поддерживать — выносите в политики.
  • Путать роли и политики. Роли — простой случай; сложные правила (возраст, владелец ресурса) — политики/требования.

Best practices

  • Применяйте принцип «по умолчанию закрыто»: глобальный [Authorize], а открытое помечайте [AllowAnonymous].
  • Сложные правила инкапсулируйте в политики и requirement-обработчики — это тестируемо и переиспользуемо.
  • Проверяйте владение ресурсом (пользователь меняет только свои данные), а не только роль.

Роли против политик: когда чего достаточно

Роли — простейшая модель: пользователю присвоен ярлык (Admin, Editor), а эндпоинт требует наличия роли через [Authorize(Roles="Admin")]. Этого хватает для грубого разграничения. Но реальные правила часто сложнее: «доступ только совершеннолетним», «менять можно только свой ресурс», «нужно одно из нескольких условий». Для них существуют политики — именованные наборы требований, заданные централизованно в AddAuthorization и применяемые через [Authorize(Policy="...")]. Политика инкапсулирует логику в одном месте и переиспользуется на множестве эндпоинтов.

Под капотом политика состоит из требований (requirements) и обработчиков (handlers). Каждое требование — это вопрос («есть ли claim age ≥ 18?»), обработчик — код, отвечающий на него по ClaimsPrincipal. Запускаемая врезка выше показала суть: набор правил-предикатов проверяется против claims пользователя, и доступ даётся, только если правило выполнено. Реальный механизм ASP.NET Core устроен так же, просто с расширяемыми классами вместо лямбд.

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

Часто решение зависит не только от того, кто пользователь, но и от того, над чем он действует: владелец заметки может её редактировать, посторонний — нет, даже если у обоих одинаковая роль. Это resource-based авторизация: проверку проводят, имея на руках конкретный ресурс, через IAuthorizationService. Чисто ролевой моделью такое не выразить — нужна привязка к данным. Игнорирование этого слоя — классическая уязвимость (IDOR), когда сменой id в URL пользователь дотягивается до чужих данных.

Организационно надёжнее всего работает принцип default-deny: глобально требовать аутентификацию (fallback-политика или глобальный [Authorize]), а публичные точки помечать [AllowAnonymous] явно. Тогда новый эндпоинт по умолчанию защищён, и забыть закрыть его невозможно — забыть можно только открыть, что замечается сразу. Сложные правила держат в политиках и requirement-обработчиках: это тестируется в изоляции, переиспользуется и не расползается условиями по контроллерам. Декларативные атрибуты плюс централизованные политики дают защиту, которую видно и легко проверить.

Итог: [Authorize], роли и политики дают декларативную, централизованную защиту. Дальше — обработка ошибок и здоровье приложения в проде.

Проверьте себя
1. Что делает [Authorize(Roles = Admin)] на методе?
AОткрывает метод всем
BДопускает к методу только аутентифицированных пользователей с ролью Admin
CУдаляет метод
DДелает метод анонимным
2. Зачем нужны политики авторизации вместо условий в коде?
AПолитики работают быстрее
BОни централизуют и переиспользуют правила доступа, делая их тестируемыми, а не разбросанными по методам
CУсловия не компилируются
DПолитики заменяют аутентификацию