Зачем вам вообще использовать встроенную ролевую авторизацию в вашем API?

[HttpGet("get_something")]
[Authorize(Role = "admin, admins_cat")]
[Authorize(Role = "hacker")]
public Task<IActionResult> GetSomething()

Хорошо, это выглядит красиво и хорошо работает, но в случае большого SaaS-приложения вы теряете некоторую гибкость. Представим, что у вас есть 3 основные роли: StandartUser, PremiumUser и EnterpriseUser. Вы написали множество функций и сотни конечных точек. И сейчас самое время расширить список подписчиков. Вы хотите добавить роль UltraUser, такой пользователь будет иметь полный доступ ко всем функциям. Итак, теперь вам следует поместить это во все свое приложение.

[Authorize(Role = "UltraUser")]

Я предпочитаю использовать флаги перечисления для ролей, разрешений и т. д. Давайте углубимся в эту тему.

Мы создаем новый PermissionEnum со всеми функциями, которые наше приложение предоставляет пользователю. Например, у нас есть: «Добавить», «Обновить», «Удалить», «Получить» и «Специальные функции». Это будет выглядеть так.

    [Flags]
    public enum PermissionEnum : int
    {
        None = 0,
        Add = 1,
        Update = 1 << 1,
        Delete = 1 << 2,
        Get = 1 << 3,
        Special = 1 << 4
    }

Если вы не знакомы с флагами перечисления, вам следует потратить некоторое время на чтение этого:



Для подписок мы также будем использовать перечисление. Это дало бы нам возможность создавать разрешения. По этой причине создается PermissionService. Вся логика разрешений и подписок находится здесь.

    public enum RoleEnum
    {
        Standart = 1,
        Premium = 2,
        Ultra = 3,
        Admin = 4
    }

    public class PermissionService
    {
        public PermissionEnum GetPermissions(RoleEnum role)
        {
            var permissionsList = PermissionEnum.None;

            switch (role)
            {
                case RoleEnum.Admin:
                    permissionsList = permissionsList
                    | PermissionEnum.Get | PermissionEnum.Delete | PermissionEnum.Add
                    | PermissionEnum.Update | PermissionEnum.Special;
                    break;

                case RoleEnum.Ultra:
                    permissionsList = permissionsList
                    | PermissionEnum.Get | PermissionEnum.Delete | PermissionEnum.Add
                    | PermissionEnum.Update;
                    break;

                case RoleEnum.Premium:
                    permissionsList = permissionsList
                    | PermissionEnum.Get | PermissionEnum.Add
                    | PermissionEnum.Update;
                    break;

                case RoleEnum.Standart:
                    permissionsList = permissionsList
                    | PermissionEnum.Get | PermissionEnum.Add;
                    break;
            }

            return permissionsList;
        }
    }

Итак, как нам следует использовать эту услугу?
Когда токен доступа создан, мы помещаем PermissionEnum в полезную нагрузку JWT. Тогда этот PermissionEnum будет доступен в каждом запросе.

_tokenService.CreateToken(user.Id.ToString(), _permissionService.GetPermissions(user.Role));

Пришло время добавить атрибут, отвечающий за разрешения. Вы можете расширитьAuthorizeAttribute,, но для простоты я напишу свой собственный.

    public class AllowedForAttribute : Attribute
    {
        public IEnumerable<PermissionEnum> AllowedFor { get; set; }
        public AllowedForAttribute(params PermissionEnum[] rights)
        {
            AllowedFor = rights;
        }
    }

Последний шаг — добавить промежуточное программное обеспечение, которое будет проверять, есть ли у пользователя все необходимые разрешения для доступа к определенной конечной точке.

    public class EndpointPermissionMiddleware : IMiddleware
    {
        public async Task InvokeAsync(HttpContext context, RequestDelegate next)
        {
            //get and parse JWT to get PermissionEnum
            var endpoint = context.Features.Get<IEndpointFeature>()?.Endpoint;

            CheckIfAllowed(endpoint, userPermissions);

            await next.Invoke(context);

        }

        private void CheckIfAllowed(Endpoint? endpoint, PermissionEnum permissions)
        {
            var attribute = endpoint?.Metadata.GetMetadata<AllowedForAttribute>();

            if (attribute is not null && !IsAllowed(attribute.AllowedFor, permissions))
            {
                throw new UnauthorizedAccessException();
            }
        }

        private bool IsAllowed(IEnumerable<PermissionEnum> rights, PermissionEnum permissions)
        {
            foreach (var right in rights)
            {
                if (permissions.HasFlag(right))
                {
                    return true;
                }
            }

            return false;
        }

Краткое содержание

Отличная работа! Теперь вы можете легко расширять и изменять свои подписки. Вы можете использовать функциональные возможности разрешений следующим образом:

        [AllowedFor(PermissionEnum.Get | PermissionEnum.Delete | PermissionEnum.Add
                    | PermissionEnum.Update, PermissionEnum.Special)]
        public IEnumerable<WeatherForecast> Ultra()

Надеюсь, вам понравилась эта идея! Видите ли вы какие-либо проблемы в этом потоке?

Код можно получить здесь.