Валидация и авторизация в многоуровневой архитектуре

13

Я знаю, что вы думаете (или, возможно, кричите), "не другой вопрос, спрашивающий, где валидация относится к многоуровневой архитектуре?!?" Ну, да, но, надеюсь, это будет немного другой взгляд на эту тему.

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

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

public class CashDropApi  // This is in the Service Facade Layer
{
    [WebInvoke(Method = "POST")]
    public void AddCashDrop(NewCashDropContract contract)
    {
        // 1
        Service.AddCashDrop(contract.Amount, contract.DriverId);
    }
}

public class CashDropService  // This is the Application Service in the Domain Layer
{
    public void AddCashDrop(Decimal amount, Int32 driverId)
    {
        // 2
        CommandBus.Send(new AddCashDropCommand(amount, driverId));
    }
}

internal class AddCashDropCommand  // This is a command object in Domain Layer
{
    public AddCashDropCommand(Decimal amount, Int32 driverId)
    {
        // 3
        Amount = amount;
        DriverId = driverId;
    }

    public Decimal Amount { get; private set; }
    public Int32 DriverId { get; private set; }
}

internal class AddCashDropCommandHandler : IHandle<AddCashDropCommand>
{
    internal ICashDropFactory Factory { get; set; }       // Set by IoC container
    internal ICashDropRepository CashDrops { get; set; }  // Set by IoC container
    internal IEmployeeRepository Employees { get; set; }  // Set by IoC container

    public void Handle(AddCashDropCommand command)
    {
        // 4
        var driver = Employees.GetById(command.DriverId);
        // 5
        var authorizedBy = CurrentUser as Employee;
        // 6
        var cashDrop = Factory.CreateCashDrop(command.Amount, driver, authorizedBy);
        // 7
        CashDrops.Add(cashDrop);
    }
}

public class CashDropFactory
{
    public CashDrop CreateCashDrop(Decimal amount, Employee driver, Employee authorizedBy)
    {
        // 8
        return new CashDrop(amount, driver, authorizedBy, DateTime.Now);
    }
}

public class CashDrop  // The domain object (entity)
{
    public CashDrop(Decimal amount, Employee driver, Employee authorizedBy, DateTime at)
    {
        // 9
        ...
    }
}

public class CashDropRepository // The implementation is in the Data Access Layer
{
    public void Add(CashDrop item)
    {
        // 10
        ...
    }
}

Я указал 10 мест, где я видел проверки в коде. Мой вопрос заключается в том, какие проверки вы, если таковые имеются, выполняете для каждого из следующих бизнес-правил (наряду со стандартными проверками длины, диапазона, формата, типа и т. Д.):

  1. Сумма сброса должна быть больше нуля.
  2. Денежный перевод должен иметь действующего водителя.
  3. Текущий пользователь должен быть авторизован для добавления денежных средств (текущий пользователь не является водителем).

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

SonOfPirate
источник
SE не совсем подходящая платформа для «стимулирования теоретического и субъективного обсуждения». Голосование закрыть.
tdammers
Плохо сформулированное заявление. Я действительно ищу лучшие практики.
SonOfPirate
2
@tdammers - Да, это правильное место. По крайней мере, так хочется. Из FAQ: «Субъективные вопросы разрешены». Вот почему они сделали этот сайт вместо Stack Overflow. Не будь близким нацистом. Если вопрос отстой, он исчезнет в безвестности.
FastAl
@FastAI: Меня беспокоит не столько субъективная, сколько дискуссия.
tdammers
Я думаю, что вы могли бы использовать здесь объекты- CashDropAmountзначения, используя объект-значение вместо использования Decimal. Проверка наличия или отсутствия драйвера будет выполнена в обработчике команд, и то же самое касается правил авторизации. Вы можете получить авторизацию бесплатно, выполнив что-то наподобие того, Approver approver = approverService.findById(employeeId)где она выдает, если сотрудник не в роли утверждающего. Approverбудет просто объект значения, а не сущность. Вы также могли бы избавиться от фабрики или использования фабричного метода на AR вместо: cashDrop = driver.dropCash(...).
plalx

Ответы:

2

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

Так, например, в вашем классе CashDropApi я бы только проверял, что «контракт» не является нулевым. Это предотвращает исключения NullReferenceExceptions и все, что необходимо для правильной работы этого метода.

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

Хранилище должно проверять только то, что данные, содержащиеся в объекте, соответствуют схеме, определенной в вашей базе данных, и операция daa будет выполнена успешно. Например, если у вас есть столбец, который не может быть нулевым или имеет максимальную длину, и т. Д.

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

jpm70
источник
1

Ваше первое бизнес-правило

Сумма сброса должна быть больше нуля.

выглядит как инвариант вашей CashDropсущности и вашего AddCashDropCommandкласса. Есть несколько способов, которыми я применяю такой инвариант:

  1. Возьмите маршрут Design By Contract и используйте кодовые контракты с комбинацией предусловий, постусловий и [ContractInvariantMethod] в зависимости от вашего случая.
  2. Напишите явный код в конструкторе / установщике, который выдает исключение ArgumentException, если вы передаете сумму, которая меньше 0.

Ваше второе правило имеет более широкий характер (в свете деталей, указанных в вопросе): действительно ли означает, что у объекта «Водитель» есть флаг, указывающий, что он может управлять автомобилем (то есть не имеет приостановленных водительских прав), означает ли это, что водитель был на самом деле работает в тот день или это просто означает, что идентификатор DriverId, переданный в CashDropApi, действителен в хранилище сохраняемости.

В любом из этих случаев вам нужно будет перемещаться по модели вашего домена и получать Driverэкземпляр от вашего IEmployeeRepository, как вы это делаете location 4в своем примере кода. Итак, здесь вы должны убедиться, что вызов репозитория не возвращает ноль, и в этом случае ваш driverId был недействительным, и вы не можете продолжить обработку дальше.

Для других 2 (моих гипотетических) проверок (есть ли у водителя действительные водительские права, был ли водитель сегодня работающим) вы выполняете бизнес-правила.

Здесь я обычно использую коллекцию классов валидаторов, которые работают с сущностями (точно так же, как шаблон спецификации из книги Эрика Эванса «Дизайн, управляемый доменом»). Я использовал FluentValidation для создания этих правил и валидаторов. Затем я могу составить (и, следовательно, повторно) более сложные / более полные правила из более простых правил. И я могу решить, какие слои в моей архитектуре использовать. Но у меня все они закодированы в одном месте, а не разбросаны по всей системе.

Ваше третье правило относится к сквозной проблеме: авторизация. Поскольку вы уже используете контейнер IoC (при условии, что ваш контейнер IoC поддерживает перехват методов), вы можете выполнить некоторую AOP . Напишите приложение, которое выполняет авторизацию, и вы можете использовать свой контейнер IoC для внедрения этого режима авторизации там, где это необходимо. Большим преимуществом здесь является то, что вы однажды написали логику, но вы можете повторно использовать ее в своей системе.

Чтобы использовать перехват через динамический прокси (Castle Windsor, Spring.NET, Ninject 3.0 и т. Д.), Ваш целевой класс должен реализовать интерфейс или наследовать от базового класса. Вы должны были бы перехватить перед вызовом целевого метода, проверить авторизацию пользователя и запретить переход вызова к фактическому методу (сгенерировать исключение, журнал, вернуть значение, указывающее сбой или что-то еще), если у пользователя нет правильные роли для выполнения операции.

В вашем случае вы можете перехватить вызов либо

CashDropService.AddCashDrop(...) 

AddCashDropCommandHandler.Handle(...)

Проблемы здесь, может быть, CashDropServiceне могут быть перехвачены, потому что нет интерфейса / базового класса. Или AddCashDropCommandHandlerне создается вашим IoC, поэтому ваш IoC не может создать динамический прокси для перехвата вызова. Spring.NET имеет полезную функцию, позволяющую нацеливать метод на класс в сборке с помощью регулярного выражения, поэтому это может сработать.

Надеюсь, что это дает вам некоторые идеи.

RobertMS
источник
Можете ли вы объяснить, как я "использовал бы ваш контейнер IoC для внедрения этого режима авторизации там, где он должен быть"? Это звучит заманчиво, но пока что совместная работа AOP и IoC ускользает от меня.
SonOfPirate
В остальном я согласен с размещением валидации в конструкторе и / или установщиках, чтобы предотвратить переход объекта в недопустимое состояние (обработка инвариантов). Но помимо этого и ссылки на нулевую проверку после перехода в IEmployeeRepository, чтобы найти драйвер, вы не предоставите никаких подробностей, где бы вы выполняли остальную часть проверки. Учитывая использование FluentValidation и его повторное использование и т. Д., Где бы вы применили правила в данной модели?
SonOfPirate
Я отредактировал свой ответ - посмотрите, поможет ли это. Что касается «где бы вы применили правила в данной модели?»; вероятно около 4, 5, 6, 7 в вашем командном обработчике. У вас есть доступ к репозиториям, которые могут предоставить информацию, необходимую для проверки бизнес-уровня. Но я думаю, что есть другие, которые не согласятся со мной здесь.
RobertMS
Чтобы уточнить, все зависимости вводятся. Я оставил это, чтобы сохранить код ссылки кратким. Мой запрос больше связан с наличием зависимости внутри аспекта, поскольку аспекты не вводятся через контейнер. Итак, как AuthorizationAspect получает ссылку на AuthorizationService, например?
SonOfPirate
1

Для правил:

1- сумма сброса наличных должна быть больше нуля.

2- Денежный перевод должен иметь действующего водителя.

3- Текущий пользователь должен быть авторизован, чтобы добавлять денежные сбросы (текущий пользователь не водитель).

Я бы сделал проверку в местоположении (1) для бизнес-правила (1) и удостоверился, что идентификатор не является нулевым или отрицательным (при условии, что ноль действителен) в качестве предварительной проверки для правила (2). Причиной является мое правило: «Не пересекайте границу слоя с неверными данными, которые вы можете проверить с помощью доступной информации». Исключением из этого является то, что служба выполняет проверку как часть своего долга перед другими абонентами. В этом случае будет достаточно провести валидацию только там.

Для правил (2) и (3) это должно быть сделано на уровне доступа к базе данных (или на самом уровне db) только потому, что оно включает доступ к db. Нет необходимости намеренно перемещаться между слоями.

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

Хороший вопрос!

Без шансов
источник
+1 за авторизацию - размещение в пользовательском интерфейсе - альтернатива, которую я не упомянул в своем ответе.
RobertMS
Несмотря на то, что проверки авторизации в пользовательском интерфейсе предоставляют пользователю более интерактивный интерфейс, я разрабатываю основанный на службах API и не могу делать какие-либо предположения относительно того, какие правила вызывающий абонент применял или не реализовал. Поскольку многие из этих проверок можно легко делегировать пользовательскому интерфейсу, я решил использовать проект API в качестве основы для публикации. Я ищу лучшие практики, а не учебник быстро и легко.
SonOfPirate
@SonOfPirate, INMO, пользовательский интерфейс должен выполнять валидацию, потому что он быстрее и содержит больше данных, чем сервис (в некоторых случаях). Теперь служба не должна отправлять данные за пределы своей границы, не выполняя собственных проверок, поскольку это является частью ее обязанностей, если вы хотите, чтобы служба не доверяла клиенту. Соответственно, я предлагаю, чтобы в сервисе (снова) проводились проверки не-db перед отправкой данных в базу данных для дальнейшей обработки.
NoChance