Как правильно создать и реализовать собственный кастомный тег Authorize в ASP.NET 8?

Здравствуйте! Я сталкиваюсь с определенной проблемой в процессе реализации двухфакторной авторизации в моем API. В настоящее время я храню состояние прохождения авторизации для некоторых пользователей только в токене. Первый уровень авторизации проверяется с помощью атрибута [Authorize], а второй – через кастомный атрибут [Authorize(Policy = "TwoFactorPolicy")], причем оба фактора проверяются одновременно. Проверка работает корректно, однако мне необходимо возвращать разные сообщения об ошибке в ситуации, когда пользователь успешно прошел первый уровень авторизации, но не прошел второй. Это можно сделать через return Unauthorized("custom message") или, при необходимости, использовать другой код ошибки. Главное, чтобы фронтенд мог четко понимать, на каком этапе авторизации находится пользователь. У меня есть код кастомного обработчика, который я привожу ниже: 

<pre><code class="cs">public class TwoFactorAuthorizationHandler : AuthorizationHandler&lt;TwoFactorAuthorizationRequirement&gt; {
	private readonly ITwoFactorAuthHelpService _twoFactorAuthService;
	private readonly ILogger&lt;TwoFactorAuthorizationHandler&gt; _logger;
	private readonly ITokenService _tokenService;
	private readonly IUserHelpService _userHelpService;
	private readonly IHttpContextAccessor _httpContextAccessor;

	public TwoFactorAuthorizationHandler(ILogger&lt;TwoFactorAuthorizationHandler&gt; logger, ITwoFactorAuthHelpService twoFactorAuthService,
		ITokenService tokenService, IUserHelpService userHelpService, IHttpContextAccessor httpContextAccessor) {
		_logger = logger;
		_twoFactorAuthService = twoFactorAuthService;
		_tokenService = tokenService;
		_userHelpService = userHelpService;
		_httpContextAccessor = httpContextAccessor;
	}

	protected override async Task HandleRequirementAsync(Authori
  • 17 марта 2025 г. 12:25
Ответы на вопрос 1
Для реализации кастомного атрибута `[Authorize(Policy = "TwoFactorPolicy")]` в ASP.NET 8 и возврата различных сообщений об ошибках в зависимости от уровня авторизации, вам нужно следовать нескольким шагам. Я дам вам полный пример реализации, включая создание кастомного атрибута, обработчика, а также настройку политик.

### Шаг 1: Создание требования и обработчика

Для начала, создайте класс требования `TwoFactorAuthorizationRequirement` и обработчик, который будет его обрабатывать:

```csharp
public class TwoFactorAuthorizationRequirement : IAuthorizationRequirement
{
    // Здесь можно определить дополнительные параметры требования, если это нужно
}

public class TwoFactorAuthorizationHandler : AuthorizationHandler<TwoFactorAuthorizationRequirement>
{
    private readonly ITwoFactorAuthHelpService _twoFactorAuthService;
    private readonly IHttpContextAccessor _httpContextAccessor;

    public TwoFactorAuthorizationHandler(ITwoFactorAuthHelpService twoFactorAuthService, IHttpContextAccessor httpContextAccessor)
    {
        _twoFactorAuthService = twoFactorAuthService;
        _httpContextAccessor = httpContextAccessor;
    }

    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, TwoFactorAuthorizationRequirement requirement)
    {
        var user = context.User;

        if (user == null || !user.Identity.IsAuthenticated)
        {
            // Пользователь не авторизован
            context.Fail();
            return;
        }

        // Предполагается, что у вас есть метод для проверки состояния двухфакторной аутентификации
        bool isTwoFactorEnabled = await _twoFactorAuthService.IsTwoFactorEnabledAsync(user);

        if (isTwoFactorEnabled)
        {
            // Проверяем, есть ли успешный двухфакторный код
            if (await _twoFactorAuthService.IsTwoFactorCodeValidAsync(user))
            {
                context.Succeed(requirement);
            }
            else
            {
                // Если код не валиден, провалить с определённым сообщением
                context.Fail(new AuthorizationFailureReason("Invalid two-factor authentication code."));
            }
        }
        else
        {
            // Если двухфакторная аутентификация не включена
            context.Succeed(requirement);
        }
    }
}
```

### Шаг 2: Регистрация требуемого обработчика в Startup.cs

В `Program.cs` (или `Startup.cs`, в зависимости от конфигурации) вам нужно зарегистрировать политику и обработчик:

```csharp
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("TwoFactorPolicy", policy =>
        policy.Requirements.Add(new TwoFactorAuthorizationRequirement()));
});

builder.Services.AddSingleton<IAuthorizationHandler, TwoFactorAuthorizationHandler>();
```

### Шаг 3: Создание кастомного атрибута

Теперь создайте кастомный атрибут на основе стандартного `[Authorize]`:

```csharp
using Microsoft.AspNetCore.Authorization;

public class TwoFactorAuthorizeAttribute : AuthorizeAttribute, IAuthorizationPolicyProvider
{
    public TwoFactorAuthorizeAttribute()
    {
        Policy = "TwoFactorPolicy";
    }
}
```

### Шаг 4: Обработка ответов с ошибками

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

```csharp
public class CustomAuthorizationFailureHandler : IAsyncAuthorizationFilter
{
    public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
    {
        if (context.Result is ForbidResult)
        {
            // Убедитесь, что это именно ошибка двухфакторной аутентификации
            // Вам нужно проверить причины неудачи в вашем обработчике
            var reasons = (context.HttpContext.Items["AuthorizationFailureReasons"] as IEnumerable<AuthorizationFailureReason>)?.ToList();

            if (reasons != null && reasons.Any())
            {
                var firstReason = reasons.First().Message; // Получаем первое сообщение
                context.Result = new UnauthorizedObjectResult(new { message = firstReason });
            }
        }
        else if (context.Result is UnauthorizedResult)
        {
            context.Result = new UnauthorizedObjectResult(new { message = "User is not authenticated." });
        }

        await Task.CompletedTask; // Можно сделать дополнительные асинхронные действия здесь, если нужно
    }
}
```

### Шаг 5: Использование атрибута в контроллерах

Теперь вы можете использовать ваш кастомный атрибут в контроллерах:

```csharp
[ApiController]
[Route("[controller]")]
public class SampleController : ControllerBase
{
    [HttpGet]
    [TwoFactorAuthorize]
    public IActionResult GetSecureData()
    {
        return Ok("This is secure data.");
    }
}
```

### Зак
Похожие вопросы