Convalida e autorizzazione nell'architettura a strati

13

So che stai pensando (o forse stai urlando), "non un'altra domanda che chiede dove la validazione appartiene a un'architettura a strati?!?" Beh, sì, ma si spera che questo sia un po 'un approccio diverso sull'argomento.

Sono fermamente convinto che la convalida abbia molte forme, sia basata sul contesto e vari a ogni livello dell'architettura. Questa è la base per il post - aiutando a identificare quale tipo di convalida deve essere eseguita in ogni livello. Inoltre, una domanda che emerge spesso è dove i controlli di autorizzazione appartengono.

Lo scenario di esempio viene da un'applicazione per un'azienda di catering. Periodicamente durante il giorno, un conducente può consegnare all'ufficio qualsiasi somma in eccesso accumulata durante il trasporto del camion da un sito all'altro. L'applicazione consente a un utente di registrare il "contante" raccogliendo l'ID del conducente e l'importo. Ecco un codice di scheletro per illustrare gli strati coinvolti:

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

Ho indicato 10 località in cui ho visto i controlli di convalida inseriti nel codice. La mia domanda è quali verifiche si eseguiranno, se del caso, ad ogni dato le seguenti regole aziendali (insieme ai controlli standard per lunghezza, intervallo, formato, tipo, ecc.):

  1. L'importo della caduta di denaro deve essere maggiore di zero.
  2. La caduta di denaro deve avere un Driver valido.
  3. L'utente corrente deve essere autorizzato ad aggiungere cash drop (l'utente corrente non è il driver).

Per favore condividi i tuoi pensieri, come hai o vorrebbe avvicinarti a questo scenario e i motivi delle tue scelte.

    
posta SonOfPirate 19.04.2012 - 21:53
fonte

3 risposte

2

Sono d'accordo che ciò che stai convalidando sarà diverso in ogni strato dell'applicazione. In genere, convalido solo ciò che è necessario per eseguire il codice nel metodo corrente. Cerco di trattare i componenti sottostanti come scatole nere e non convalidare in base a come questi componenti sono implementati.

Quindi, ad esempio, nella tua classe CashDropApi, verificherei solo che 'contratto' non è nullo. Ciò impedisce NullReferenceExceptions ed è tutto ciò che è necessario per garantire che questo metodo venga eseguito correttamente.

Non so che avrei convalidato nulla nel servizio o nelle classi di comando e il gestore avrebbe solo verificato che 'comando' non fosse nullo per gli stessi motivi della classe CashDropApi. Ho visto (e fatto) la convalida in entrambi i modi con le classi factory e entity. Uno o l'altro è dove vorresti convalidare il valore di 'importo' e che gli altri parametri non siano nulli (le tue regole aziendali).

Il repository dovrebbe solo convalidare che i dati contenuti nell'oggetto siano coerenti con lo schema definito nel database e che l'operazione daa abbia esito positivo. Ad esempio, se hai una colonna che non può essere nullo o ha una lunghezza massima, ecc.

Per quanto riguarda il controllo di sicurezza, penso che sia davvero una questione di intenti. Poiché la regola ha lo scopo di impedire l'accesso non autorizzato, vorrei effettuare questo controllo il più presto possibile per ridurre il numero di passaggi non necessari che ho intrapreso se l'utente non è autorizzato. Probabilmente lo inserirò in CashDropApi.

    
risposta data 20.04.2012 - 14:24
fonte
1

La tua prima regola aziendale

The amount of the cash drop must be greater than zero.

sembra un invariante della tua entità CashDrop e della tua classe AddCashDropCommand . Ci sono un paio di modi in cui impongo un'invarianza come questa:

  1. Segui la rotta Design by Contract e utilizza il codice contratti con una combinazione di precondizioni, postcondizioni e a [ContractInvariantMethod] a seconda del caso.
  2. Scrivi il codice esplicito nel costruttore / setter che lancia un ArgumentException se passi un importo inferiore a 0.

La tua seconda regola è di natura più ampia (alla luce dei dettagli nella domanda): valido significa che l'entità Driver ha una bandiera che indica che possono guidare (cioè non ha la licenza di guida sospesa), significa che il driver stava funzionando in quel giorno o significa semplicemente che l'ID driver, passato a CashDropApi, è valido nell'archivio di persistenza.

In uno di questi casi dovrai navigare nel tuo modello di dominio e ottenere l'istanza Driver dal tuo IEmployeeRepository , come fai in location 4 nell'esempio di codice. Quindi, qui devi assicurarti che la chiamata al repository non restituisca null, nel qual caso il tuo driverId non era valido e non puoi procedere ulteriormente con l'elaborazione.

Per gli altri 2 (i miei ipotetici) assegni (il driver ha una patente di guida valida, il driver funzionava oggi) stai eseguendo le regole aziendali.

Quello che tendo a fare qui è usare una raccolta di classi di validatori che operano su entità (proprio come il specifica dal libro di Eric Evans - Domain Driven Design). Ho usato FluentValidation per costruire queste regole e validatori. Posso quindi comporre (e quindi riutilizzare) regole più complesse / più complete con regole più semplici. E posso decidere quali livelli della mia architettura eseguirli. Ma li ho tutti codificati in un unico posto, non sparsi sul sistema.

La terza regola riguarda una preoccupazione trasversale: l'autorizzazione. Dato che stai già utilizzando un contenitore IoC (supponendo che il tuo contenitore IoC supporti l'intercettazione dei metodi) puoi fare un AOP . Scrivi un apsect che fa l'autorizzazione e puoi usare il tuo contenitore IoC per iniettare questo comportamento di autorizzazione dove deve essere. La grande vittoria qui è che hai scritto la logica una volta, ma puoi riutilizzarla attraverso il tuo sistema.

Per utilizzare l'intercettazione tramite un proxy dinamico (Castle Windsor, Spring.NET, Ninject 3.0, ecc.) la classe di destinazione deve implementare un'interfaccia o ereditare da una classe base. Intercetterai prima della chiamata al metodo di destinazione, controllerai l'autorizzazione dell'utente e impedirai alla chiamata di passare al metodo effettivo (lanciare un'aspirazione, registrare, restituire un valore che indica un errore o qualcos'altro) se l'utente non ha i ruoli giusti per eseguire l'operazione.

Nel tuo caso potresti intercettare la chiamata in

CashDropService.AddCashDrop(...) 

AddCashDropCommandHandler.Handle(...)

I problemi qui potrebbero forse essere che CashDropService non può essere intercettato perché non esiste una classe interfaccia / base. Oppure AddCashDropCommandHandler non viene creato dal tuo IoC, quindi il tuo IoC non può creare un proxy dinamico per intercettare la chiamata. Spring.NET ha una funzione utile in cui puoi scegliere come target un metodo su una classe in un assembly tramite un'espressione regolare, quindi potrebbe funzionare.

Spero che questo ti dia qualche idea.

    
risposta data 15.06.2012 - 00:08
fonte
1

Per le regole:

1- The amount of the cash drop must be greater than zero.

2- The cash drop must have a valid Driver.

3- The current user must be authorized to add cash drops (current user is not the driver).

Effettuerò la validazione in posizione (1) per la regola aziendale (1) e assicurarmi che l'Id non sia nullo o negativo (supponendo che lo zero sia valido) come pre-controllo per la regola (2). La ragione è la mia regola "Non attraversare un limite di livello con dati errati che puoi verificare con le informazioni disponibili". Un'eccezione a questo sarebbe se il servizio fa la convalida come parte del suo dovere verso altri chiamanti. In tal caso, sarà sufficiente avere la convalida solo lì.

Per le regole (2) e (3), questo deve essere fatto a livello di accesso al database (o al livello db stesso) solo perché implica l'accesso db. Non c'è bisogno di viaggiare tra i livelli intenzionalmente.

In particolare la regola (3) può essere evitata se lasciamo che la GUI impedisca agli utenti non autorizzati di premere il pulsante abilitando questo scenario. Anche se questo è più difficile da codificare, è meglio.

Buona domanda!

    
risposta data 02.07.2012 - 17:08
fonte

Leggi altre domande sui tag