Защита эндпоинтов: роли и политики
Защита эндпоинтов: [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], роли и политики дают декларативную, централизованную защиту. Дальше — обработка ошибок и здоровье приложения в проде.