Asp.net Core WebAPI Авторизация на основе ресурсов вне уровня контроллера

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

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

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

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

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

Обновить

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

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

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


person Mozart Al Khateeb    schedule 16.04.2020    source источник
comment
Вы можете использовать такой маршрут, как users/{userId}/orders/{orderId}, и использовать SQL-запрос, подобный этому, где OwnerId = @userId и OrderId = @orderId. Дополнительно добавьте фильтр авторизации, чтобы убедиться, что значение параметра маршрута userId совпадает со значением аутентифицированного пользователя.   -  person Sergey Vishnevskiy    schedule 22.04.2020
comment
@SergeyVishnevsky Ну, это идея, но в итоге я получу идентификатор пользователя в каждом маршруте, когда я смогу просто получить UserId из httpContext, и мне придется добавить настраиваемые маршруты для администраторов, которым не принадлежат заказы . Я пытаюсь найти более чистый стандартный подход.   -  person Mozart Al Khateeb    schedule 22.04.2020
comment
Чего именно вы хотите добиться? Вы хотите разрешить пользователю получать только его заказы? Или вы хотите создать роли пользователей для разных заказов?   -  person Oleg Kyrylchuk    schedule 22.04.2020
comment
Как я вижу, авторизация на основе политик с несколькими обработчиками docs.microsoft.com/en-us/aspnet/core/security/authorization/ может помочь. Вы можете использовать параметр маршрута ownerId в качестве индикатора, который является владельцем ресурса для всех маршрутов, и добавить два или более AuthorizationHandler для требования авторизации. Первая проверяет, что текущий идентификатор пользователя равен значению маршрута ownerId, вторая проверяет, что текущий пользователь является администратором. Если один из этих обработчиков одобрил требование, то запрос авторизован.   -  person Sergey Vishnevskiy    schedule 22.04.2020
comment
@OlegKyrylchuk См. обновленный вопрос   -  person Mozart Al Khateeb    schedule 22.04.2020
comment
@SergeyVishnevsky Скоро проверю   -  person Mozart Al Khateeb    schedule 22.04.2020


Ответы (6)


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

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

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

person Oleg Kyrylchuk    schedule 23.04.2020
comment
Ресурсы в моем случае могут быть документами, но могут быть и записями базы данных. - person Mozart Al Khateeb; 25.04.2020
comment
@MozartAlKhateeb да, ресурс может быть любым объектом базы данных. - person Oleg Kyrylchuk; 25.04.2020

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

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

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

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

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
}

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

person Matt Hensley    schedule 23.04.2020
comment
В этом случае все сущности с реализацией IAccountOwnedEntity? тогда у меня есть идентификатор пользователя во всех таблицах? - person Mozart Al Khateeb; 25.04.2020
comment
Да это верно. Теоретически, если запись относится к определенной учетной записи, они будут связаны вместе в базе данных. - person Matt Hensley; 26.04.2020
comment
это кажется мне слишком инженерным... Очевидно, это зависит от многих сущностей, которые могут быть у вас, и от того, сколько у вас разных запросов... но это два интерфейса класса два для одного, где - person L.Trabacchin; 29.04.2020
comment
также заявленный вопрос. Конечно, я могу получить JWT из запроса и запросить базу данных, чтобы проверить, владеет ли этот пользователь этим заказом, но это сделает действия моего контроллера слишком тяжелыми. этот ответ не касается этого, мой не так, но, по крайней мере, я объяснил, почему ... - person L.Trabacchin; 29.04.2020
comment
Кроме того, такой подход, как правило, делает ваш проект более жестким, заданным... сценарии реального мира не могут соответствовать определению владельца, вполне вероятно, что вам понадобятся создатели, редакторы, администраторы, компания, которая управляет этими объектами... Я бы не стал следовать этому подходу, просто мои два цента... - person L.Trabacchin; 29.04.2020

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

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

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

void ValidateUserOwnsOrder(Order order, string username)
{
            if (order.username != username)
            {
                throw new Exception("Wrong user.");
            }
}
person Daniel Stackenland    schedule 28.04.2020

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

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

или замените "UserA" на "Accounting" и "Name" на "Role".
Затем ограничьте методы контроллера по роли:

[Authorize(Policy = "UserA")

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

person Squirrelkiller    schedule 22.04.2020
comment
См. обновленный вопрос. Роли не гарантируют, что пользователь A не сможет получить доступ к данным пользователя B. Возможно, политики можно использовать по-другому, чтобы учитывать пользователя текущего сеанса при его проверке, я изучу это. - person Mozart Al Khateeb; 22.04.2020
comment
Ну, другой уровень зависит от того, где вы хотите определить, кому какие данные принадлежат. Например, у вас может быть уровень сохраняемости, который принимает токен пользователя в качестве дополнительного аргумента для проверки пользователя, сохраненного в базе данных с этими данными. - person Squirrelkiller; 22.04.2020
comment
Имена политик в [Authorize] должны быть постоянными значениями, что означает, что вам нужно будет иметь новое постоянное значение для каждого пользователя, которого вы хотите добавить. Это не похоже на то, что он будет очень хорошо масштабироваться после 1 или двух пользователей. - person Matt Hensley; 23.04.2020
comment
@MattHensley Именно поэтому я добавил идею роли. Просто поместите UserA туда, чтобы технически соответствовать вопросу OP. - person Squirrelkiller; 23.04.2020

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

Излишняя оптимизация — корень всех зол

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

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

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

что-то вроде

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

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

person L.Trabacchin    schedule 23.04.2020
comment
Что ж, отличная цель этого вопроса - услышать о стандартных подходах и указать, что не так. Этот API все еще находится в стадии разработки, в прошлом я использовал идентификатор пользователя для отправки запроса в базу данных. Итак, что касается ICustomerStore, это IService или IRepo, которые отвечают за запрос данных? - person Mozart Al Khateeb; 23.04.2020
comment
Ну, это зависит от... Мой опыт заключается в том, чтобы делать все как можно быстрее, следуя некоторым шаблонам... Затем рефакторинг того, что, по вашему мнению, требует дополнительного внимания. так что не кладите слишком много слоев. хороший способ принять решение и выполнить некоторые тесты - написать требования в виде тестов, это даст вам некоторое представление о том, сколько слоев вам понадобится. - person L.Trabacchin; 23.04.2020
comment
вы используете какую-то форму? как структура сущности? я обычно разрабатываю эти хранилища как слой между инфраструктурой сущности (БД) и контроллером/webapi, поэтому последнему не нужно ничего знать об источнике данных, обмен хранилища с макетом также должен работать, если вы чувствуете необходимость для доступа к базе данных просто добавьте несколько методов в хранилище - person L.Trabacchin; 23.04.2020
comment
Да, я использую структуру сущностей, исходя из вашего описания, это может быть сервисный уровень, который инкапсулирует запросы БД вместе с бизнес-логикой. - person Mozart Al Khateeb; 23.04.2020
comment
абсолютно да, эти данные могут поступать из любого источника, вы просто создаете сокет. Затем, если это недостаточно быстро, вы создадите кеш для своей службы, может быть еще одна меньшая таблица в БД, может быть в памяти, все, что вам нравится и работает. - person L.Trabacchin; 23.04.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 проверить контекст и обойти наивные проверки соответствия пользователя/заказа, если императивные проверки были «объявлены». '.

person Mark Churchill    schedule 29.04.2020