User Tools

Site Tools


patterns:inheritancerolestragegyoverview:strategy

This is an old revision of the document!


Das Strategie Muster

Strategien sind mächtige und sinnvolle Werkzeuge für Verhalten, das austauschbar ist. Zudem kann das Verhalten von allen abgeleiteten Klassen geerbt werden. Ein tolles Beispiel hierfür ist die Authentifizierung eines Kontos. Es gibt diverse Möglichkeiten1) wie ein Konto authentifiziert werden kann. Beispiele sind:

  • über die Angabe eines Benutzernamens und eines Kennwortes,
  • über eine vorhandene Authentifizierung, z.B. wenn ein Anwender bereits an Windows angemeldet ist,
  • über ein X.509 Zertifikat,
  • über einen Dongle,
  • gar nicht,
  • usw.

Wenn die Authentifizierung als Strategie umgesetzt wird, kann man sich erst mal für eine entscheiden. Die anderen können dann bei Bedarf dazu gebaut werden, und in einigen Fällen werden neue Strategien dazu kommen, die zum jetzigen Zeitpunkt noch gar nicht bekannt waren.

Eine Strategie wird vorbereitend in ein Aggregate eingebaut2). Sie wird abstrakt definiert. Sie kann beim Erzeugen des Aggregates gesetzt werden und kann Implementationsabhängig danach verändert werden oder nicht. Wie das Ganze in CQRS/ES funktioniert, kann am besten an einem Beispiel erläutert werden.

Die Benutzernamen Strategie zur Authentifizierung

Konstruktor des Aggregates mit Strategien

Da Strategien austauschbar und erweiterbar sind, muss das Aggregate wissen, wie es eine Strategie laden kann. Dafür gibt es eine ISelectAuthenticationStrategy Schnittstelle

public interface ISelectAuthenticationStrategy
{
    AuthenticationStrategy Load(AuthenticationStrategyDTO);
}

Im Konstruktor des Aggregates wird dieses Interface übergeben

public abstract Account: Aggregate
{
    private ISelectAuthenticationStrategy AuthStrategySelector;
    public Account(ISelectAuthenticationStrategy authStrategySelector) { AuthStrategySelector = authStrategySelector; }
    public Account(Guid id, ISelectAuthenticationStrategy authStrategySelector)
    {
        AuthStrategySelector = authStrategySelector;
        ...
    }
}

Das Aggregate3) wird entweder vom Command Handler neu erzeugt, oder vom Repository wenn es erneut geladen wird. Der Command Handler wird ein neuen UserAccount z.B. wie folgt erzeugen4)

public void Execute(AddUserAccount message, SecurityPrincipal principal)
{
    var authSelector = ContextRegistry.GetContext().GetObject("AccountAuthenticationStrategySelector") as ISelectAuthenticationStrategy;
    var authStrategyConverter = ContextRegistry.GetContext().GetObject("AccountAuthenticationStrategyConverter") as IConvertAuthenticationStrategy;
    var UserAccount = new UserAccount(message.AggregateId, message.PersonId, authStrategyConverter.convert(message.AuthenticationStrategyDTO, authSelector));
    _repository.Save(UserAccount);

IConvertAuthenticationStrategy kann dieselbe Implementierung wie ISelectAuthenticationStrategy verwenden, falls das AuthenticationStrategyDTO dieselbe Klasse ist wie das AuthenticationStrategyDTO das im Domain Event zum Speichern des Ereignisses verwendet wird5). Das Repository kann beim Erzeugen eines Accounts nun ebenfalls das korrekte ISelectAuthenticationStrategy Interface instanziieren und damit den UserAccount leer erzeugen, bevor die Ereignisse angewendet werden. Die Implementierung von ISelectAuthenticationStrategy wird in aller Regel einen ServiceLocator verwenden, dazu gleich mehr.

Es wird die abstrakte Klasse AuthenticationStrategyDTO geben6), mit bestimmten Implementierungen7)

[DataContract]
public abstract AuthenticationStrategyDTO { }
 
[DataContract]
public UsernameAuthenticationStrategyDTO: AuthenticationStrategyDTO
{
    [DataMember]
    public string UserName { get; private set; }
    [DataMember]
    public string Password { get; private set; }
    public UsernameAuthenticationStrategyDTO(string userName, string password)
    {
        UserName = userName;
        Password = password;
    }
}

Ebenso wird es die tatsächlichen Strategien geben8)

public abstract AuthenticationStrategy { }
 
public UsernameAuthenticationStrategy: AuthenticationStrategy
{
    public string UserName { get; private set; }
    public string Password { get; private set; }
    public UsernameAuthenticationStrategy(string userName, string password)
    {
        UserName = userName;
        Password = password;
    }
 
    public UsernameAuthenticationStrategyDTO Serialize()
    {
        return new UsernameAuthenticationStrategyDTO(UserName, Password);
    }
 
    // hier die tatsächliche Funktionalität
}

Wenn der Konstruktor9) von UserAccount über den CommandHandler aufgerufen wird, wird nach erfolgreichen Prüfungen u.a. das Setzen der Strategie vorgenommen

public abstract UserAccount: Account
{
    public UserAccount(ISelectAuthenticationStrategy authStrategySelector)
        : base(authStrategySelector) { }
    public UserAccount(Guid id, AuthenticationStrategy authStrategy, ISelectAuthenticationStrategy authStrategySelector)
        : base(authStrategySelector)
    {
        // Prüfungen
        ApplyChange(new AuthenticationStrategySet(authStrategy.Serialize()); // es wird also das DTO in das Ereignis gepackt
    }
}

Das Ereignis AuthenticationStrategySet muss nun angewendet werden. Das passiert in der Basisklasse Account

public class Account: AggregateRoot
{
    private AuthenticationStrategy authenticationStrategy; // man braucht in der Domäne ja nur Eigenschaften die die Business Logik betreffen. In diesem Fall muss das Konto Befehle an die Strategie weiterleiten, d.h. sie muss zwingend gehalten werden
    ....
    private void Apply(AuthenticationStrategySet e)
    {
        authenticationStrategy = ISelectAuthenticationStrategy.Load(e.AuthenticationStrategyDTO);
    }
}

Da Apply auch vom Repository aufgerufen wird wenn das Konto erneut geladen wird, haben wir jetzt einen relativ einfachen Weg gefunden, um Strategien zu setzen und das Ergebnis zu speichern. Zudem kann der Strategie in ISelectAuthenticationStrategy.Load() alles Mögliche übergeben werden, was dem Konto unbekannt ist. Im Falle einer Kerberos Strategie z.B. eine Verschlüsselungsschnittstelle für die Tickets. Die Veschlüsselung kann zwar Teil der Kerberos Strategie sein, da das Konto aber nach erfolgreicher Ticket Erstellung nicht erneut verwendet wird solange das Ticket gültig ist, müssen sich die Strategie und der Ticket Authentifizierungsdienst eine Verschlüsselung teilen. Diese soll also nicht fest in der Domäne verankert sein, sondern austauschbar an die Domäne und an den Dienst übergeben werden.

Strategien werden in der Literatur immer sehr einfach Beispielhaft dargestellt. Sie haben alle dieselben Methoden mit denselben10) Parametern, dafür aber unterschiedliche Ergebnisse. Zudem haben sie keinen eigenen Zustand. Dazu drei Punkte, die im einzelnen besprochen werden

Zustand einer Strategie

Eine Strategie kann zur Durchführung ihrer Aufgabe weitere Informationen benötigen. Eine Strategie zur Authentifizierung per Benutzername und Kennwort muss wissen, gegen welches Kennwort authentifiziert werden muss. Sie muss also u.a. eine Methode haben, um das Kennwort zu ändern. Eine andere Strategie, z.B. für die Windows Authentifizierung, hält kein Kennwort für das Konto.

Um das umsetzen zu können, muss eine Strategie für die eigenen Methoden eigene Command Handler haben. Diese sind genauso umgesetzt wie die Command Handler des Aggregates, nur dass sie Methoden in der Strategie aufrufen und nicht im Aggregate. Dafür muss das Konto die Strategie freigeben11). Es heißt zwar, dass jede Änderung durch das Aggregate Root laufen müssen, wenn ich aber eine Methode auf einer Entität aufrufe, die mir vom Aggregate Root gegeben wurde, dann geschieht die Änderung rein logisch auch durch das Aggregate Root.

Der Command Handler für ChangeUserAccountPassword würde dann wie folgt aussehen

public class AccountUsernameAuthorizationHandler: RequestHandler, Executes<ChangeUserAccountPassword>
{
    private readonly Repository<Account> _repository;
    public AccountUsernameAuthorizationHandler(Repository<Account> repository)
    {
        _repository = repository;
    }
 
    public void Execute(ChangeUserAccountPasswordmessage, SecurityPrincipal principal)
    {
        var accnt = _repository.GetById(message.AggregateId, message.RequiredVersion);
        (accnt.AuthenticationStrategy as UsernameAuthenticationStrategy).ChangePassword(message.Password);
        _repository.Save(accnt);
    }
}
Zwischenbemerkungen

Das fängt alles danach auszusehen, als ob UsernameAuthenticationStrategy vielleicht doch eher ein eigenes Aggregate wäre. Man gibt dem Account ein ISelectAuthenticationStrategy mit, was im Grunde ein Repository ist, die Strategie hat eigene Befehle und Handler dafür, und wie wir sehen werden, auch eigene Ereignisse. Könnte man nicht einfach eine Strategie als eigenes Aggregate definieren und dann im Konto eine Strategie setzen indem die ID der Strategie dort gesetzt wird?

Die Antwort ist ja, könnte man. Die Id der Strategie verhält sich dann wie ein Funktionszeiger, was eine Strategie ja im Grunde ist. Es wäre sicher gestellt, dass das Konto immer genau eine Strategie hat. Man müsste noch sicherstellen, dass keine zwei Konten ein und dieselbe Strategie verwenden12).

Getrennte Aggregate wären in diesem Fall aber irgendwie merkwürdig, unnatürlich. Bei einer Rolle ist das nicht so sehr der Fall. Die Rolle Mitarbeiter würde auch ohne eine Person Sinn haben, und alles was man mit dem Mitarbeiter macht passiert unabhängig von der Person13). Man kann den Mitarbeiter eine Personalnummer geben, einem Projekt zuordnen, ein Arbeitszeitmodell geben, usw.

Eine UsernameAuthenticationStrategy ohne dazugehöriges Konto kann es aber nicht geben. Wenn ich mich gegen die Strategie mit meinem Benutzernamen und Kennwort authentifiziere, und es kein Konto dazu gibt, oder das Konto die Strategie gar nicht (mehr) verwendet, habe ich ins Leere authentifiziert. Zudem kann das Konto inaktiv sein, was Teil der Authentifizierung ist. D.h. dass das Verhalten der Strategie das Konto benötigt, bzw. verwendet.

Dann fällt mir der Fall ein, dass man die Strategie ändert, z.B. von Username zu Windows. Bei einem anderen Konto macht man das ander herum. Die Ereignisse müssten so behandelt werden, dass die ersetzten Strategien abgeschaltet werden, ansonsten blockiert die Username Strategie des ersten Kontos eventuell einen Benutzernamen, der nun vom zweiten Konto verwendet werden soll.

Zusammengefasst, ist das alles machbar. Man kann die Strategie als eigenes Aggregate umsetzen, was vielleicht einiges vereinfacht. Andererseits ist es ein interessantes Gedankenspiel, das Strategie Muster in CQRS mit ES einzubauen, weil es vielleicht irgendwann Situationen gibt, in denen es nicht anders geht. Also weiter mit dem Beispiel.


  • Der Befehl, um ein Konto zu erzeugen, ist AddAccount(Guid id, AuthenticationStrategyDTO strategy). Dieser Befehl ist abstrakt, und von AddUserAccount implementiert werden. AddUserAccount würde wie folgt erzeugt werden:
    new AddUserAccount(new Guid("3F2504E0-4F89-11D3-9A0C-0305E82C3301"), PersonId, new UserNameStrategyDTO("abc", "Kennwort789"));
  • Der Command Handler ruft im Konto14) den Konstruktor wie folgt auf15)
    var accnt = new UserAccount(cmd.Id, cmd.PersonId, cmd.strategy);
  • Im Konstruktor von UserAccount wird nach allen Prüfungen das Ereignis UserAccountAdded(Guid id, Guid personId, AuthenticationStrategyDTO strategy) veröffentlicht über
    ApplyEvent(new UserAccountAdded(Id, PersonId, strategy));
  • Dabei kann16) das Kennwort kein einfacher string sein, sondern ein cryptedString17), der verschlüsselt in den Befehl und in das Domänenereignis geschrieben wird,
  • In dem UserAccount wird in Apply(UserAccountAdded @event) aus @event.strategy eine AuthenticationStrategy, ganz einfach über
    new AuthenticationStrategy(@event.UserName, Decrypt(@event.Password));

    also kein DTO das18) in Befehle und Ereignisse geschrieben wird, sondern ein Strategie Objekt mit einem entsprechenden Verhalten.

Was die Projektions-Seite angeht, läuft das ähnlich wie bei der Vererbung. Wenn man für die AuthenticationStrategy gemeinsame Eigenschaften identifizieren kann, kann man diese über einen weiteren Info19)-Kontext implementieren, und für jede Implementierung der Authentifizierungs Strategie müsste dieser Kontext befüllt werden.

Die Strategie verwenden

Wie kann eine Authentifizierungs Strategie verwendet werden? Authentifiziert werden immer die credentials20). Es muss also die abstrakte Klasse Credentials geben. Das UI erzeugt die entsprechenden Credentials, und sendet dann einen AuthenticateAccountCredentials Befehl an die Domäne21), z.B.

authenticationProxy.Apply(new AuthenticateAccountCredentials(accountId, textBoxPassword.Text));

Der Command Handler macht dann folgendes

public void Handle(AuthenticateAccountCredentials command)
{
    var repo = new Repository<Account>();
    var agg = repo.Get(command.Id);
    agg.Authenticator.Authenticate(command.Credentials);
    repo.Save(agg);    
}

In diesem Fall ist somit nicht nur die Strategie austauschbar, sondern auch die Information, die jede Strategie zur Prüfung benötigt. Möglicherweise sind die Anmeldeprozesse so unterschiedlich22), dass der Command Handler abhängig des Typs der Credentials noch über Service Locator einen weiteren Dienst aufruft, der weiß, wie die Authentifizierung im Einzelnen ablaufen muss.

Kerberos Authentifizierung

Die Kerberos Authentifizierung läuft in mehreren Schritten ab. Das sollte aber wie folgt möglich sein:

  1. über den Benutzernamen fragt das Front-End nach der Account-ID,
  2. ein Lesedienst fragt die denormalisierten Kerberos Konten ab23) und sendet das Ergebnis zurück an den Client,
  3. der Client sendet eine Anfrage mit der ID nach dem verschlüsselten AuthenticationKey und dem AuthenticationTicket an die Query-Seite des Kontos24),
  4. Der Kerberos Query Dienst lädt das Konto, wandelt die Strategie in eine KerberosAuthenticationStrategy, und fragt sie nach den angeforderten Daten25),
  5. Der Client entschlüsselt mit dem Kennwort den AuthenticationKey, verschlüsselt damit die ID und das Timeout, das mit dem letzten Aufruf zurück erhalten wurde, und sendet diese beiden zusammen mit dem AuthenticationTicket26) als KerberosCredentials an das Konto als Authenticate Befehl,
  6. Hinweis27): Der AuthenticateCredentials Befehl ist abstrakt. Das ist für Befehle für Strategien generell sinnvoll. Es wird dann einen Command Handler geben, der AuthenticateKerberosCredentials Befehle behandelt. Dieser wird dann das Konto über das Repository laden, und eine entsprechende Methode zur Authentifizierung aufrufen. Dieser Methode können neben den Credentials dann weitere Dinge übergeben werden, wie z.B. die ICryptTickets Schnittstelle,
  7. Der Command Handler aus dem vorherigen Punkt lädt also das Konto, und ruft die entsprechende Authenticate Methode der Authenticate Strategie auf28). Wenn das klappt, wird ein Authenticated Ereignis veröffentlicht, das einen SessionKey enthält, verschlüsselt mit dem AuthenticationKey, und ein neues SessionTicket29),
  8. Wenn das nicht klappt, wird NotAuthenticated Ereignis ausgelöst. Nach 5 Versuchen wird das Konto gesperrt,
  9. Der Client pollt mitlerweile nach dem Ergebnis der Anmeldung. Erhält er ein positives Ergebnis, ist er angemeldet. Er verschlüsselt dann die UserId und das Timeout mit dem erhaltenen SessionKey, den er vorher mit dem AuthenticationKey entschlüsseln muss, und sendet dieses gemeinsam mit dem SessionTicket in jedem Aufruf an den Server mit,
  10. Der Server braucht jetzt das Konto nicht mehr zu laden um den Dienstaufruf zu authentifizieren. Anhand der mit gesendeten Credentials wird über Service Locator ein IAuthenticateCredentials Dienst geladen. Der Dienst für die Kerberos Credentials kann das von ICryptTickets verschlüsselte Ticket entschlüsseln30) und mit dem darin enthaltenen SessionKey die vom Client mit dem SessionKey verschlüsselten Daten ermitteln, und die UserId und das Timeout in beiden vergleichen. Stimmt alles, dann kennt der Client den SessionKey, was er ja nur kann, wenn er ihn mit dem AuthenticationKey entschlüsseln konnte, den er vorher mit dem Passwort31) entschlüsseln konnte.
1) Strategien
2) Oder in eine neue Rolle zu dem Aggregate, falls der Bereich komplett neu dazu gebaut wird
3) in diesem Fall Account
4) das _repository wird per Constructor Injection gesetzt
5) muss es aber nicht
6) hier wird angenommen, dass diese Klasse für die Befehle verwendet wird, sowie für die Ereignisse. Das muss aber nicht der Fall sein
7) die über die Konfigurationsdatei den KnownTypes der AuthenticationStrategyDTO hinzugefügt werden
8) die DTOs sind für den Datenaustausch und die Datenhaltund des Zustands des Accounts da
9) oder eine Methode, die die Strategie ändert
10) meist keinen
11) was in DDD erlaubt ist - man darf Entitäten temporär raus geben
12) wenn man es umdreht, und die Kontonummer in die Strategie schreibt, müsste man sicherstellen, dass es zu einem Konto nicht mehrere Strategien gibt. Und eine Strategie als Rolle des Kontos mit einer geteilten Id des Kontos zu sehen geht nicht, wenn man die Strategie austauschbar haben möchte
13) zumindest auf der Befehlsseite
14) UserAccount
15) cmd ist der AddUserAccount Befehl
16) sollte
17) eigener Typ
18) teil-verschlüsselt
19) oder Lese
20) Berechtigungsnachweis
21) nachdem das Konto ermittelt wurde, das authentifiziert werden soll, z.B. über den Benutzernamen
22) z.B. Kerberos mit mehreren Nachrichten, die hin-und-her gesendet werden
23) SELECT ID FROM KerberosAccount WHERE UserName=@UserName
24) hierfür muss ein Kerberos Query Dienst eingerichtet sein
25) , 28) wobei eine ICryptTickets Schnittstelle in dem Methdenaufruf mitgegeben wird
26) das vom Client nicht entschlüsselt werden kann
27) noch sauberer in den Workflow einbauen, ist mir gerade eingefallen
29) die Tickets sind durch ICryptTickes verschlüsselt, so dass es auch nicht vom Client entschlüsselt werden kann. Sie enthalten u.a. die UserId, das Timeout und den Key. Mit dem Key werden die vom Client verschlüsselten UserId und Session-Timeout Daten entschlüsselt, um zu prüfen, ob der Client das alles mit dem korrekten Schlüssel verschlüsselt hat - den Schlüssel kann der Client ja nur haben, wenn er initial das Passwort zur entschüsselung kannte.
30) Er ist i.d.R. der Dienst, der als ICryptTickets an das Konto übergeben wurde
31) mit dem MD5Hash vom Passort
patterns/inheritancerolestragegyoverview/strategy.1357746950.txt.gz · Last modified: 2013/01/09 16:55 by rtavassoli