Avatar ·

Создание кастомного AuthorizeAttribute в ASP.NET Core: пошаговое руководство

Всем привет! Я новичок в ASP.NET Core и пытаюсь реализовать кастомную атрибутику авторизации для своего веб-приложения. В старом ASP.NET MVC я мог просто унаследоваться от `AuthorizeAttribute` и переопределить метод `AuthorizeCore`. Но в ASP.NET Core всё иначе. Мне нужно проверять, есть ли у пользователя определённое разрешение (permission) в базе данных, прежде чем дать доступ к контроллеру или экшену. Например, у меня есть магазин, и только пользователи с разрешением "CanEditProducts" могут редактировать товары. Вот что я пробовал: ```csharp public class CustomAuthorizeAttribute : AuthorizeAttribute { private readonly string _permission; public CustomAuthorizeAttribute(string permission) { _permission = permission; } protected override bool AuthorizeCore(HttpContextBase httpContext) { // Старый способ из ASP.NET MVC return httpContext.User.HasClaim("permission", _permission); } public override void OnAuthorization(AuthorizationContext filterContext) { base.OnAuthorization(filterContext); // Какая-то логика... } } ``` Но компилятор ругается, что `AuthorizeCore` и `OnAuthorization` не существуют в ASP.NET Core. Я читал про `IAuthorizationFilter` и `AuthorizationHandler`, но запутался в этих интерфейсах. Как правильно создать свой атрибут авторизации в ASP.NET Core 6+? Мне нужно, чтобы атрибут принимал параметр (название разрешения) и проверял его через сервис `IPermissionService`. Подскажите, пожалуйста, с рабочим примером. И ещё: можно ли использовать такой атрибут одновременно с обычным `[Authorize]`? Спасибо! <<>>

Правильный подход к кастомной авторизации в ASP.NET Core

В ASP.NET Core архитектура авторизации полностью изменилась по сравнению с классическим ASP.NET MVC. Вместо переопределения методов в атрибуте, рекомендуется использовать политики авторизации (Authorization Policies) в связке с обработчиками требований (Authorization Handlers). Это даёт гибкость, тестируемость и возможность внедрения зависимостей (DI).

Вот пошаговое решение для вашей задачи с проверкой разрешения через сервис.

Шаг 1. Определите требование (Requirement)

Создайте класс, реализующий интерфейс IAuthorizationRequirement. Он просто хранит данные, которые нужны для проверки (например, имя разрешения).

public class PermissionRequirement : IAuthorizationRequirement
{
    public string PermissionName { get; }

    public PermissionRequirement(string permissionName)
    {
        PermissionName = permissionName;
    }
}

Шаг 2. Создайте обработчик (AuthorizationHandler)

Здесь находится логика проверки. Обработчик может использовать DI для получения любых сервисов (например, IPermissionService).

public class PermissionHandler : AuthorizationHandler<PermissionRequirement>
{
    private readonly IPermissionService _permissionService;

    public PermissionHandler(IPermissionService permissionService)
    {
        _permissionService = permissionService;
    }

    protected override async Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        PermissionRequirement requirement)
    {
        // Получаем ID пользователя из claims (например, NameIdentifier)
        var userIdClaim = context.User.FindFirst(ClaimTypes.NameIdentifier);
        if (userIdClaim == null)
        {
            context.Fail();
            return;
        }

        var userId = Guid.Parse(userIdClaim.Value);
        bool hasPermission = await _permissionService.UserHasPermissionAsync(userId, requirement.PermissionName);

        if (hasPermission)
        {
            context.Succeed(requirement);
        }
        else
        {
            context.Fail();
        }
    }
}

Шаг 3. Зарегистрируйте политику и обработчик в Program.cs

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("CanEditProducts", policy =>
        policy.Requirements.Add(new PermissionRequirement("CanEditProducts")));
});

// Регистрируем обработчик как singleton или scoped
builder.Services.AddSingleton<IAuthorizationHandler, PermissionHandler>();

// Ваш сервис проверки разрешений
builder.Services.AddScoped<IPermissionService, PermissionService>();

Шаг 4. Используйте атрибут [Authorize] с политикой

Теперь вы можете применить политику к контроллеру или экшену:

[Authorize(Policy = "CanEditProducts")]
public IActionResult EditProduct(int id)
{
    // Ваша логика
    return View();
}

Как сделать атрибут с параметром (более удобный синтаксис)

Если хотите использовать атрибут как [RequirePermission("CanEditProducts")], создайте свой атрибут, который реализует IAuthorizationFilter или использует фабрику политик. Вот простой способ через фабрику:

public class RequirePermissionAttribute : AuthorizeAttribute
{
    public RequirePermissionAttribute(string permission) 
        : base(policy: permission) // Название политики = название разрешения
    {
    }
}

// В Program.cs регистрируем динамическую политику:
builder.Services.AddAuthorization(options =>
{
    // Можно не регистрировать каждое разрешение вручную,
    // а использовать AuthorizationPolicyBuilder в обработчике
});

// Или более продвинутый вариант — через IAuthorizationPolicyProvider
// (см. документацию Microsoft)

Важные моменты

  • Не используйте переопределение AuthorizeAttribute напрямую — это нарушает принципы DI и тестируемости.
  • Обработчик может быть асинхронным (async Task), что отлично для работы с БД.
  • Можно комбинировать несколько политик: [Authorize(Policy = "CanEditProducts")] + [Authorize(Roles = "Admin")] — обе должны быть выполнены.
  • Для возврата 403 вместо редиректа на логин настройте CookieAuthenticationOptions.AccessDeniedPath.

Этот подход официально рекомендуется Microsoft и используется в production-приложениях. Он легко масштабируется: вы можете добавить проверку нескольких разрешений, работу с ролями, claims и т.д.

<<>>

Альтернативный подход: фильтр авторизации (IAuthorizationFilter)

Если вам нужно что-то более простое и вы не хотите углубляться в систему политик, можно использовать фильтр, реализующий IAuthorizationFilter. Однако этот метод менее гибкий и не поддерживает асинхронность на уровне фреймворка.

public class PermissionFilter : IAuthorizationFilter
{
    private readonly string _permission;
    private readonly IPermissionService _permissionService;

    public PermissionFilter(string permission, IPermissionService permissionService)
    {
        _permission = permission;
        _permissionService = permissionService;
    }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var userIdClaim = context.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier);
        if (userIdClaim == null)
        {
            context.Result = new ForbidResult();
            return;
        }

        bool hasPermission = _permissionService.UserHasPermissionAsync(
            Guid.Parse(userIdClaim.Value), _permission).GetAwaiter().GetResult();

        if (!hasPermission)
        {
            context.Result = new ForbidResult();
        }
    }
}

// Использование через атрибут:
public class RequirePermissionAttribute : TypeFilterAttribute
{
    public RequirePermissionAttribute(string permission) 
        : base(typeof(PermissionFilter))
    {
        Arguments = new object[] { permission };
    }
}

// Регистрация в Program.cs:
builder.Services.AddScoped<IPermissionService, PermissionService>();

Недостатки: синхронный вызов GetAwaiter().GetResult() может привести к deadlock в некоторых сценариях, сложнее тестировать. Рекомендую использовать первый вариант с политиками — он более современный и надёжный.