Walidacja i autoryzacja w architekturze warstwowej


13

Wiem, że myślisz (a może krzyczysz): „nie ma innego pytania, gdzie należy sprawdzić poprawność w architekturze warstwowej?!?” Cóż, tak, ale mam nadzieję, że będzie to trochę inne spojrzenie na ten temat.

Jestem głęboko przekonany, że sprawdzanie poprawności przybiera wiele form, jest oparte na kontekście i różni się na każdym poziomie architektury. To jest podstawa do postu - pomaga określić, jaki rodzaj walidacji powinien zostać wykonany na każdej warstwie. Ponadto często pojawia się pytanie, gdzie należą kontrole autoryzacji.

Przykładowy scenariusz pochodzi z aplikacji dla firmy cateringowej. Okresowo w ciągu dnia kierowca może zwrócić się do biura z nadwyżką gotówki zgromadzonej podczas transportu ciężarówki z miejsca na miejsce. Aplikacja pozwala użytkownikowi zarejestrować „wypłatę gotówki” poprzez zebranie identyfikatora kierowcy i kwoty. Oto szkielet kodu ilustrujący zaangażowane warstwy:

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
        ...
    }
}

Wskazałem 10 lokalizacji, w których widziałem sprawdzanie poprawności umieszczone w kodzie. Moje pytanie dotyczy tego, jakie kontrole przeprowadzilibyście przy każdej z następujących reguł biznesowych (wraz ze standardowymi kontrolami długości, zakresu, formatu, typu itp.):

  1. Kwota zrzutu gotówki musi być większa od zera.
  2. Upuszczenie gotówki musi mieć ważnego Kierowcę.
  3. Bieżący użytkownik musi być upoważniony do dodawania zrzutów gotówki (bieżący użytkownik nie jest kierowcą).

Proszę podzielić się swoimi przemyśleniami, jak masz lub podejmiesz ten scenariusz i powody swoich wyborów.


SE nie jest właściwą platformą do „wspierania teoretycznej i subiektywnej dyskusji”. Głosowanie na zakończenie.
tdammers

Źle sformułowane oświadczenie. Naprawdę szukam najlepszych praktyk.
SonOfPirate

2
@tdammers - Tak, to właściwe miejsce. Przynajmniej tak chce. Z FAQ: „Subiektywne pytania są dozwolone”. Właśnie dlatego stworzyli tę witrynę zamiast Przepełnienia stosu. Nie bądź bliskim nazistą. Jeśli pytanie jest do bani, zniknie w zapomnieniu.
FastAl

@FastAI: Niepokoi mnie nie tyle „subiektywna” część, co raczej „dyskusja”.
tdammers

Myślę, że można tutaj wykorzystać obiekty wartości, mając CashDropAmountobiekt wartości zamiast używać Decimal. Sprawdzenie, czy sterownik istnieje, czy nie, zostanie wykonane w module obsługi poleceń i to samo dotyczy reguł autoryzacji. Możesz uzyskać autoryzację za darmo, robiąc coś w taki sposób, w Approver approver = approverService.findById(employeeId)jaki wyrzuca, jeśli pracownik nie pełni roli osoby zatwierdzającej. Approverbyłby tylko obiektem wartości, a nie bytem. Można też pozbyć się fabryki lub użyć metody fabryki na AR zamiast: cashDrop = driver.dropCash(...).
plalx

Odpowiedzi:


2

Zgadzam się, że to, co zatwierdzasz, będzie różne w każdej warstwie aplikacji. Zazwyczaj sprawdzam tylko to, co jest wymagane do wykonania kodu w bieżącej metodzie. Staram się traktować podstawowe komponenty jako czarne skrzynki i nie sprawdzam ich poprawności w oparciu o sposób ich implementacji.

Na przykład w klasie CashDropApi sprawdziłbym tylko, czy „kontrakt” nie jest zerowy. Zapobiega to NullReferenceExceptions i jest wszystkim, co jest potrzebne do zapewnienia prawidłowego działania tej metody.

Nie wiem, czy sprawdziłbym cokolwiek w klasie usługi lub polecenia, a moduł obsługi sprawdzałby tylko, czy „polecenie” nie ma wartości zerowej z tych samych powodów, co w klasie CashDropApi. Widziałem (i zrobiłem) walidację w obie strony w odniesieniu do klas fabryki i encji. Jedno lub drugie to miejsce, w którym chcesz zweryfikować wartość „kwoty”, a pozostałe parametry nie mają wartości zerowej (reguły biznesowe).

Repozytorium powinno sprawdzać tylko, czy dane zawarte w obiekcie są zgodne ze schematem zdefiniowanym w bazie danych, a operacja daa zakończy się powodzeniem. Na przykład, jeśli masz kolumnę, która nie może być pusta lub ma maksymalną długość itp.

Jeśli chodzi o kontrolę bezpieczeństwa, myślę, że to naprawdę kwestia zamiaru. Ponieważ reguła ma zapobiegać nieautoryzowanemu dostępowi, chciałbym przeprowadzić tę kontrolę tak wcześnie, jak to możliwe, aby zmniejszyć liczbę niepotrzebnych kroków, które podjąłem, jeśli użytkownik nie jest autoryzowany. Prawdopodobnie umieściłbym to w CashDropApi.


1

Twoja pierwsza reguła biznesowa

Kwota zrzutu gotówki musi być większa od zera.

wygląda jak niezmiennik twojej CashDropistoty i twojej AddCashDropCommandklasy. Istnieje kilka sposobów egzekwowania niezmiennika takiego jak ten:

  1. Wybierz trasę według umowy i korzystaj z kodów umów z kombinacją warunków wstępnych, warunków dodatkowych i [ContractInvariantMethod] w zależności od przypadku.
  2. Napisz jawny kod w konstruktorze / ustawieniach, który zgłasza ArgumentException, jeśli przekażesz w ilości mniejszej niż 0.

Twoja druga zasada ma szerszy charakter (w świetle szczegółów w pytaniu): czy ważna oznacza, że ​​jednostka Kierowcy ma flagę wskazującą, że może prowadzić (tj. Nie zawieszono jej prawa jazdy), czy oznacza to, że kierowca był faktycznie działa tego dnia, czy oznacza to po prostu, że driverId, przekazany do CashDropApi, jest ważny w sklepie trwałości.

W każdym z tych przypadków będziesz musiał nawigować w swoim modelu domeny i pobrać Driverinstancję z własnego IEmployeeRepository, tak jak w location 4przykładzie kodu. Tak więc tutaj musisz upewnić się, że wywołanie do repozytorium nie zwraca null, w którym to przypadku sterownik nie był prawidłowy i nie możesz kontynuować przetwarzania.

W przypadku pozostałych 2 (moich hipotetycznych) kontroli (czy kierowca ma ważne prawo jazdy, czy kierowca pracuje dzisiaj) przestrzegasz reguł biznesowych.

To, co zwykle robię, to użycie kolekcji klas walidacyjnych, które działają na jednostkach (podobnie jak wzorzec specyfikacji z książki Erica Evansa - Domain Driven Design). Użyłem FluentValidation do zbudowania tych reguł i walidatorów. Mogę następnie skomponować (a zatem ponownie wykorzystać) bardziej złożone / pełniejsze reguły z prostszych reguł. I mogę zdecydować, które warstwy w mojej architekturze je uruchomić. Ale mam je wszystkie zakodowane w jednym miejscu, a nie rozrzucone po całym systemie.

Twoja trzecia zasada dotyczy zagadnienia przekrojowego: autoryzacji. Ponieważ używasz już kontenera IoC (zakładając, że twój kontener IoC obsługuje przechwytywanie metod), możesz zrobić AOP . Napisz apkę, która wykonuje autoryzację i możesz użyć kontenera IoC do wstrzyknięcia tego zachowania autoryzacji tam, gdzie musi. Wielką wygraną jest to, że raz napisałeś logikę, ale możesz jej ponownie użyć w całym systemie.

Aby korzystać z przechwytywania za pośrednictwem dynamicznego serwera proxy (Castle Windsor, Spring.NET, Ninject 3.0 itp.), Klasa docelowa musi zaimplementować interfejs lub odziedziczyć po klasie podstawowej. Przechwycisz przed wywołaniem metody docelowej, sprawdzisz autoryzację użytkownika i zapobiegniesz przejściu wywołania do rzeczywistej metody (rzucisz wykrzyknik, zarejestrujesz, zwrócisz wartość wskazującą błąd lub coś innego), jeśli użytkownik nie ma odpowiednie role do wykonania operacji.

W twoim przypadku możesz przechwycić połączenie do obu

CashDropService.AddCashDrop(...) 

AddCashDropCommandHandler.Handle(...)

Problemy tutaj CashDropServicemogą być nie do przechwycenia, ponieważ nie ma interfejsu / klasy bazowej. Lub AddCashDropCommandHandlernie jest tworzony przez IoC, dlatego Twój IoC nie może utworzyć dynamicznego proxy do przechwycenia połączenia. Spring.NET ma przydatną funkcję, w której można kierować metodę na klasę w zespole za pomocą wyrażenia regularnego, więc może to działać.

Mam nadzieję, że daje to kilka pomysłów.


Czy możesz wyjaśnić, w jaki sposób „użyłbym twojego kontenera IoC, aby wprowadzić to zachowanie autoryzacji tam, gdzie powinno”. Brzmi atrakcyjnie, ale do tej pory ucieka mnie współpraca AOP i IoC.
SonOfPirate

Jeśli chodzi o resztę, zgadzam się na umieszczenie sprawdzania poprawności w konstruktorze i / lub seterach, aby zapobiec wprowadzeniu niepoprawnego stanu obiektu (obsługa niezmienników). Ale poza tym i odniesieniem do kontroli zerowej po przejściu do IEmployeeRepository w celu zlokalizowania sterownika, nie podajesz żadnych szczegółów, w których przeprowadziłbyś resztę weryfikacji. Biorąc pod uwagę wykorzystanie FluentValidation i ponowne użycie itp., Jakie to zapewnia, gdzie miałbyś zastosować reguły w danym modelu?
SonOfPirate

Zredagowałem swoją odpowiedź - sprawdź, czy to pomoże. Co do „gdzie zastosowałbyś reguły w danym modelu?”; prawdopodobnie około 4, 5, 6, 7 w module obsługi poleceń. Masz dostęp do repozytoriów, które mogą dostarczyć informacji potrzebnych do przeprowadzenia weryfikacji na poziomie biznesowym. Ale myślę, że są tu inni, którzy by się ze mną nie zgodzili.
RobertMS

Aby wyjaśnić, wstrzykiwane są wszystkie zależności. Zostawiłem to, aby kod referencyjny był krótki. Moje zapytanie dotyczy bardziej zależności od aspektu, ponieważ aspekty nie są wstrzykiwane przez pojemnik. W jaki sposób, na przykład, AuthorizationAspect otrzymuje odniesienie do usługi AuthorizationService?
SonOfPirate

1

Do zasad:

1- Kwota wypłaty gotówki musi być większa od zera.

2- Upuszczenie gotówki musi mieć ważnego Kierowcę.

3- Bieżący użytkownik musi być upoważniony do dodawania zrzutów gotówki (obecny użytkownik nie jest kierowcą).

Zrobiłbym sprawdzanie poprawności w lokalizacji (1) dla reguły biznesowej (1) i upewnienie się, że identyfikator nie jest zerowy lub ujemny (zakładając, że zero jest prawidłowe) jako wstępne sprawdzenie reguły (2). Powodem jest moja zasada: „Nie przekraczaj granicy warstwy z niewłaściwymi danymi, które możesz sprawdzić za pomocą dostępnych informacji”. Wyjątkiem jest sytuacja, w której usługa dokonuje walidacji w ramach obowiązku wobec innych dzwoniących. W takim przypadku walidacja będzie wystarczająca tylko tam.

W przypadku reguł (2) i (3) należy to zrobić tylko w warstwie dostępu do bazy danych (lub w samej warstwie bazy danych), ponieważ wymaga to dostępu do bazy danych. Nie trzeba celowo podróżować między warstwami.

W szczególności można uniknąć zasady (3), jeśli pozwolimy, aby GUI uniemożliwiał nieautoryzowanym użytkownikom naciśnięcie przycisku włączającego ten scenariusz. Chociaż kodowanie jest trudniejsze, jest lepsze.

Dobre pytanie!


+1 za autoryzację - umieszczenie go w interfejsie użytkownika jest alternatywą, o której nie wspomniałem w mojej odpowiedzi.
RobertMS

Chociaż sprawdzanie autoryzacji w interfejsie użytkownika zapewnia bardziej interaktywne doświadczenie dla użytkownika, opracowuję interfejs API oparty na usługach i nie mogę poczynić żadnych założeń dotyczących zasad, które program wywołujący wprowadził lub nie wdrożył. To dlatego, że tak wiele z tych kontroli można łatwo przekazać do interfejsu użytkownika, postanowiłem użyć projektu interfejsu API jako podstawy wpisu. Szukam najlepszych praktyk, a nie podręcznika szybkiego i łatwego.
SonOfPirate

@ SonOfPirate, INMO, interfejs użytkownika musi przeprowadzać weryfikacje, ponieważ jest szybszy i ma więcej danych niż usługa (w niektórych przypadkach). Teraz usługa nie powinna wysyłać danych poza swoje granice bez przeprowadzania własnych weryfikacji, ponieważ jest to część jej obowiązków, o ile nie chcesz, aby usługa nie ufała klientowi. W związku z tym sugeruję, aby w usłudze (ponownie) wykonać kontrole inne niż db przed przesłaniem danych do bazy danych w celu dalszego przetwarzania.
NoChance
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.