Asp. net Базовая авторизация на основе ресурсов WebAPI вне уровня контроллера - PullRequest
7 голосов
/ 16 апреля 2020

Я создаю веб-API для пользователей с разными ролями, кроме того, как и для любого другого приложения, я не хочу, чтобы пользователь А получал доступ к ресурсам пользователя Б. Как показано ниже:

Заказы / 1 ( Пользователь A )

Заказы / 2 ( Пользователь B )

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

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

Обновление

Для маршрутов первой линией защиты является политика безопасности, требующая определенных требований.

Мой вопрос касается второй линии защиты, которая обеспечивает пользователям доступ только к своим данным / ресурсам.

Существуют ли стандартные подходы? быть принятым в этом сценарии?

Ответы [ 6 ]

4 голосов
/ 23 апреля 2020

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

Я использую интерфейс, чтобы указать, какие записи данных являются учетными записями c.

public interface IAccountOwnedEntity
{
    Guid AccountKey { get; set; }
}

И предоставить интерфейс для ввода логики c для определения того, на какую учетную запись должен быть нацелен репозиторий.

public interface IAccountResolver
{
    Guid ResolveAccount();
}

Реализация IAccountResolver, который я использую сегодня, основана на утверждениях аутентифицированных пользователей.

public class ClaimsPrincipalAccountResolver : IAccountResolver
{
    private readonly HttpContext _httpContext;

    public ClaimsPrincipalAccountResolver(IHttpContextAccessor httpContextAccessor)
    {
        _httpContext = httpContextAccessor.HttpContext;
    }

    public Guid ResolveAccount()
    {
        var AccountKeyClaim = _httpContext
            ?.User
            ?.Claims
            ?.FirstOrDefault(c => String.Equals(c.Type, ClaimNames.AccountKey, StringComparison.InvariantCulture));

        var validAccoutnKey = Guid.TryParse(AccountKeyClaim?.Value, out var accountKey));

        return (validAccoutnKey) ? accountKey : throw new AccountResolutionException();
    }
}

Затем в репозитории я ограничиваю все возвращаемые записи тем, что они принадлежат этой учетной записи.

public class SqlRepository<TRecord, TKey>
    where TRecord : class, IAccountOwnedEntity
{
    private readonly DbContext _dbContext;
    private readonly IAccountResolver _accountResolver;

    public SqlRepository(DbContext dbContext, IAccountResolver accountResolver)
    {
        _dbContext = dbContext;
        _accountResolver = accountResolver;
    }

    public async Task<IEnumerable<TRecord>> GetAsync()
    {
        var accountKey = _accountResolver.ResolveAccount();

        return await _dbContext
                .Set<TRecord>()
                .Where(record => record.AccountKey == accountKey)
                .ToListAsync();
    }


    // Other CRUD operations
}

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

3 голосов
/ 23 апреля 2020

Использование атрибута [Authorize] называется декларативной авторизацией. Но он выполняется до того, как будет выполнен контроллер или действие. Если вам нужна авторизация на основе ресурсов, а у документа есть свойство author, вы должны загрузить документ из хранилища до оценки авторизации. Это называется императивной авторизацией.

В Документах Microsoft есть пост о том, как обращаться с императивной авторизацией в ASP. NET Core. Я думаю, что он достаточно всеобъемлющий и отвечает на ваш вопрос о стандартном подходе.

Также здесь вы можете найти пример кода.

2 голосов
/ 28 апреля 2020

Чтобы убедиться, что Пользователь A не может просматривать Заказ с Id = 2 (принадлежит Пользователь B ). Я бы сделал одну из этих двух вещей:

Один: Иметь GetOrderByIdAndUser(long orderId, string username), и, конечно, вы берете имя пользователя из jwt. Если пользователь не владеет ордером, он его не увидит и не будет лишнего вызова db.

Два: Сначала извлеките ордер GetOrderById(long orderId) из базы данных, а затем подтвердите это имя пользователя. свойство заказа такое же, как вошедшего в систему пользователя в jwt. Если пользователь не является владельцем исключения «Бросок заказа», верните 404 или что-то еще, и никаких дополнительных вызовов db.

void ValidateUserOwnsOrder(Order order, string username)
{
            if (order.username != username)
            {
                throw new Exception("Wrong user.");
            }
}
2 голосов
/ 22 апреля 2020

Вы можете создать несколько политик в методе ConfigureServices вашего запуска, содержащего роли или, в соответствии с приведенным здесь примером, имена, например:

AddPolicy("UserA", builder => builder.RequireClaim("Name", "UserA"))

или заменить «UserA» на «Accounting» и «Имя» с «Ролью».
Затем ограничьте методы контроллера ролью:

[Authorize(Policy = "UserA")

Конечно, это снова на уровне контроллера, но вам не нужно взламывать токены или базу данных. , Это даст вам прямой индикатор того, какую роль или пользователь может использовать какой метод.

1 голос
/ 29 апреля 2020

Первый вопрос, на который вам нужно ответить: «Когда я смогу принять решение об авторизации?». Когда у вас есть информация, необходимая для проверки?

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

Если вы не можете понять, может ли пользователь возиться с ресурсом, пока вы на самом деле не изучите его, то вы в значительной степени застряли с обязательными проверками. Для этого существует стандартная структура , но она не так полезна, как структура политики. Возможно, в какой-то момент полезно написать IUserContext, который может быть введен в тот момент, когда вы запрашиваете домен (например, в репозитории / везде, где вы используете linq), который инкапсулирует некоторые из этих фильтров (IEnumerable<Order> Restrict(this IEnumerable<Order> orders, IUserContext ctx)).

Для сложной области не будет легкой серебряной пули. Если вы используете ORM, это может помочь вам - но не забывайте, что навигационные отношения в вашем домене позволят коду нарушать контекст, особенно если вы не были строгими в попытке сохранить агрегаты изолированными (myOrder.Items [ n] .Product.Orderees [notme] ...).

В последний раз, когда я делал это, мне удавалось использовать подход, основанный на политике на маршруте, в 90% случаев, но все же пришлось сделать некоторые ручные императивные проверки для нечетного списка или сложного запроса. Опасность использования императивных проверок, как я уверен, вы знаете, заключается в том, что вы их забываете. Возможное решение для этого - применить [Authorize(Policy = "MatchingUserPolicy")] на уровне контроллера, добавить дополнительную политику «ISolemlySwearIHaveDoneImperativeChecks» к действию, а затем в вашем MatchUserRequirementsHandler проверить контекст и обойти наивные проверки соответствия пользователя / порядка, если обязательные проверки были выполнены. 'объявил'.

1 голос
/ 23 апреля 2020

Ваши утверждения неверны, и вы также неправильно их проектируете.

Чрезмерная оптимизация - это root всего зла

Эта ссылка может быть обобщена в «проверить производительность, прежде чем утверждать, что она не будет работать».

Использование идентификатора (токена jwt или чего-либо еще, что вы настроили), чтобы проверить, имеет ли реальный пользователь доступ к нужному ресурсу (или, возможно, лучше обслуживать только ресурсы) он владеет) не слишком тяжелый. Если он становится тяжелым, вы делаете что-то не так. Может случиться так, что у вас есть тонны одновременного доступа, и вам просто нужно кэшировать некоторые данные, например, порядок словаря -> ownerid, который очищается со временем ... но это не так.

about дизайн: создать сервис многократного использования, который может быть внедрен, и иметь метод для доступа к любому ресурсу, который вам нужен, который принимает пользователя (IdentityUser или субъект jwt, только идентификатор пользователя или что-то еще у вас)

что-то вроде

ICustomerStore 
{
    Task<Order[]> GetUserOrders(String userid);
    Task<Bool> CanUserSeeOrder(String userid, String orderid);
}

реализует соответственно и использует этот класс для систематической проверки, может ли пользователь получить доступ к ресурсам.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...