Перевірка та авторизація у багатошаровій архітектурі


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. Поточному користувачеві необхідно дозволити додавати грошові краплі (поточний користувач не є драйвером).

Будь ласка, поділіться своїми думками про те, як ви чи підходили б до цього сценарію та причини вашого вибору.


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. Я бачив (і робив) перевірку обох способів wrt до заводських та юридичних класів. Одне або інше - це те, де ви хочете перевірити значення "сума", а інші параметри не є нульовими (правила вашого бізнесу).

Репозиторій повинен лише підтвердити, що дані, що містяться в об'єкті, відповідають схемі, визначеній у вашій базі даних, і операція daa буде успішною. Наприклад, якщо у вас стовпець, який не може бути нульовим або має максимальну довжину тощо.

Щодо перевірки безпеки, я думаю, що це справді питання про наміри. Оскільки правило призначене для запобігання несанкціонованого доступу, я хотів би зробити цю перевірку якомога раніше в процесі, щоб зменшити кількість непотрібних кроків, які я вжив, якщо користувач не має авторизації. Я, мабуть, поставив би це в CashDropApi.


1

Ваше перше ділове правило

Сума падіння грошових коштів повинна бути більше нуля.

схоже на інваріант вашої CashDropсутності та вашого AddCashDropCommandкласу. Є кілька способів, як я застосовую інваріант, як це:

  1. Перейдіть по маршруту «Дизайн за контрактом» та використовуйте Код-контракти з комбінацією передумов, Пост-умов та [ContractInvariantMethod] залежно від вашого випадку.
  2. Напишіть явний код у конструктор / сеттер, який викидає ArgumentException, якщо ви передаєте суму, меншу за 0.

Ваше друге правило має більш широкий характер (з огляду на деталі у запитанні): чи дійсно означає, що в особі водія є прапор, який вказує, що вони можуть керувати автомобілем (тобто не було призупинено посвідчення водія), чи означає, що водій був насправді працює в цей день або це означає просто, що драйвер ID, переданий в CashDropApi, дійсний у магазині збереження.

У будь-якому з цих випадків вам потрібно буде орієнтуватися на вашу модель домену та отримати Driverекземпляр від вашого IEmployeeRepository, як це робиться у location 4прикладі коду. Отже, тут вам потрібно переконатися, що виклик до сховища не повертає нуль, і в цьому випадку ваш драйвер не був дійсним, і ви не можете продовжувати обробку далі.

Для інших 2 (моїх гіпотетичних) перевірок (чи має водій дійсне посвідчення водія, чи працював водій сьогодні) ви виконуєте ділові правила.

Що я тут схильний робити, це використовувати колекцію класів валідаторів, які працюють над сутностями (подібно до шаблону специфікацій з книги Еріка Еванса - Design Driven Design). Я використовував FluentValidation для створення цих правил і валідаторів. Тоді я можу скласти (і тому повторно використовувати) більш складні / повніші правила з більш простих правил. І я можу вирішити, якими шарами в моїй архітектурі керувати ними. Але у мене вони всі закодовані в одному місці, а не розкидані по всій системі.

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

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

У вашому випадку ви можете перехопити виклик на будь-який

CashDropService.AddCashDrop(...) 

AddCashDropCommandHandler.Handle(...)

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

Сподіваюсь, це дає деякі ідеї.


Чи можете ви пояснити, як я би "використовував ваш контейнер IoC, щоб ввести цю поведінку авторизації там, де це потрібно"? Це звучить привабливо, але змушення AOP та IoC працювати разом покидає мене.
SonOfPirate

Щодо решти, я погоджуюся з розміщенням валідації в конструкторі та / або сеттерах, щоб запобігти введенню об'єкта в недійсний стан (обробка інваріантів). Але крім цього і посилання на перевірку нуля після переходу до IEposleeeRepository, щоб знайти драйвер, ви не надаєте жодних деталей, де ви б виконували решту перевірки. З огляду на використання FluentValidation та повторного використання тощо, де ви б застосували правила у даній моделі?
SonOfPirate

Я відредагував свою відповідь - подивіться, чи це допомагає. Що стосується "де ви б застосували правила у даній моделі?"; ймовірно, близько 4, 5, 6, 7 у вашому обробці команд. У вас є доступ до сховищ, які можуть отримати інформацію, необхідну для перевірки рівня бізнесу. Але я думаю, що тут є інші, які не погоджуються зі мною.
RobertMS

Для уточнення всі залежності вводяться. Я залишив це в вимкненому стані, щоб тримати короткий довідковий код. Мій запит пов'язаний із залежністю в аспекті, оскільки аспекти не вводяться через контейнер. Отже, як, наприклад, AuthorizationAspect отримує посилання на AuthorizationService?
SonOfPirate

1

Для правил:

1- Сума падіння грошових коштів повинна бути більше нуля.

2- У падінні грошових коштів повинен бути дійсний водій.

3- Поточному користувачеві необхідно дозволити додавати грошові краплі (поточний користувач не є драйвером).

Я би зробив валідацію в розташуванні (1) для ділового правила (1) і переконайтеся, що Id не є нульовим або негативним (якщо припустимо, що нуль дійсний) як попередня перевірка на правило (2). Причини - це моє правило "Не перетинати межу шару з неправильними даними, які можна перевірити за наявною інформацією". Винятком з цього стане, якщо служба виконує перевірку як частину свого обов'язку перед іншими абонентами. У такому випадку достатньо мати перевірку лише там.

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

Зокрема, правила (3) можна уникнути, якщо дозволити GUI запобігти несанкціонованому користувачеві натискати кнопку, що дозволяє цей сценарій. Хоча це важче кодувати, але краще.

Гарне питання!


+1 для авторизації - розміщення його в інтерфейсі є альтернативою, яку я не згадував у своїй відповіді.
RobertMS

Незважаючи на те, що перевірка авторизації в інтерфейсі користувача, забезпечує інтерактивну практику для користувача, я розробляю API, що базується на сервісах, і не можу робити жодних припущень щодо правил, які абонент має або не застосовував. Саме тому, що стільки цих перевірок можна легко делегувати в інтерфейс, я вирішив використовувати проект API як основу для публікації. Я шукаю кращі практики, а не підручник швидко і легко.
SonOfPirate

@SonOfPirate, INMO, інтерфейс повинен виконувати перевірки, оскільки він швидший і він має більше даних, ніж сервіс (в деяких випадках). Тепер сервіс не повинен надсилати дані за межі своєї межі, не роблячи власних перевірок, оскільки це частина його обов'язків, якщо ви хочете, щоб служба не довіряла клієнту. Відповідно, я пропоную в службі (знову ж таки) зробити неперевірені перевірки перед відправленням даних у базу даних для подальшої обробки.
NoChance
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.