Angular Camp https://angular-camp.de/ Wed, 04 Sep 2024 09:30:59 +0000 de-DE hourly 1 https://wordpress.org/?v=6.5.2 Implizite Bibliotheken mit Nx – leichtgewichtige Angular-Architekturen https://angular-camp.de/blog/implizite-bibliotheken-mit-nx/ Tue, 03 Sep 2024 07:52:20 +0000 https://angular-camp.de/?p=7896 Die Build-Lösung Nx [1] unterstützt seit Jahren beim Aufbau großer Projekte und Monorepos. Ab Werk unterstützt sie Angular und React sowie einige Node.js-basierte Frameworks.

The post Implizite Bibliotheken mit Nx – leichtgewichtige Angular-Architekturen appeared first on Angular Camp.

]]>

Durch ein Plug-in-Konzept lassen sich auch andere Frameworks wie Vue.js integrieren. Nx beschleunigt Build-Prozesse durch Caching und Parallelisierung und erlaubt das Einschränken von Zugriffen zwischen Programmteilen, um eine lose Kopplung zu erzwingen. Beides erfolgt in der Regel auf der Ebene von Bibliotheken, die sich mit einem Dependency Graph visualisieren lassen (Abb. 1).

steyer_kolumne_1
Abb. 1: Nx Dependency Graph

Da Bibliotheken in Nx nicht nur zur Schaffung von wiederverwendbarem Code, sondern auch zur Strukturierung der gesamten Lösung zum Einsatz kommen, weist ein Nx-basiertes Monorepo in der Regel eine Vielzahl an Bibliotheken auf. Jede Bibliothek hat wiederum eine Vielzahl an Konfigurationsdateien (Abb. 2).

steyer_kolumne_2
Abb. 2: Bibliothek mit Konfigurationsdateien

Auch wenn Nx diese Dateien generiert, empfinden sie Entwickler:innen immer wieder als lästigen Overhead, der von der eigentlichen Arbeit ablenkt. Genau diesen Kritikpunkt nehmen implizite Bibliotheken ins Visier. Die Idee stammt von Angular GDE Younes Jaaidi, der sie in einem Blogartikel [2] ausführlich beschrieben hat. Um die Konfigurationsdateien loszuwerden, leitet er die Konfiguration der einzelnen Bibliotheken mittels Konventionen her.

In diesem Artikel gehe ich auf diese Idee ein. Das verwendete Beispiel, das auf den Ideen aus [2] basiert, findet sich unter [3].

Architekturmatrix

Große Nx-Projekte werden häufig sowohl vertikal als auch horizontal untergliedert (Abb. 3). Aus der vertikalen Untergliederung gehen Anwendungsbereiche, z. B. Subdomänen, hervor. Die horizontale Untergliederung beschreibt technische Schichten. Je nach Projekt sind auch noch weitere Dimensionen denkbar, z. B. eine Untergliederung in server- und clientseitigen Quellcode.

steyer_kolumne_3
Abb. 3: Architekturmatrix

Durch diese Vorgehensweise wird der Code besser strukturiert und es ergeben sich weniger Diskussionen darüber, wo bestimmte Programmteile abzulegen bzw. zu finden sind. Außerdem lassen sich nun Architekturregeln einführen, wie z. B., dass jeder Layer nur auf Layer darunter Zugriff bekommt. Eine weitere Regel könnte festlegen, dass eine Subdomäne nur ihre eigenen Bibliotheken und jene aus dem Bereich shared nutzen darf. Eine sich im Lieferumfang von Nx befindliche Linting-Regel kann solche Einschränkungen sicherstellen.

BRINGEN SIE LICHT INS ANGULAR-DUNKEL

Die ersten Schritte in Angular geht man am besten im Basic Camp.
→ Nächster Termin: 9. - 11. Dezember, online

Jeder Kreuzpunkt in der oben gezeigten Matrix entspricht in Nx einer eigenen Bibliothek. Abbildung 4 zeigt eine mögliche Ordnerstruktur für diese Bibliotheken.

steyer_kolumne_4
Abb. 4: Architekturmatrix in Ordnerstruktur abgebildet

Implizite Bibliotheken mit Project Crystal

Um zu verhindern, dass jede Bibliothek die eingangs erwähnte Vielzahl an Konfigurationsdateien erhält, nutzt die Idee von impliziten Bibliotheken ein Nx-Plug-in, dass die Konfigurationen der Bibliotheken herleitet. Möglich macht das das sogenannte Project Crystal. Dabei handelt es sich um eine Neuerung in Nx, die es erlaubt, Projektkonfigurationen programmatisch mit einem Graph zu beschreiben.

Plug-ins lassen sich entweder in npm-Paketen oder direkt im Nx-Projekt ablegen. Das hier betrachtete Beispiel nutzt letztere Variante und platziert das Plug-in unter tools/plugins/implicit-libs/src/index.ts. Diese Datei exportiert ein Tupel createNodesV2, das Nx zum Ermitteln der impliziten Bibliotheken und deren Konfigurationen einsetzt (Listing 1).

Listing 1

export const createNodesV2: CreateNodesV2 = [
  'libs/**/index.ts',
  async (indexPathList, _, { workspaceRoot }): Promise<CreateNodesResultV2> => {

    […]

  }
];
 

Der erste Eintrag definiert einen Glob. Jedes Match erkennt das Plug-in als Einsprungpunkt in eine Bibliothek. Demnach handelt es sich bei Bibliotheken um Ordner unterhalb von libs, die eine index.ts aufweisen.

Der zweite Eintrag legt eine Funktion fest. Nx übergibt die ermittelten Einsprungspunkte an den ersten Parameter indexPathList. Die Variable workspaceRoot verweist auf das Stammverzeichnis des gesamten Nx-Workspace.

Die Aufgabe dieser Funktion besteht darin, die Konfigurationen der einzelnen Bibliotheken zu erstellen und in Form des Typs CreateNodesResultV2 zurückzuliefern. Das verlinkte Beispiel konfiguriert pro Bibliothek ESlint sowie Vitest zum Ausführen von Unit-Tests.

Außerdem leitet es aus der Ordnerstruktur eine Kategorisierung für die Bibliotheken ab. Diese Kategorisierung spiegelt die Position in der Architekturmatrix (Abb. 3) wider. Eine Featurebibliothek in der Domäne „Tickets“ bekommt zum Beispiel die Kategorien type:feature und scope:tickets zugewiesen. Die so ermittelten Kategorien sind die Basis für die oben erwähnten Architekturregeln, die der Linter erzwingt. Sie finden sich in der ESlint-Konfigurationsdatei im Stammverzeichnis des Nx-Workspace (Listing 2).

Listing 2

[…]
rules: {
  '@nx/enforce-module-boundaries': [
    'error',
    {
      enforceBuildableLibDependency: true,
      allow: [],
      depConstraints: [
        {
          sourceTag: 'scope:checkin',
          onlyDependOnLibsWithTags: [
            'scope:checkin',
            'scope:shared'
          ]
        },
        {
          sourceTag: 'scope:luggage',
          onlyDependOnLibsWithTags: [
            'scope:luggage',
            'scope:shared'
          ]
        },
        {
          sourceTag: 'scope:tickets',
          onlyDependOnLibsWithTags: [
            'scope:tickets',
            'scope:shared'
          ]
        },
        {
          sourceTag: 'type:feature',
          onlyDependOnLibsWithTags: [
            'type:feature',
            'type:ui',
            'type:domain',
            'type:util'
          ]
        […]
    ]
  }
}
[…]
 

Um Nx zu veranlassen, das Plug-in aufzugreifen, ist es in der Datei nx.json, die sich ebenfalls im Stammverzeichnis des Monorepos befindet, zu referenzieren:

"plugins": [
  "./tools/plugins/implicit-libs/src/index.ts"
]
 

Neben dem Plug-in enthält [3] auch einen Generator, der Path-Mappings für sämtliche implizite Bibliotheken einrichtet:

nx g @demo/implicit-libs:update-tsconfig-paths
 

Diese Path-Mappings ermöglichen den Zugriff auf die einzelnen Bibliotheken über logische Namen:

import { TicketsService } from '@demo/tickets-data';
 

ABTAUCHEN IM DEEP DIVE

Im Fortgeschrittenen Camp tauchen Sie ab unter die Oberfläche einer modernen Angular-Anwendung.
→ Nächster Termin: 2. - 4. Dezember, Berlin

Daemon und Cache deaktivieren

Nx macht sämtliche Projektkonfigurationen und Informationen über Abhängigkeiten zwischen Projekten über einen Daemon zugänglich. Außerdem platziert es das Ergebnis einzelner Build-Aufgaben in einem Build-Cache.

Während diese Maßnahmen die Performance von Nx erheblich verbessern, können sie beim Entwickeln von Plug-ins zum Verhängnis werden. Um zu verhindern, dass Ergebnisse des Plug-ins während der Entwicklung im Cache landen, empfiehlt es sich, beide Mechanismen zu deaktivieren. Das lässt sich zum Beispiel bewerkstelligen, indem man die Umgebungsvariablen NX_DAEMON und NX_CACHE auf false setzt. Unter Windows lassen sich dazu die folgenden Anweisungen nutzen:

set NX_DAEMON=false
set NX_CACHE=false
 

Implizite Bibliotheken in Aktion

Um eine implizite Bibliothek anzulegen, ist lediglich ein entsprechender Ordner unter libs einzurichten und mit einer index.ts zu versehen (Abb. 5).

steyer_kolumne_5
Abb. 5: Struktur einer impliziten Bibliothek

Um zu prüfen, ob Nx die implizite Bibliothek erkennt, bietet es sich an, mit ng graph einen Dependency Graph zu erzeugen. Alternativ dazu lässt sich Nx auch dazu veranlassen, die Namen sämtlicher Bibliotheken auf der Konsole auszugeben: nx show projects. Um herauszufinden, wie das Plug-in die einzelnen Bibliotheken konfiguriert hat, kann die folgende Anweisung genutzt werden:

nx show project tickets-feature-booking
 

Daraufhin generiert Nx eine Seite, die die abgeleitete Konfiguration beschreibt (Abb. 6).

steyer_kolumne_6
Abb. 6: Abgeleitete Konfiguration einsehen

Hier ist zum Beispiel zu sehen, dass die Bibliothek tickets-feature-booking die Kategorien (Tags) type:feature und scope:tickets erhalten hat sowie Linting und Unit-Tests unterstützt. Demnach lassen sich die folgenden Befehle aufrufen:

nx lint tickets-feature-booking
nx test shared-ui-common
 

Die Kategorisierung fließt in die Linting-Rules zum Erzwingen der Architekturvorgaben ein. Versucht man beispielsweise, aus der Ticketing-Domäne heraus auf die Luggage-Domäne zuzugreifen, erhält man eine Fehlermeldung (Abb. 7).

steyer_kolumne_7
Abb. 7: Erkennen einer Zugriffsverletzung

Fazit

Implizite Bibliotheken, deren Konfigurationen ein Nx-Plug-in mittels Konventionen herleiten, vereinfachen die Arbeit mit Nx enorm. Um eine neue Bibliothek anzulegen, ist lediglich ein Ordner mit einer index.ts einzurichten. Möglich macht das Project Crystal, mit dem sich Nx-Projekte in Form eines Graphs beschreiben lassen.

 

Newsletter

Jetzt anmelden & regelmäßig wertvolle Insights in Angular sowie aktuelle Weiterbildungen erhalten!

 

The post Implizite Bibliotheken mit Nx – leichtgewichtige Angular-Architekturen appeared first on Angular Camp.

]]>
Sicherheit in einer Angular-Anwendung https://angular-camp.de/blog/sicherheit-in-einer-angular-anwendung/ Tue, 09 Apr 2024 09:15:55 +0000 https://angular-camp.de/?p=7799 Dieser Artikel beleuchtet die Grundlagen und Best Practices für die Integration von Authentifizierungs- und Autorisierungsmechanismen in Angular-Anwendungen. Wir werden uns clientseitige Ansätze ansehen, die zur Absicherung von Angular-basierten Projekten beitragen, sowie gängige Herausforderungen und Lösungen in diesem Bereich diskutieren. Mit einem Fokus auf aktuelle Technologien und Methoden bietet dieser Artikel wertvolle Einblicke und Anleitungen für Entwickler:innen, die ihre Angular-Anwendungen sicherer und widerstandsfähiger gegenüber Bedrohungen machen wollen.

The post Sicherheit in einer Angular-Anwendung appeared first on Angular Camp.

]]>
Authentifizierung (AuthN) und Autorisierung (AuthZ) sind zwei entscheidende Aspekte der Sicherheit in modernen Webanwendungen. In der Welt von Angular, einem populären Framework für das Erstellen von Single Page Applications (SPAs), ist die Implementierung robuster AuthN- und AuthZ-Strategien unerlässlich, um die Sicherheit der Anwendung und der darin verarbeiteten Daten zu gewährleisten.

Bei der Diskussion um Sicherheit in Webanwendungen rückt unweigerlich die Frage in den Fokus, was genau es zu schützen gilt. In diesem Kontext bietet die sogenannte CIA-Triade [1] einen klaren Rahmen, indem sie drei essenzielle Sicherheitsziele definiert – Vertraulichkeit, Integrität und Verfügbarkeit (Abb. 1).

kraus_authn_authz_1.tif_fmt1.jpgAbb. 1: Die CIA-Triade
  1. Vertraulichkeit (Confidentiality): Dieses Ziel bezieht sich auf den Schutz von Daten vor unbefugtem Zugriff oder Offenlegung. Es gewährleistet, dass sensible Informationen nur für diejenigen zugänglich sind, die berechtigt sind. Maßnahmen zur Aufrechterhaltung der Vertraulichkeit umfassen Verschlüsselung, Zugriffskontrollen und strenge Authentifizierungsverfahren.

  2. Integrität (Integrity): Integrität bezieht sich auf die Bewahrung der Genauigkeit und Vollständigkeit von Daten. Es geht darum, sicherzustellen, dass Informationen nicht ohne Autorisierung verändert, manipuliert oder auf andere Weise beschädigt werden. Methoden zur Sicherung der Integrität umfassen Hashfunktionen, digitale Signaturen und Redundanzmechanismen.

  3. Verfügbarkeit (Availability): Verfügbarkeit bedeutet, dass Informationen und Ressourcen im Bedarfsfall zugänglich und nutzbar sind. Dieses Ziel soll sicherstellen, dass Systeme und Daten trotz verschiedener Bedrohungen wie Hardwareausfällen, Softwareproblemen oder Cyberangriffen zugänglich bleiben. Maßnahmen zur Gewährleistung der Verfügbarkeit sind unter anderem Back-up-Systeme, Redundanzen und effiziente Wartungsprozesse.

Die CIA-Triade dient als Grundlage für das Verständnis und die Bewertung von Sicherheitsmaßnahmen in IT-Systemen. Mit der Integration von Authentifizierung und Autorisierung sorgen wir also für einen Schutz des Sicherheitsziels der Vertraulichkeit: Schutz vor unbefugtem Zugriff.

Authentifizierung und Autorisierung

Authentifizierung und Autorisierung sind zwei grundlegende Sicherheitskonzepte, die vor allem in der Welt der Informationstechnologie Verwendung finden. Immer wieder werden beide Konzepte vermischt oder sogar synonym verwendet, daher möchte ich mit einer kleinen Definition beide Sicherheitskonzepte noch einmal voneinander abtrennen:

Authentifizierung bezieht sich hierbei auf den Prozess, mit dem überprüft wird, ob jemand oder etwas tatsächlich das ist, was es vorgibt zu sein. Im Kontext von Webanwendungen bedeutet das in der Regel die Überprüfung der Identität eines Benutzers, typischerweise durch Benutzername und Passwort, biometrische Daten, Einmal-Passwörter oder andere Methoden. Authentifizierung ist der erste Schritt, um zu gewährleisten, dass der Zugriff auf ein System oder eine Anwendung von einer legitimen Quelle aus erfolgt. Oftmals werden bei der Authentifizierung sogenannte zweite Faktoren (Multi-factor Authentication) genutzt, um eine weitere Sicherheitsstufe zu integrieren. Das kann in Form eines SMS-Codes oder über eine speziell dafür ausgelegte Applikation [2] auf dem Smartphone integriert werden.

Autorisierung hingegen ist der Prozess der Entscheidung, ob ein authentifizierter Benutzer Zugriff auf bestimmte Ressourcen oder Funktionen erhalten soll. Das beinhaltet in der Regel die Überprüfung von Benutzerrechten oder Rollen gegenüber den Zugriffsanforderungen. Autorisierung erfolgt nach der Authentifizierung und bestimmt, was ein Benutzer in einem System oder einer Anwendung tun darf.

Diese beiden Konzepte sind eng miteinander verknüpft, aber dennoch unterschiedlich:

  • Authentifizierung stellt fest, wer der Benutzer ist.

  • Autorisierung bestimmt, was der Benutzer tun darf.

Die Herausforderung besteht darin, ein Gleichgewicht zu finden zwischen dem Schutz sensibler Daten und Ressourcen und der Bereitstellung eines nahtlosen und benutzerfreundlichen Erlebnisses für legitime Benutzer. So ist beispielsweise eine Ressource optimal geschützt, wenn der Benutzer noch vier oder mehr weitere Faktoren zur Authentifizierung angeben muss; der Benutzer selbst ist aber vermutlich schnell genervt von dem bereitgestellten Authentifizierungsprozess.

BRINGEN SIE LICHT INS ANGULAR-DUNKEL

Die ersten Schritte in Angular geht man am besten im Basic Camp.
→ Nächster Termin: 9. - 11. Dezember, online

Authentifizierung in einer Webapplikation

Bei der Integration eines Authentifizierungsprozesses in eine Webanwendung stehen uns im Wesentlichen zwei etablierte Methoden zur Verfügung:

  • cookiebasierte Authentifizierung

  • tokenbasierte Authentifizierung

Beide Ansätze finden heutzutage große Anwendung und bringen jeweils ihre spezifischen Vorzüge und Herausforderungen mit sich. Im Folgenden werde ich diese beiden Konzepte detailliert erörtern und die potenziellen Vor- und Nachteile jedes Ansatzes beleuchten, um ein tieferes Verständnis ihrer Anwendung und Wirksamkeit in modernen Webanwendungen zu vermitteln.

Cookiebasierte Authentifizierung

Cookiebasierte Authentifizierung ist ein verbreiteter Ansatz zur Verwaltung von Benutzersitzungen in Webanwendungen. Dabei wird nach erfolgreicher Authentifizierung des Nutzers ein kleines Stück Daten – bekannt als Cookie – vom Server an den Webbrowser des Benutzers gesendet und dort gespeichert. Dieses Cookie wird dann bei jeder folgenden Anfrage des Browsers an den Server zurückgesendet, wodurch der Server den Benutzer und dessen Sitzungszustand über verschiedene Anfragen hinweg verfolgen kann. Dabei werden folgende Schritte durchgeführt:

  • Anmeldung: Der Benutzer gibt seine Anmeldeinformationen (wie Benutzername und Passwort) ein. Nach erfolgreicher Überprüfung dieser Informationen durch den Server wird ein Sitzungscookie erstellt (Abb. 2).

  • Sitzungscookie: Dieses Cookie enthält in der Regel eine einzigartige Sitzungs-ID, die den Benutzer identifiziert. Es speichert keine sensiblen Benutzerdaten direkt, da Cookiedaten innerhalb eines Webbrowsers gespeichert werden und diese theoretisch jederzeit einsehbar sind (Abb. 3).

  • Sicherheit: Um die Sicherheit zu erhöhen, können Cookies mit Attributen wie httpOnly (verhindert den Zugriff durch Client-side-Skripte) und Secure (sorgt dafür, dass Cookies nur über HTTPS gesendet werden) versehen werden [3].

  • Sitzungsverwaltung: Bei jeder Anfrage des Benutzers an den Server wird das Sitzungscookie mitgesendet. Der Server prüft die Gültigkeit des Cookies und erlaubt den Zugriff auf geschützte Ressourcen, wenn das Cookie gültig ist (Abb. 4).

  • Ablauf und Abmeldung: Cookies haben ein Ablaufdatum. Nach dem Ablauf oder wenn der Benutzer sich explizit abmeldet, wird das Cookie entweder vom Server als ungültig markiert oder vom Browser gelöscht.

Die cookiebasierte Authentifizierung ist besonders effektiv für traditionelle Webanwendungen, bei denen der Server eine aktive Rolle bei der Sitzungsverwaltung spielt. Ein weiterer wichtiger Faktor ist das Management der Cookies über den Browser: Entwickler:innen müssen hierbei keinen eigenen Code für das Verwalten der Cookies schreiben. Das verringert die Komplexität der Webanwendung, da wir uns darauf verlassen können, dass der Webbrowser bei jeder Anfrage an unser Backend die Cookiedaten automatisch mitsendet.

kraus_authn_authz_2.tif_fmt1.jpgAbb. 2: Nach dem Log-in wird ein Sitzungscookie erstellt
kraus_authn_authz_3.tif_fmt1.jpgAbb. 3: Das Cookie wird im Browser gespeichert
kraus_authn_authz_4.tif_fmt1.jpgAbb. 4: Der Server prüft die Gültigkeit des gespeicherten Cookies

Durch den Einsatz der bereits erwähnten Cookieattribute secure und httpOnly erhöhen wir die Sicherheit in der Verwaltung unserer Cookies. Insbesondere bietet das Setzen des httpOnly-Attributs einen wirksamen Schutz gegen Manipulationen des Cookies durch bösartige JavaScript-Einschleusungen, bekannt als Cross-site-Scripting-(XSS-)Attacken. Dieses Attribut bewirkt, dass die Cookies nicht mehr über das Browser-API document.cookie zugänglich sind, wodurch ein zusätzliches Sicherheitsniveau im Umgang mit sensiblen Benutzerdaten etabliert wird.

Auch wenn die Nutzung von Cookies bösartige Manipulationen über JavaScript verringern kann, sind sie leider die Hauptursache für sogenannte Cross-site-Request-Forgery-(CSRF-)Attacken.

Eine CSRF-Attacke, auch als „One Click Attack“ oder „Session Riding“ bekannt, ist eine Art von Cyberangriff, bei dem ein Angreifer Nutzer dazu bringt, ungewollte Aktionen auf einer Webseite auszuführen, auf der sie gerade angemeldet sind. Das geschieht typischerweise, ohne dass der Nutzer sich dessen bewusst ist. Hierbei wird genau die Tatsache ausgenutzt, dass das Opfer bereits auf der Zielwebseite über ein Cookie authentifiziert ist und dieses Cookie bei allen Anfragen auf die Zielwebseite automatisch vom Webbrowser mitgesendet wird. Die Anfrage kann daraufhin verschiedene Aktionen auslösen, wie das Ändern von Kontoinformationen, das Versenden von Nachrichten oder das Durchführen von Transaktionen. Die Open-Web-Application-Security-Project-(OWASP-)Organisation hat einige Tipps und Tricks in einem Cheat Sheet [4] zusammengestellt, wie man sich am besten vor CSRF-Attacken schützen kann.

Neben der Einführung von potenziellen CSRF-Attacken haben Cookies noch einen weiteren Nachteil: Sie sind weniger geeignet für moderne, verteile Architekturen wie Single Page Applications (SPAs) und Microservices, in denen oft tokenbasierte Authentifizierungsmethoden, wie z. B. JSON Web Tokens (JWT), bevorzugt werden. In Anbetracht dessen möchten wir uns nun der tokenbasierten Authentifizierung zuwenden und diese eingehend beleuchten.

Newsletter

Jetzt anmelden & regelmäßig wertvolle Insights in Angular sowie aktuelle Weiterbildungen erhalten!

Tokenbasierte Authentifizierung

Tokenbasierte Authentifizierung ist ein Verfahren, bei dem statt traditioneller Session-Cookies ein Authentifizierungstoken für die Identifizierung und Verwaltung von Benutzersitzungen verwendet wird. Dieses Verfahren wird wie bereits erwähnt häufig in modernen Webanwendungen und insbesondere in API-basierten Diensten und Single-Page-Anwendungen eingesetzt. Folgendes sind die Kernaspekte der tokenbasierten Authentifizierung:

  • Authentifizierung und Tokenerstellung: Der Benutzer gibt seine Anmeldeinformationen (Benutzername und Passwort) ein. Nach erfolgreicher Überprüfung erstellt der Server ein Token, oft ein JSON Web Token (JWT), das Informationen über den Benutzer und die Sitzung enthält. Meist kommt hierbei ein sogenannter externer Identity Provider ins Spiel der die sichere Generierung und Benutzerverwaltung übernimmt (Abb. 5).

  • Tokenversendung: Das Token wird an den Client (den Browser oder die Anwendung des Benutzers) gesendet und dort gespeichert, beispielsweise im Local Storage, in einer Session oder in einem Cookie (Abb. 6).

  • Clientseitige Verwendung des Tokens: Bei jeder folgenden Anfrage an den Server fügt der Client das Token im HTTP-Header hinzu. Dies dient als Nachweis der Authentifizierung und Autorisierung für die Anfrage (Abb. 7).

  • Serverseitige Validierung: Der Server validiert das Token bei jeder Anfrage, um sicherzustellen, dass es gültig und nicht manipuliert wurde. Nach erfolgreicher Validierung gestattet der Server den Zugriff auf geschützte Ressourcen.

  • Ablauf und Erneuerung: Token haben in der Regel ein Ablaufdatum. Nach Ablauf kann der Benutzer entweder erneut seine Anmeldeinformationen eingeben oder ein Refresh-Token verwenden, um ein neues Zugriffstoken zu erhalten.

Diese Methode ist besonders effektiv für Anwendungen, die Ressourcen über mehrere Server oder Dienste verteilen, da das Token leicht zwischen verschiedenen Systemkomponenten übertragen werden kann, ohne die Notwendigkeit einer zentralen Sitzungsverwaltung.

kraus_authn_authz_5.tif_fmt1.jpg
Abb. 5: Bei der tokenbasierten Authentifizierung ist meist ein Identity Provider im Spiel
kraus_authn_authz_6.tif_fmt1.jpg
Abb. 6: Das Token wird an den Client gesendet
kraus_authn_authz_7.tif_fmt1.jpg
Abb. 7: Das Token im Header dient als Authentifizierungsnachweis

Token können auf verschiedene Arten in Browsern oder Webanwendungen gespeichert werden. Doch auch hierbei gibt es einiges zu beachten: Wenn Sie den lokalen Speicher eines Browsers verwenden, kann eine Subdomain nicht auf Tokens zugreifen. Sie können jedoch von jedem JavaScript-Code auf der Webseite sowie von Browser-Plug-ins aufgerufen und manipuliert werden. Das ist also keine empfohlene Methode – sie stellt zum einen ein Sicherheitsrisiko dar und darüber hinaus müssen Sie den Speicher selbst verwalten.

Die Verwaltung der Tokens erfordert oftmals viel manuelle Arbeit: Ein Token kann beispielsweise nicht widerrufen werden. Selbst wenn ein Token gestohlen wird, bleibt es gültig, bis es abläuft, was zu einer schwerwiegenden Sicherheitslücke führt. Um dieses Problem zu umgehen, müssen Sie eine Sperrlistentechnik implementieren, die eine komplexere Einrichtung erfordert.

Aber auch ohne an bösartige Attacken zu denken, kann eine eigenständige Verwaltung der Tokens kompliziert sein: Die Informationen in einem Token stellen eine Momentaufnahme zum Zeitpunkt der ursprünglichen Erstellung des Tokens dar. Der zugehörige Benutzer verfügt möglicherweise nun über andere Zugriffsebenen oder wurde vollständig aus dem System entfernt.

Angesichts der Komplexität und Bedeutung einer sicheren Tokengenerierung und -verwaltung empfiehlt es sich fast immer, diese Aufgaben nicht eigenständig zu übernehmen. Oft ist es vorteilhafter und sicherer, externe Identity-as-a-Service-Anbieter (sogenannte Identity Provider wie Azure Active Directory [5] oder Auth0 [6] by Okta) zu integrieren oder etablierte Identity- und Access-Management-Tools zu verwenden, wie sie beispielsweise mit Keycloak [7] von Red Hat angeboten werden.

Diese Lösungen bieten nicht nur eine robuste Verwaltung von Nutzerkonten und Tokens, sondern erleichtern durch bereitgestellte Entwicklerbibliotheken auch signifikant die Integration in Webanwendungen. Dadurch wird nicht nur die Sicherheit erhöht, sondern auch der Entwicklungsprozess effizienter und benutzerfreundlicher gestaltet.

Wie das Ganze nun konkret aussehen kann, möchte ich Ihnen anhand einer beispielhaften Implementierung demonstrieren. Hierbei werde ich den Identity Provider Auth0 by Okta nutzen, um Authentifizierung und Autorisierung in eine bestehende Angular-Applikation zu integrieren.

Bevor wir mit der konkreten Implementierung starten können, müssen wir uns zuvor kurz mit den offenen Standards für Autorisierung und Authentifizierung auseinandersetzen. Diese beschreiben verschiedene Workflows und Richtlinien, wie eine Webanwendung ein Token sicher erhalten kann, und bieten eine Spezifikation für weitere Authentifizierungskonzepte wie Single Sign-on (SSO) oder die Integration sozialer Medien (Social Log-in).

OAuth und OpenID Connect

OpenID Connect ist eine Identitätsverwaltungsschicht, die auf dem OAuth-Protokoll aufbaut. Sie ermöglicht es Clients, die Identität eines Endbenutzers zu verifizieren und grundlegende Profilinformationen über den Benutzer zu erhalten. OpenID Connect wird oft für Single-Sign-on-(SSO-)Lösungen verwendet und ist weit verbreitet in modernen Webanwendungen und Mobile-Apps. OpenID Connect erweitert hierbei OAuth um die Einführung von ID-Tokens. Diese Tokens sind im JWT-Format und enthalten Informationen über die Authentifizierung des Benutzers.

OAuth ist ein offener Standard für Zugriffsdelegation, der es Benutzern ermöglicht, Dritten eingeschränkten Zugriff auf ihre Ressourcen auf einem anderen Server zu gewähren, ohne dabei ihre Zugangsdaten preiszugeben. Ursprünglich für die API-Autorisierung entwickelt, hat sich OAuth zu einem Schlüsselstandard in der modernen Web- und Anwendungsentwicklung entwickelt. OAuth ermöglicht es Anwendungen, im Namen des Benutzers auf Ressourcen zuzugreifen, indem sie ein Zugriffstoken verwenden, das vom Ressourcenbesitzer (dem Benutzer) genehmigt wurde. Das bedeutet, dass Anwendungen keine Benutzernamen und Passwörter speichern müssen. Kurz gesagt ist OAuth eine Spezifikation für die Autorisierung eines Benutzers: Sie können spezifische Berechtigungen (Scopes) an die Anwendungen vergeben, was eine fein abgestimmte Kontrolle darüber ermöglicht, auf welche Informationen und Funktionen die Anwendung zugreifen darf.

OAuth definiert verschiedene Flows (auch als Grant Types bezeichnet), um unterschiedliche Anwendungsfälle und Szenarien für die Authentifizierung und Autorisierung zu unterstützen. Jeder dieser Flows beschreibt einen spezifischen Prozess zur Erlangung eines Zugriffstokens – dem sogenannten Access-Token.

Je nach Anwendung werden verschiedene Flows empfohlen. Die aktuelle Version OAuth 2.1 [8] spezifiziert hierbei hauptsächlich drei Flows:

  • Client Credentials Flow

  • Device Code Flow

  • Authorization Code Flow mit Proof-Key-Code-Exchange-(PKCE-)Erweiterung

Der Client Credentials Flow wird immer dann verwendet, wenn der Zugriff zwischen zwei Anwendungen ohne Benutzerinteraktion erfolgt. Die Anwendung authentifiziert sich mit ihren eigenen Credentials (nicht mit Benutzer-Credentials) beim OAuth-Server und erhält ein Zugriffstoken. Der Device Code Flow wird für Geräte genutzt, die keine einfache Möglichkeit bieten, Text einzugeben (wie Smart-TVs oder Spielkonsolen). Es verwendet ein Gerät, das einen Code anzeigt, den der Benutzer auf einem anderen Gerät (z. B. einem Smartphone) eingibt, um die Authentifizierung zu bestätigen.

Wenn wir mit Angular eine SPA entwickeln, wird empfohlen, dass wir den Authorization Code Flow mit PKCE nutzen. Dieser Flow ist für Anwendungen gedacht, die auf einem Server laufen. Bei einem einfachen Authorization Code Flow (ohne PKCE-Erweiterung) authentifiziert sich der Benutzer zunächst bei seinem Identity Provider und erteilt der Anwendung die Berechtigung (Abb. 8). Daraufhin erhält die Anwendung einen Autorisierungscode, den sie gegen ein Zugriffstoken eintauschen kann (Abb. 9). Dieser Flow gilt als einer der sichersten, da die Nutzer-Credentials zu keinem Zeitpunkt in unserer Frontend-Applikation einsehbar sind.

kraus_authn_authz_8.tif_fmt1.jpg
Abb. 8: Authentifizierung beim Identity Provider
kraus_authn_authz_9.tif_fmt1.jpg
Abb. 9: Autorisierungscode wird gegen Access-Token getauscht

Mit der Proof-Key-Code-Exchange-Erweiterung – die seit OAuth 2.1 auch verpflichtend ist – fügt man dem Authorization Code Flow noch eine weitere Sicherheitsstufe hinzu. Hierbei generiert der Client einen zufälligen String, bekannt als Code Verifier. Daraufhin erstellt der Client eine Code Challenge aus diesem Verifier, in der Regel, indem er einen Hashwert (SHA256) des Verifiers bildet und den Hash dann in Base64-URL kodiert. Bei der erneuten Authentifizierung des Benutzers werden neben den Credentials die Code Challenge und die Hashfunktion mitgegeben, mit der eben diese Code Challenge erstellt wurde. Daraufhin erhält, wie auch zuvor, die Anwendung einen Autorisierungscode und sendet diesen, diesmal mit dem Code Verifier, an seinen Identity-Provider. Dieser verwendet den empfangenen Code Verifier, um die Code Challenge zu generieren (d. h., er berechnet den SHA256-Hash und kodiert ihn in Base64-URL). Der Identity-Provider vergleicht dann die generierte Challenge mit der ursprünglich vom Client gesendeten Challenge. Stimmen diese überein, weiß der Server, dass die Anfrage vom gleichen Client stammt, der den Autorisierungscode angefordert hat und liefert ebenfalls das Access-Token aus.

Zum Glück entfällt die Notwendigkeit, sich mit den Details der Implementierung dieser Flows auseinanderzusetzen, da das von externen Bibliotheken, wie beispielsweise denen, die Auth0 by Okta bereitstellt, abgedeckt wird. Dennoch ist ein grundlegendes Verständnis der verschiedenen OAuth Flows von unschätzbarem Wert, um eine fundierte und sichere Entscheidung über den am besten geeigneten Flow für unsere spezifischen Anforderungen treffen zu können.

ABTAUCHEN IM DEEP DIVE

Im Fortgeschrittenen Camp tauchen Sie ab unter die Oberfläche einer modernen Angular-Anwendung.
→ Nächster Termin: 2. - 4. Dezember, Berlin

Konfiguration des Identity Provider

Wie bereits erwähnt werde ich in meiner Beispielimplementierung den Identity Provider Auth0 by Okta verwenden. Sämtliche gezeigten Funktionalitäten und Konzepte finden sich aber in anderen Identity-as-a-Service-Lösungen ebenfalls wieder. Auth0 bietet einen kostenlosen Nutzungsplan [9], der bis zu 7 500 aktive Benutzer und unbegrenzte Log-ins umfasst. Diese kostenlosen Pläne sind ideal, um die grundlegenden Funktionen von Auth0 by Okta kennenzulernen und zu testen, wie gut sie sich in Ihre Anwendung oder Ihr Projekt integrieren lassen.

Wenn wir uns nun kostenlos registrieren, können wir eine neue Applikation – auch Tenant genannt – erstellen (Abb. 10). Ein Tenant bezeichnet eine dedizierte Instanz der Identity-Provider-Plattform, die für einen Kunden oder ein Projekt eingerichtet wird. Es ist im Wesentlichen ein separater Container, in dem alle Ihre Benutzer, Sicherheitseinstellungen, Anwendungen und Konfigurationen für Ihre Authentifizierungs- und Autorisierungsvorgänge gespeichert werden. Jeder Tenant bei Auth0 ist durch eine eindeutige Domain gekennzeichnet und isoliert, was bedeutet, dass die Daten und Konfigurationen eines Tenants nicht mit denen anderer Tenants geteilt werden.

kraus_authn_authz_10.tif_fmt1.jpg
Abb. 10: Erstellen einer Applikation

Nachdem der Tenant erfolgreich angelegt wurde können wir diesen nun konfigurieren. Ebenso können wir alle wichtigen Daten direkt einsehen (beispielsweise die Domain und die Client-ID), die wir benötigen, um unsere Frontend-Applikation mit Auth0 zu verbinden (Abb. 11).

kraus_authn_authz_11.tif_fmt1.jpg
Abb. 11: Informationen unserer Applikation

Wichtig hierbei ist, dass wir konfigurieren, welche sogenannten Origins, also URLs auf unseren Tenant zugreifen dürfen. Da wir eine lokale Angular-Anwendung implementieren, müssen wir http://localhost:4200 eintragen. Darüber hinaus ist dieser URL ebenfalls noch in die Liste der Allowed Callback URLs (eine URL-Liste, in der aufgelistet wird, wohin der Identity Provider nach erfolgreicher Authentifizierung wieder navigieren soll) und in die Liste der Allowed Logout URLs (ebenfalls eine Liste der URLs, auf die der Identity Provider uns navigiert, sobald sich der Benutzer erfolgreich ausloggt) einzutragen. Nach erfolgreicher Konfiguration können wir nun damit starten, Auth0 in unserer Angular-Applikation zu integrieren.

Integration in eine Angular-Anwendung

Auth0 erleichtert die Integration erheblich durch die Bereitstellung eines SDK, das als npm-Paket [10] verfügbar ist. Dieses können wir wie folgt installieren:

> npm install @auth/auth0-angular
 

Nach erfolgreicher Installation nutzen wie uns zur Verfügung gestellte provideAuth0-Funktion und konfigurieren sie mit den spezifischen Details unserer Auth0Domain und der Client-ID wie in Listing 1.

Listing 1

//main.ts
import { provideAuth0 } from '@auth0/auth0-angular';
 
bootstrapApplication(AppComponent, {
  providers: [
    provideAuth0({
      domain: 'YOUR_AUTH0_DOMAIN',
      clientId: 'YOUR_AUTH0_CLIENT_ID',
      authorizationParams: {
        redirect_uri: window.location.origin,
      }
    }),
  ]
});
 

Nun fehlen lediglich noch eine Login- und eine Logout-Komponente damit sich unsere Nutzer gegenüber unserem Identity Provider authentifizieren können. Der bereitgestellte AuthService erleichtert und die Implementierung und Verwaltung des vollständigen Authorization Code Flow mit PKCE-Erweiterung (Listing 2).

Listing 2

import { AuthService } from "@auth0/auth0-angular";
 
@Component({
  selector: "app-login-button",
  template: `
    <button class="button__login" (click)="handleLogin()">Log In</button>
  `
})
export class LoginButtonComponent {
  constructor(private auth: AuthService) {
  }
 
  handleLogin(): void {
    this.auth.loginWithRedirect({
      prompt: "login"
    });
  }
}
 

Beim Klick auf den Log-in-Button erfolgt eine Weiterleitung zur Log-in-Maske unseres Identity Providers (Abb. 12). Werfen wir dabei einen Blick auf den Network-Tab unseres Webbrowsers, sehen wir einen Request an den / authorize-Endpoint unseres Identity Provider (Abb. 13). Ebenfalls zu sehen sind die Properties code_challenge_method und code_challenge. Diese sind notwendig für die PKCE-Erweiterung. Die Property response_type mit dem Wert code gibt hierbei an, dass der Client unseren Identity Provider zu einem Authorization Code Flow aufruft.

kraus_authn_authz_12.tif_fmt1.jpg
Abb.12: Log-in-Maske von Auth0
kraus_authn_authz_13.tif_fmt1.jpg
Abb. 13: Payload des Requests

Wenn wir uns nun erfolgreich einloggen, können wir eine weitere Anfrage auf den /token-Endpoint unseres Identity Provider sehen. Der mitgesendete Payload sieht dieses Mal wie folgt aus:

client_id: "afRDjs6KFS0TV5NL7NzltUw9Wlktukutyk77"
code: "bigvau6q0DIA9HTbJKcXyRG09j1lZX1jTa8zLAsCIV2Wh"
code_verifier: "zBtJLPMI1TcKpLcOgHoY5ukNGmVnstxhWUksfh1Rso7"
grant_type: "authorization_code"
redirect_uri: "http://localhost:4040/callback"
 

Wie bereits erwähnt wird bei diesem Aufruf nun der Authorization-Code (code) mitgesendet werden, sowie der Code Verifier (code_verifier), damit unser Identity Provider überprüfen kann, ob beide Anfragen vom selben Client stammen.

Als Antwort erhalten wir das Access-Token sowie ein ID-Token, da OpenID Connect ebenfalls automatisch aktiviert ist (Abb. 14). Diese Tokens werden standardmäßig in einem Cookie abgespeichert. Wir können das SDK so konfigurieren, dass die Tokens im localStorage abgelegt werden, allerdings ist das nur in Verbindung mit einem Refresh-Token ratsam.

kraus_authn_authz_14.tif_fmt1.jpg
Abb. 14: Response mit Access-Token und ID-Token

Das Access-Token können wir nun für die Autorisierung gegenüber verschiedenen Backend-APIs verwenden. Hierfür setzen wir den Authorization-Header bei allen Anfragen auf das entsprechende API. Das kann uns auch von einem von Auth0 bereitgestellten Angular Inteceptor abgenommen werden (Listing 3).

Listing 3

//main.ts
import { provideAuth0, authHttpInterceptorFn } from '@auth0/auth0-angular';
 
bootstrapApplication(AppComponent, {
  providers: [
    provideAuth0(...),
    provideHttpClient(
      withInterceptors([authHttpInterceptorFn])
    )
  ]
});
 

Senden wir nun also eine Anfrage an ein geschütztes API, wird automatisch der Authorization-Header mit dem Wert des Access-Tokens gesetzt. Um die Anwendung vollständig abzurunden, fügen wir noch eine Logout-Komponente hinzu, die erneut den AuthService injiziert der uns eine Log-out-Funktion anbietet (Listing 4).

kraus_authn_authz_15.tif_fmt1.jpg
Abb. 15: Access-Token im Header

Listing 4

import { Component, Inject } from "@angular/core";
import { AuthService } from "@auth0/auth0-angular";
import { DOCUMENT } from "@angular/common";
 
@Component({
  selector: "app-logout-button",
  template: `
    <button class="button__logout" (click)="handleLogout()">Log Out</button>
  `
})
export class LogoutButtonComponent {
  constructor(
    private auth: AuthService,
    @Inject(DOCUMENT) private doc: Document
  ) {
  }
  
  handleLogout(): void {
    this.auth.logout({ returnTo: this.doc.location.origin });
  }
}
 

Dieser AuthService bietet uns noch viele weitere Observables an, z. B. isAuthenticated$, um herauszufinden, ob der Benutzer bereits eingeloggt ist oder nicht. Möchten wir hingegen Daten des Benutzers erhalten, wie beispielsweise den Usernamen, einen URL auf ein Profilbild oder die E-Mail-Adresse, so erhalten wir diese Daten, wenn wir uns auf das user$-Observable des AuthServices abonnieren.

Wie wir gesehen haben, gestaltet sich die Integration eines Identity Provider in eine Angular-Applikation recht einfach. Selbst bei anderen SPA-Frameworks wie React oder Vue.js bietet Auth0 entsprechende SDKs an, um auch dort die Integration mit so wenig Aufwand wie möglich zu gestalten. Wenn Sie nicht Auth0 nutzen möchten, bietet Microsoft eigene SDKs [11] zur Integration von Azure Active Directory. Darüber hinaus gibt es auch vielerlei etablierte Open-Source-SDKs [12] für die Integration verschiedener Identity Provider in eine Angular-Anwendung.

Die Absicherung Ihrer Backend-Ressourcen durch Autorisierung ist ein absolutes Muss einer jeden modernen Webanwendung. Der richtige Umgang mit dem Access-Token bietet vielerlei Fallstricke, wie beispielsweise die Auswahl eines schwachen kryptographischen Verfahrens zur Verschlüsselung ihrer Access-Tokens. Nicht signierte Tokens können leicht manipuliert werden, sodass ein Nutzer sich mehr Rechte zuweisen kann, als er eigentlich hat. Mehr als die Hälfte der Top 10 API Security Risks [13], die von der OWASP-Organisation veröffentlicht werden, handeln von fehlerhafter oder falsch implementierter Autorisierung. Es ist also definitiv ratsam, sich nicht selbst an der Implementierung einer Authentifizierungs- und Autorisierungslösung zu versuchen und auf bestehende Lösungen zu setzen, wie sie von Identity Providern zur Verfügung gestellt werden.

Abschließend lässt sich festhalten, dass die Integration von Authentifizierung und Autorisierung in Angular-Anwendungen eine entscheidende Rolle bei der Gewährleistung von Sicherheit und Benutzerfreundlichkeit spielt. Durch die sorgfältige Implementierung von AuthN und AuthZ können Entwickler robuste und sichere Webanwendungen erstellen, die nicht nur den Schutz sensibler Daten ermöglichen, sondern auch ein nahtloses und effizientes Nutzererlebnis bieten. Die in diesem Artikel vorgestellten Methoden und Best Practices bieten dabei einen umfassenden Leitfaden, um diese komplexen Prozesse erfolgreich in Angular-basierten Projekten umzusetzen. Mit dem wachsenden Fokus auf Websicherheit und Datenschutz ist es für Entwickler unerlässlich, sich kontinuierlich weiterzubilden und die neuesten Technologien und Methoden in diesem dynamischen Bereich zu adaptieren.

 

Links & Literatur

[1] https://it-service.network/it-lexikon/cia-triade

[2] Google Authenticator: https://de.wikipedia.org/wiki/Google_Authenticator

[3] https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#security

[4] https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html

[5] https://www.microsoft.com/de-de/security/business/identity-access/microsoft-entra-id

[6] https://auth0.com/de

[7] https://www.keycloak.org

[8] https://oauth.net/2.1/

[9] https://auth0.com/pricing

[10] https://www.npmjs.com/package/@auth0/auth0-angular

[11] https://learn.microsoft.com/en-us/entra/identity-platform/msal-overview

[12] https://github.com/manfredsteyer/angular-oauth2-oidc und https://github.com/damienbod/angular-auth-oidc-client

[13] https://owasp.org/API-Security/editions/2023/en/0x11-t10/

The post Sicherheit in einer Angular-Anwendung appeared first on Angular Camp.

]]>
Strategic Design mit Sheriff und Standalone https://angular-camp.de/blog/strategic-design-mit-sheriff-und-standalone/ Fri, 24 Nov 2023 09:36:16 +0000 https://angular-camp.de/?p=7359 Moderne Architekturen mit Angular – Teil 1:
Angular kommt häufig für das Frontend großer, unternehmenskritischer Lösungen zum Einsatz. Gerade in diesem Umfeld ist besonders auf eine gut wartbare Architektur zu achten. Gleichzeitig soll aber auch Over-Engineering vermieden werden. Aktuelle Features wie Standalone Components und Standalone APIs helfen dabei.

The post Strategic Design mit Sheriff und Standalone appeared first on Angular Camp.

]]>
In dieser zweiteiligen Artikelserie zeige ich Ihnen, wie Sie beide Anforderungen unter einen Hut bekommen. Der erste Teil beleuchtet die Umsetzung Ihres strategischen Designs auf der Basis von Standalone Components und Standalone APIs. Die vorgegebene Architektur wird mit dem Open-Source-Projekt Sheriff umgesetzt. Die verwendeten Beispiele finden sich in meinem GitHub-Konto unter [1].

Die Leittheorie: Strategic Design aus DDD

Als Leittheorie für die Strukturierung moderner Frontends hat sich Strategic Design, eine der beiden ursprünglichen Disziplinen von Domain-Driven Design (DDD), bewährt. Im Kern geht es dabei darum, ein Softwaresystem in verschiedene fachliche Teilbereiche (Subdomänen) zu zerlegen. Bei einer Fluggesellschaft könnte man zum Beispiel die in Abbildung 1 dargestellten Subdomänen finden.

steyer_sheriff_1.tif_fmt1.jpg
Abb. 1: Domänenschnitt

 

Die Identifizierung der einzelnen Domänen erfordert einen Blick auf die zu unterstützenden Geschäftsprozesse. Das Zusammenspiel zwischen Entwickler:innen sowie Architekt:innen auf der einen und Domänenexpert:innen auf der anderen Seite ist dabei essenziell. Workshopformate wie Event Storming [2], die DDD mit Ideen der agilen Softwareentwicklung verbinden, bieten sich hierfür an.

Die Beziehungen und Abhängigkeiten zwischen einzelnen Domänen werden in einer Context Map dargestellt (Abb. 2). Ziel ist es, die einzelnen Domänen voneinander zu entkoppeln. Je weniger sie voneinander wissen, desto besser. Das verhindert, dass sich Änderungen an einem Anwendungsteil auf andere Teile auswirken, und verbessert so die Wartbarkeit. Bei größeren Projekten ist es üblich, jedem Teilteam eine oder mehrere Domänen zuzuweisen (Kasten: „Domäne vs. Bounded Context“).

steyer_sheriff_2.tif_fmt1.jpg
Abb. 2: Eine einfache Context Map

 

Um die gewünschte Entkopplung zu erreichen, könnte Booking im gezeigten Beispiel nur einige ausgewählte Services veröffentlichen. Alternativ dazu könnten Informationen über gebuchte Flüge per Messaging im Backend verteilt werden. Darüber hinaus bietet Strategic Design zahlreiche weitere Muster und Überlegungen, die bei der Umsetzung einer losen Kopplung helfen.

 

BRINGEN SIE LICHT INS ANGULAR-DUNKEL

Die ersten Schritte in Angular geht man am besten im Basic Camp.
→ Nächster Termin: 9. - 11. Dezember, online

Domäne vs. Bounded Context

Streng genommen wird eine Domäne im Rahmen der Implementierung auf einen oder mehrere Bounded Contexts abgebildet, und jeder Bounded Context kann wiederum eine oder mehrere Domänen enthalten. Somit spiegelt der Bounded Context die Lösungssicht wider, während die Domäne einen Teil der Problemsicht darstellt.

Jeder Bounded Context hat ein Domänenmodell, das die jeweilige Fachlichkeit widerspiegelt, z. B. die Struktur und Handhabung von Flügen und Tickets. Dieses Domänenmodell ist nur innerhalb des Bounded Context sinnvoll. Auch wenn die gleichen Begriffe in anderen Kontexten verwendet werden, ist es sehr wahrscheinlich, dass diese Kontexte eine andere Sichtweise darauf haben. Ein Flug sieht aus Booking-Sicht anders aus als aus Sicht des Boarding. Diese beiden Sichten werden bewusst getrennt dargestellt. Damit vermeidet man sowohl eine Vermischung der Kontexte als auch ein unübersichtliches Modell, das versucht, zu viel auf einmal zu beschreiben.

Der Einfachheit halber gehen die Ausführungen in diesem Artikel davon aus, dass pro Domäne ein Bounded Context vorherrscht.

Übergang zum Quellcode: die Architekturmatrix

Für die Umsetzung im Quellcode bietet es sich an, die einzelnen Domänen weiter in verschiedene Module zu untergliedern (Abb. 3).

steyer_sheriff_3.tif_fmt1.jpgAbb. 3: Architekturmatrix

 

Eine Kategorisierung dieser Module erhöht die Übersichtlichkeit. In [3] werden unter anderem folgende Kategorien vorgeschlagen, die sich in unserer täglichen Arbeit bewährt haben:

  • Feature: Ein Feature-Modul implementiert einen Anwendungsfall (oder auch ein technisches Feature) mit sogenannten Smart Components. Durch die Fokussierung auf ein Feature sind solche Komponenten wenig wiederverwendbar. Ein weiteres Merkmal ist, dass Smart Components mit dem Backend kommunizieren. Typischerweise erfolgt diese Kommunikation in Angular über einen Store oder Services.

  • UI: UI-Module enthalten sogenannte Dumb oder Presentational Components. Dabei handelt es sich um wiederverwendbare Komponenten, die die Implementierung einzelner Features zwar unterstützen, sie aber nicht direkt kennen. Die Umsetzung eines Designsystems besteht aus solchen Komponenten. UI-Module können aber auch allgemeine fachliche Komponenten enthalten, die anwendungsfallübergreifend zum Einsatz kommen. Ein Beispiel hierfür wäre eine Ticketkomponente, die sicherstellt, dass Tickets in verschiedenen Features einheitlich dargestellt werden. Häufig kommunizieren solche Komponenten nur über Eigenschaften und Events mit ihrer Umgebung. Sie haben keinen Zugriff auf das Backend oder einen Store.

  • Data: Data-Module enthalten das jeweilige Domänenmodell (eigentlich die clientseitige Sicht darauf) sowie Services, die darauf operieren. Solche Services validieren z. B. Entitäten und kommunizieren mit dem Backend. Auch das State Management inkl. der Bereitstellung von ViewModels kann in Data-Modulen untergebracht werden. Das ist vor allem dann sinnvoll, wenn mehrere Features derselben Domäne auf denselben Daten basieren.

  • Util: Allgemeine Hilfsfunktionen werden in Utility-Modulen untergebracht. Beispiele dafür sind Logging, Authentifizierung oder das Arbeiten mit Datumswerten.

Eine weitere Besonderheit bei der Umsetzung im Code ist der shared-Bereich, der Code für alle Domänen bereitstellt. Er sollte in erster Linie technischen Code enthalten – fachlicher Code ist in der Regel in den einzelnen Domänen zu finden.

Die hier dargestellte Struktur bringt Ordnung in das System: Es gibt weniger Diskussionen darüber, wo bestimmte Codeabschnitte zu finden bzw. zu platzieren sind. Außerdem lassen sich auf Basis dieser Matrix zwei einfache, aber wirkungsvolle Regeln einführen:

  • Jede Domäne darf im Sinne des Strategic Design nur mit ihren eigenen Modulen kommunizieren. Eine Ausnahme bildet der shared-Bereich, auf den jede Domäne Zugriff hat.

  • Jedes Modul darf nur auf Module in tieferen Schichten der Matrix zugreifen. Dabei geht aus jeder Modulkategorie eine eigene Schicht hervor.

Beide Regeln unterstützen die Entkopplung der einzelnen Module bzw. Domänen und helfen, Zyklen zu vermeiden.

Projektstruktur für die Architekturmatrix

Die Architekturmatrix kann im Quellcode in Form von Ordnern abgebildet werden: Jede Domäne bekommt einen eigenen Ordner, der wiederum für jedes seiner Module einen Unterordner erhält (Abb. 4).

steyer_sheriff_4.tif_fmt1.jpg
Abb. 4: Ordnerstruktur mit Domänen

 

Die Modulnamen haben den Namen der jeweiligen Modulkategorie als Präfix. So ist auf den ersten Blick ersichtlich, wo in der Architekturmatrix sich das jeweilige Modul befindet. Innerhalb der Module gibt es typische Angular Building Blocks wie Komponenten, Direktiven, Pipes oder Services.

Der Einsatz von Angular-Modulen ist seit der Einführung von Standalone Components (Dirketiven und Pipes) nicht mehr notwendig. Stattdessen wird die Flag standalone auf true gesetzt (Listing 1). Bei Komponenten ist außerdem der sogenannte Compilation Context zu importieren. Dabei handelt es sich um alle weiteren Standalone Components, Direktiven und Pipes, die im Template zum Einsatz kommen.

Listing 1

@Component({
  selector: 'app-flight-booking',
  standalone: true,
  imports: [CommonModule, RouterLink, RouterOutlet],
  templateUrl: './flight-booking.component.html',
  styleUrls: ['./flight-booking.component.css'],
})
export class FlightBookingComponent {
}
Zur Definition der öffentlichen Schnittstelle bekommt jedes Modul eine index.ts-Datei. Das ist ein sogenanntes Barrel, das festlegt, welche Modulbestandteile auch außerhalb des Moduls genutzt werden dürfen:
export * from './flight-booking.routes';

Bei der Wartung der veröffentlichten Konstrukte ist Vorsicht geboten, da sich Breaking Changes tendenziell auch auf andere Module auswirken. Alles jedoch, was hier nicht veröffentlicht wird, ist ein Implementierungsdetail des Moduls. Änderungen an diesen Teilen sind daher weniger kritisch.

Domänenschnitt mit Sheriff erzwingen

Die bisher diskutierte Architektur basiert auf mehreren Konventionen:

  • Module dürfen nur mit Modulen derselben Domäne sowie shared kommunizieren.

  • Module dürfen nur mit Modulen tieferliegender Schichten kommunizieren.

  • Module dürfen nur auf die öffentliche Schnittstelle anderer Module zugreifen.

Mit dem Open-Source-Projekt Sheriff [4] können diese Konventionen via Linting erzwungen werden. Bei Nichteinhaltung wird eine Fehlermeldung in der IDE (Abb. 5) oder auf der Konsole (Abb. 6) ausgegeben.

steyer_sheriff_5.tif_fmt1.jpg
Abb. 5: Sheriff in der IDE

 

steyer_sheriff_6.tif_fmt1.jpg
Abb. 6: Sheriff auf der Konsole

 

Ersteres liefert unmittelbares Feedback während der Entwicklung, wohingegen Letzteres im Build-Prozess automatisiert werden kann. Dadurch lässt sich zum Beispiel verhindern, dass Quellcode, der gegen die definierte Architektur verstößt, im main– bzw. dev-Branch des Quellcode-Repos landet.

Um Sheriff einzurichten, müssen die folgenden beiden Pakete über npm bezogen werden:

npm i @softarc/sheriff-core @softarc/eslint-plugin-sheriff -D
Das Erste beinhaltet Sheriff selbst, das zweite ist die Anbindung an eslint. Diese ist in der Datei .eslintrc.json im Projekt-Root zu registrieren (Listing 2).

 

Listing 2
{
  [...],
  "overrides": [
    [...]
    {
      "files": ["*.ts"],
      "extends": ["plugin:@softarc/sheriff/default"]
    }
  ]
}
Sheriff betrachtet jeden Ordner, der eine index.ts-Datei enthält, als ein Modul. Ein Umschiffen dieser index-Datei und somit ein Zugriff auf Implementierungsdetails durch andere Module wird von Sheriff standardmäßig verhindert. Die im Root des Projektes einzurichtende Datei sheriff.config.ts legt Kategorien (tags) für die einzelnen Module fest und definiert darauf aufbauend Abhängigkeitsregeln (depRules). Listing 3 zeigt eine Sheriff-Konfiguration für die oben diskutierte Architekturmatrix.

 

Listing 3

import { noDependencies, sameTag, SheriffConfig } from '@softarc/sheriff-core';
 
export const sheriffConfig: SheriffConfig = {
  version: 1,
 
  tagging: {
    'src/app': {
      'domains/<domain>': {
        'feature-<feature>': ['domain:<domain>', 'type:feature'],
        'ui-<ui>': ['domain:<domain>', 'type:ui'],
        'data': ['domain:<domain>', 'type:data'],
        'util-<ui>': ['domain:<domain>', 'type:util'],
      },
    },
  },
  depRules: {
    root: ['*'],
 
    'domain:*': [sameTag, 'domain:shared'],
 
    'type:feature': ['type:ui', 'type:data', 'type:util'],
    'type:ui': ['type:data', 'type:util'],
    'type:data': ['type:util'],
    'type:util': noDependencies,
  },
};
Die Tags beziehen sich auf Ordnernamen. Ausdrücke wie <domain> oder <feature> sind Platzhalter. Jedes Modul unterhalb von src/app/domains/<domain>, dessen Ordnername mit feature- beginnt, bekommt demnach die Kategorien domain:<domain> sowie type:feature zugewiesen. Im Fall von src/app/domains/booking wären das die Kategorien domain:booking und type:feature.

Die Abhängigkeitsregeln unter depRules greifen die einzelnen Kategorien auf und legen zum Beispiel fest, dass ein Modul nur auf Module derselben Domäne und auf domain:shared zugreifen darf. Weitere Regeln definieren, dass jede Schicht nur auf darunterliegende Schichten zugreifen darf. Dank der Regel root: [‚*‘] dürfen alle nicht explizit kategorisierten Ordner im Root-Ordner und darunter auf sämtliche Module zugreifen. Das betrifft vor allem die Shell der Anwendung.

 

ABTAUCHEN IM DEEP DIVE

Im Fortgeschrittenen Camp tauchen Sie ab unter die Oberfläche einer modernen Angular-Anwendung.
→ Nächster Termin: 2. - 4. Dezember, Berlin

Leichtgewichtige Path-Mappings

Um unleserliche relative Pfade innerhalb der Imports zu vermeiden, bieten sich Path Mappings an. Diese erlauben es zum Beispiel, statt

import { FlightBookingFacade } from '../../data';

folgende Formulierung zu verwenden:

import { FlightBookingFacade } from '@demo/ticketing/data';
Solche dreistelligen Importe setzen sich aus dem Projekt- bzw. Workspace-Namen (z. B. @demo), dem Domänennamen (z. B. ticketing) sowie einem Modulnamen (z. B. data) zusammen und spiegeln damit die gewünschte Position in der Architekturmatrix wider. Diese Notation lässt sich unabhängig von der Anzahl der Domänen und Module mit einem einzigen Path Mapping innerhalb der Datei tsconfig.json im Projekt-Root ermöglichen (Listing 4).

 

Listing 4
{
  "compileOnSave": false,
  "compilerOptions": {
    "baseUrl": "./",
    [...]
    "paths": {
      "@demo/*": ["src/app/domains/*"],
    }
  },
  [...]
}
IDEs wie Visual Studio Code sollten nach dieser Änderung neu gestartet werden. Dadurch wird sichergestellt, dass sie die Änderung berücksichtigen.

Standalone APIs

Da Standalone Components die umstrittenen Angular-Module optional machen, stellt das Angular-Team nun sogenannte Standalone APIs zur Registrierung von Bibliotheken zur Verfügung. Bekannte Beispiele sind provideHttpClient und provideRouter (Listing 5).

 

Listing 5

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(),
    provideRouter(APP_ROUTES, withPreloading(PreloadAllModules)),
 
    importProvidersFrom(NextFlightsModule),
    importProvidersFrom(MatDialogModule),
 
    provideLogger({
      level: LogLevel.DEBUG,
    }),
  ],
});
Im Wesentlichen handelt es sich dabei um Funktionen, die Provider für die benötigten Services zurückliefern. Die Auswahl dieser Provider und damit das Verhalten der Bibliothek lässt sich durch die Übergabe eines Konfigurationsobjekts beeinflussen. Ein Beispiel hierfür ist die Routenkonfiguration, die provideRouter entgegennimmt.

Aus Architektursicht erfüllen Standalone-APIs noch einen weiteren Zweck: Sie erlauben es, einen Systembestandteil als Blackbox zu betrachten, die unabhängig weiterentwickelt werden kann. Aus der Blackbox kann durch die Übergabe eines Konfigurationsobjekts auch eine Graybox werden. In diesem Fall kann das Verhalten des genutzten Systembestandteils über wohldefinierte Einstellungen angepasst werden, ohne die lose Kopplung aufgeben zu müssen. Man spricht hier auch vom Open/Closed-Prinzip: offen für Erweiterungen (durch Konfiguration bzw. Polymorphismus), geschlossen für Modifikationen durch User.

Als Beispiel für ein eigenes Standalone API, das einen Logger einrichtet, wird in Listing 5 die Funktion provideLogger aufgerufen. Ihre Implementierung ist in Listing 6 zu sehen. Die Funktion provideLogger nimmt ein partielles LoggerConfig-Objekt entgegen. Der Aufrufer muss sich also nur um die Parameter kümmern, die für den aktuellen Fall relevant sind. Um eine vollständige LoggerConfig zu erhalten, führt provideLogger die übergebene Konfiguration mit einer Standardkonfiguration zusammen. Darauf aufbauend werden verschiedene Provider zurückgegeben. Die Funktion makeEnvironmentProviders aus @angular/core umhüllt das erzeugte Providerarray mit einem Objekt vom Typ EnvironmentProviders. Dieser Typ lässt sich beim Bootstrappen der Anwendung sowie innerhalb von Routingkonfigurationen nutzen. Er erlaubt somit die Bereitstellung von Providern für die gesamte Anwendung oder einzelne Teile.

 

Listing 6

export function provideLogger(
  config: Partial<LoggerConfig>
): EnvironmentProviders {
  const merged = { ...defaultConfig, ...config };
 
  return makeEnvironmentProviders([
    LoggerService,
    {
      provide: LoggerConfig,
      useValue: merged,
    },
    {
      provide: LOG_FORMATTER,
      useValue: merged.formatter,
    },
    merged.appenders.map((a) => ({
      provide: LOG_APPENDERS,
      useClass: a,
      multi: true,
    })),
  ]);
}
Im Gegensatz zu einem herkömmlichen Providerarray können EnvironmentProviders nicht innerhalb von Komponenten verwendet werden. Diese Einschränkung ist durchaus gewollt, denn die meisten Bibliotheken wie der Router sind für den komponentenübergreifenden Einsatz konzipiert.

Zusammenfassung

Strategic Design unterteilt ein System in verschiedene Teile, die möglichst unabhängig voneinander implementiert werden. Diese Entkopplung verhindert, dass sich Änderungen in einem Anwendungsbereich auf andere auswirken. Der vorgestellte Architekturansatz unterteilt die einzelnen Domänen in verschiedene Module, wobei das quelloffene Projekt Sheriff sicherstellt, dass die einzelnen Module nur nach den aufgestellten Regeln miteinander kommunizieren.

Dieser Ansatz ermöglicht die Umsetzung großer und langfristig wartbarer Frontend-Monolithen. Wegen ihres modularen Aufbaus spricht man auch von Modulithen. Ein Nachteil solcher Architekturen sind erhöhte Build- und Testzeiten. Dieses Problem kann durch inkrementelle Builds und Tests gelöst werden. Der zweite Teil dieser Artikelserie beschäftigt sich mit diesem Thema.

 

Newsletter

Jetzt anmelden & regelmäßig wertvolle Insights in Angular sowie aktuelle Weiterbildungen erhalten!

The post Strategic Design mit Sheriff und Standalone appeared first on Angular Camp.

]]>
Software Design Patterns in Angular https://angular-camp.de/blog/software-design-patterns-in-angular/ Mon, 25 Sep 2023 14:31:16 +0000 https://angular-camp.de/?p=7302 Software Design Patterns, oder deutsch Entwurfsmuster, finden sich überall und somit auch im Angular-Framework. Arbeitet man mit Angular, kommt man auch bei der Entwicklung täglich mit Design Patterns in Berührung. Wenn wir verstehen, wo diese Muster Verwendung finden, was sie tun und wie sie funktionieren, können wir sie auch besser nutzen. Das wiederum befähigt uns, bessere und robustere Anwendungen zu entwickeln.

The post Software Design Patterns in Angular appeared first on Angular Camp.

]]>
ENTWURFSMUSTER VERSTEHEN UND VERWENDEN

Generell sind Entwurfsmuster eine allgemeine wiederverwendbare Lösung für ein häufig wiederkehrendes Problem in einem bestimmten Kontext beim Softwaredesign. Sie basieren auf objektorientierten Kodierungstechniken wie Vererbung, Abstraktion, Polymorphismus und verbessern die Lesbarkeit des Codes.

TypeScript an sich ist zum Beispiel bereits so aufgebaut, dass es diese objektorientierten Programmiertechniken verwendet. Einige dieser Muster erfordern Programmierprinzipien wie Single Responsibility, Trennung unterschiedlicher Belange (Separation of Concerns) oder SOLID-Prinzipien. Die Verwendung von Entwurfsmustern schafft auch Konsistenz in einer Codebasis. Sie verbessert die Wartbarkeit, ermöglicht eine bessere Erweiterbarkeit und vereinfacht die Testbarkeit einer Softwarelösung. Dabei können vorhandene Muster verwendet werden, um zu vermeiden, atypische Lösungen für häufige Probleme zu schaffen – oder das Rad immer wieder neu zu erfinden. Auch bei der Arbeit im Team haben Software-Design-Patterns Vorteile, denn mit ihnen besitzen Entwicklerteams ein gemeinsames Vorgehen für die Implementierung bestimmter Teile der Anwendung.

Dieser Artikel beschäftigt sich mit oft verwendeten Entwurfsmustern, wie dem Template Pattern, dem Composite Pattern und dem Observer Pattern, die im Angular-Framework implementiert sind. Darüber hinaus werden Patterns beleuchtet, die bei der Entwicklung mit Angular verwendet werden können oder müssen, da das Framework sie vorgibt. Hierzu gehören das Mediator Pattern, Dependency Injection und das Facade Pattern. Somit handelt es sich nicht um eine Liste aller Patterns, die Angular verwendet oder die sich mit Angular verwenden lassen, sondern um eine exemplarische Annäherung an die Möglichkeiten, die Design Patterns bieten.

Im Angular-Framework implementierte Design Patterns

Angular nutzt Entwurfsmuster in fast allen Bereichen. Man findet sie in den Templates für die Benutzeroberfläche, in Komponenten und in Services. Darüber hinaus bietet das Framework selbst Konzepte, die die Verwendung ganz bestimmter Design Patterns für die Implementierung von Businesslogik vorsehen.

Eine der Hauptkomponenten einer Angular-Anwendung ist das Modul. Es fungiert wie eine Bibliothek oder ein Container für verwandte Komponenten. Jede Angular-Anwendung besitzt mindestens ein Modul: das Root-Modul (app.module.ts); siehe dazu Listing 1.

Listing 1: Das Root-Modul app.module.ts einer neu generierten Angular-Anwendung

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
 
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
 
@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
 

Module können in beliebig vielen weiteren Modulen organisiert werden. Je nach Bedarf der Lösung, die man anstrebt, ist es gängige Praxis, ein Core Module, ein Shared Module oder auch Feature Modules zu erstellen. Modules geben uns die Möglichkeit, Struktur innerhalb der Anwendung zu implementieren – und diese Struktur wiederum erlaubt es uns, Design Patterns bei der Entwicklung zu verwenden.

BRINGEN SIE LICHT INS ANGULAR-DUNKEL

Die ersten Schritte in Angular geht man am besten im Basic Camp.
→ Nächster Termin: 9. - 11. Dezember, online

Template Method Pattern (Schablonenmethode)

Das Template Method Design Pattern ist ein Verhaltensmuster (Behavioral Pattern). Es befasst sich, wie alle Muster, die in diese Kategorie fallen, mit Algorithmen und der Zuweisung von Verantwortlichkeiten zwischen Objekten. Das Besondere an verhaltensbasierten Entwurfsmustern ist, dass sie nicht nur die Muster von Objekten und Klassen beschreiben, sondern auch die Muster der Kommunikation zwischen ihnen.

Das Template Method Pattern ermöglicht es, einen bestimmten Prozess oder Ablauf zu erstellen, der jedes Mal, wenn eine Methode ausgeführt wird, in der richtigen Sequenz implementiert wird. Bei Angular sehen wir dieses Pattern in den Lifecycle Hooks [1] (ngOnInitngOnDestroy etc.) unserer Komponenten. Sie werden immer in einer bestimmten festgelegten Reihenfolge ausgeführt und helfen uns so dabei, Funktionalität in genau dem richtigen Moment des Lebenszyklus der Komponente auszuführen (Listing 2).

Listing 2: Angular Lifecycle Hooks in der Reihenfolge, in der sie ausgeführt werden

import {
  AfterContentChecked,
  AfterContentInit,
  AfterViewChecked,
  AfterViewInit,
  Component,
  DoCheck,
  OnChanges,
  OnDestroy,
  OnInit,
  SimpleChanges,
} from '@angular/core';
 
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent
  implements
    OnChanges,
    OnInit,
    DoCheck,
    AfterContentInit,
    AfterContentChecked,
    AfterViewInit,
    AfterViewChecked,
    OnDestroy
 
{
  ngOnChanges(): void {}
 
  ngOnInit(): void {}
 
  ngDoCheck(): void {}
 
  ngAfterContentInit(): void {}
 
  ngAfterContentChecked(): void {}
 
  ngAfterViewInit(): void {}
 
  ngAfterViewChecked(): void {}
 
  ngOnDestroy(): void {}
}
 

Composite Pattern (Kompositum)

Das Composite Pattern ist ein Entwurfsmuster, das zur Kategorie der Strukturmuster (Structural Patterns) gehört. Das Composite Pattern definiert, dass eine Gruppe von Objekten genauso zu behandeln ist, wie eine einzelne Instanz eines Objekts. Die Absicht eines Kompositums ist es, Objekte in Baumstrukturen zusammenzusetzen, um Teil-Ganzes-Hierarchien abzubilden. Vereinfacht ausgedrückt geht es bei diesem Muster darum, dass Knoten wieder andere Knoten besitzen, die auch wiederum Knoten beinhalten – wie eine Matroschka-Puppe.

Denken wir an Angular, kann dieses Prinzip generell auf Komponenten übertragen werden. Views sind in der Regel hierarchisch aufgebaut, sodass ganze UI-Abschnitte oder Seiten als Einheit geändert oder ein- und ausgeblendet werden können. Das Template, das unmittelbar mit einer Komponente verbunden ist, definiert die Host-View dieser Komponente. Die Komponente kann wiederum auch eine View-Hierarchie definieren, die eingebettete Views enthält, die von anderen Komponenten gehostet werden. Genauso baut Angular die Struktur der gesamten Benutzeroberfläche auf (Abb. 1).

varga_designpatterns_1.tif_fmt1.jpgAbb. 1: Angular-Komponentenhierarchie

Observer Pattern

Beginnt man mit der Entwicklung mit Angular, sind Observables eins der ersten zentralen Konzepte des Frameworks, mit denen man sich beschäftigt. Eine Angular-Anwendung ohne Observables zu erstellen, ist schier unmöglich.

Observables werden sowohl im Angular-Framework selbst als auch bei der Entwicklung von Angular-Anwendungen sehr intensiv genutzt. Wie funktioniert das Ganze? Man subskribiert ein Objekt, das Observable, und reagiert auf dessen Antworten. Das entspricht der Definition des Observer Pattern. Das Observer Pattern gehört zur Kategorie der Verhaltensmuster. Es dient zur Weitergabe von Änderungen an einem Objekt an von diesem Objekt abhängigen Strukturen. Dieses Muster ähnelt (ist aber nicht identisch mit) dem Publish/Subscribe-Entwurfsmuster.

Um ein selbst erstelltes Observable auszuführen und dessen Benachrichtigungen zu empfangen, wird seine subscribe()-Methode aufgerufen und ein Observer übergeben. Das ist ein JavaScript-Objekt, das die Handler für die empfangenen Benachrichtigungen definiert. Der subscribe()-Aufruf gibt ein Subscription-Objekt zurück, das eine unsubscribe()-Methode hat. Letztere muss aufgerufen werden, um den Empfang der Benachrichtigungen dieses Observables zu beenden.

Listing 3 ist der offiziellen Angular-Dokumentation entnommen [2] und veranschaulicht, wie ein Observer verwendet werden kann, um Aktualisierungen der Geolocation bereitzustellen.

Listing 3: Beispiel-Observable aus der Angular-Dokumentation

// Create an Observable that will start listening to geolocation updates
// when a consumer subscribes.
const locations = new Observable((observer) => {
  let watchId: number;
 
  // Simple geolocation API check provides values to publish.
  if ('geolocation' in navigator) {
    watchId = navigator.geolocation.watchPosition(
      (position: GeolocationPosition) => {
        observer.next(position);
      },
      (error: GeolocationPositionError) => {
        observer.error(error);
      }
    );
  } else {
    observer.error('Geolocation not available');
  }
 
  // When the consumer unsubscribes, clean up data ready for next   // subscription.
  return {
    unsubscribe() {
      navigator.geolocation.clearWatch(watchId);
    },
  };
});
 
// Call subscribe() to start listening for updates.
const locationsSubscription = locations.subscribe({
  next(position) {
    console.log('Current Position: ', position);
  },
  error(msg) {
    console.log('Error Getting Location: ', msg);
  },
});
 
// Stop listening for location after 10 seconds.
setTimeout(() => {
  locationsSubscription.unsubscribe();
}, 10000);
 

ABTAUCHEN IM DEEP DIVE

Im Fortgeschrittenen Camp tauchen Sie ab unter die Oberfläche einer modernen Angular-Anwendung.
→ Nächster Termin: 2. - 4. Dezember, Berlin

Patterns zur Implementierung von Businesslogik – Mediator Pattern

Das Mediator- bzw. Vermittler-Entwurfsmuster gehört zur Kategorie der Verhaltensmuster und stellt eine der Möglichkeiten dar, eine lose gekoppelte Kommunikation zwischen Komponenten zu ermöglichen. Dabei erfolgt die Komponentenkommunikation über den Vermittler, einen „Mittelsmann“, sodass eine Komponente A niemals direkt mit einer Komponente B kommuniziert.

Wenn eine Komponente A also Daten benötigt, wird eine andere die Daten über Bindungen (Bindings) an die Eingabeeigenschaften (Input Properties) der Komponente bereitstellen. Wer diese andere Komponente ist, interessiert die Komponente A nicht. Muss eine Komponente B der Außenwelt Daten zur Verfügung stellen, emittiert sie Ereignisse (in Angular mit dem @Output Decorator markiert) mit der Nutzlast der Daten. An wen sie die Ereignisse emittiert? Auch das interessiert die Komponente B nicht. Die Komponente, die die Daten benötigt, lauscht auf die Events (in Angular mit dem @Input Decorator markiert) dieser Komponente B und kümmert sich so darum, ihre benötigten Daten zu erhalten.

In Listing 4 bis 8 wird exemplarisch der Kommunikationsweg der Komponente buy zur Komponente cart über einen Mediator, der Parent-Komponente beider Children, gezeigt.

Listing 4: buy.component.html

<button (click)="onBuy('a123')">buy me!</button>
 

Listing 5: buy.component.ts

import { Component, EventEmitter, Output } from '@angular/core';
 
@Component({
  selector: 'app-buy',
  templateUrl: './buy.component.html',
  styleUrls: ['./buy.component.scss'],
})
export class BuyComponent {
  @Output() readonly purchasedItemIdEvent = new EventEmitter<string>();
 
  onBuy(purchasedItemId: string) {
    this.purchasedItemIdEvent.emit(purchasedItemId);
  }
}
 

Listing 6: mediator.component.html

<app-buy (purchasedItemIdEvent)="setPurchasedItemId($event)"></app-buy>
<app-cart [purchasedItemId]="purchasedItemId"></app-cart>
 

Listing 7: mediator.component.ts

import { Component } from '@angular/core';
 
@Component({
  selector: 'app-mediator',
  templateUrl: './mediator.component.html',
  styleUrls: ['./mediator.component.scss'],
})
export class MediatorComponent {
  purchasedItemId: string = '';
 
  setPurchasedItemId(itemId: string) {
    this.purchasedItemId = itemId;
  }
}
 

Listing 8: cart.component.ts

import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
 
@Component({
  selector: 'app-cart',
  templateUrl: './cart.component.html',
  styleUrls: ['./cart.component.scss'],
})
export class CartComponent implements OnChanges {
  @Input() purchasedItemId: string = '';
 
  ngOnChanges(changes: SimpleChanges): void {
    console.log('changes', changes);
  }
}
 

Dependency Injection

Das Dependency Injection Pattern ist ein Entwurfsmuster der objektorientierten Programmierung. Das Pattern beschreibt die Verwaltung von Objektabhängigkeiten zur Laufzeit. Wenn beispielsweise ein Objekt während der Initialisierung ein anderes Objekt benötigt, wird diese Dependency an einer zentralen Stelle verwaltet und wird nicht vom initiierten Objekt selbst generiert.

Laut der Dokumentation von Angular [3] ist Dependency Injection ein Entwurfsmuster und ein Mechanismus zur Erstellung und Bereitstellung einiger Teile einer Anwendung für andere Teile einer Anwendung, die diese benötigen. Angular Services folgen grundlegend diesem Muster. Wird über den Befehl ng generate service <name> ein Service erstellt, so ist dieser auch gleich per Default mit einem @Injectable() Decorator ausgestattet. Dieser spezifiziert, dass Angular diese Klasse in ihrem Dependency-Injection-System verwenden kann (Listing 9).

Listing 9: ProductService mit @Injectable() Decorator

import { Injectable } from '@angular/core';
 
@Injectable({
  providedIn: 'root',
})
export class ProductService {
  getProducts() {
    // logic
  }
}
 

 

Verwendet werden kann der Service dann sowohl in anderen Services als auch in Komponenten. In allen Fällen erfolgt eine Constructor Injection, wobei die Abhängigkeit über den jeweiligen Konstruktor zur Verfügung gestellt wird.

Listing 10: Verwendung des ProductService in app.component.ts via Constructor Injection

import { Component } from '@angular/core';
import { ProductService } from './cart-facade.service';
 
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent {
  constructor(productService: ProductService) {}
 
  getProducts() {
    this.productService.getProducts();
  }
}
 

Newsletter

Jetzt anmelden & regelmäßig wertvolle Insights in Angular sowie aktuelle Weiterbildungen erhalten!

Facade Pattern (Fassade)

Das Facade Pattern gehört zur Kategorie der Strukturmuster (Structural Design Patterns). Das Entwurfsmuster bietet eine vereinfachte Schnittstelle zu einem komplexen System von Klassen, Schnittstellen und Objekten. In einer Angular-Anwendung kann dieses Design Pattern verwendet werden, um die Interaktionen zwischen Komponenten und Services zu vereinfachen.

Häufig wird das Facade Pattern verwendet, um die Komplexität eines Systems hinter einer einfacheren, benutzerfreundlicheren Schnittstelle zu verbergen. Das ist nützlich, wenn eine konsistente Schnittstelle für ein komplexes System bereitgestellt werden soll, die einfacher zu verwenden, zu testen und zu warten ist. Um das Entwurfsmuster in Angular zu implementieren, kann ein Facade Service implementiert werden. Dieser Service kann dem Rest der Anwendung nur die notwendige Funktionalität zur Verfügung stellen, während die Implementierungsdetails dahinter verborgen bleiben.

Für das Beispiel in Listing 11 kann angenommen werden, dass ein komplexes System aus Services für einen Einkaufswagen (Cart) vorliegt. Die Verwendung des CartFacade Services würde diese Komplexität für die konsumierenden Komponenten reduzieren. Der Service fungiert als vereinfachte Schnittstelle zum komplexen Cart-System.

Listing 11: cart-facade.service.ts

import { Injectable } from '@angular/core';
 
@Injectable({
  providedIn: 'root',
})
export class CartFacadeService {
  constructor(
    private orderService: OrderService,
    private productService: ProductService,
    private userSessionService: UserSessionService,
    private localStorageService: LocalStorageService
  ) {}
 
  public addToCart(productId: string, quantity: number) {
    // add to cart logic
    const product = this.getProduct(productId);
    this.userSessionService.add(product, quantity);
    this.localStorageService.save();
  }
 
  public buy(productId: string, quantity: number) {
    // purchase logic
    this.orderService.purchase(productId, quantity);
  }
 
  public getProduct(productId: string) {
    // product logic
    this.productService.getProduct(productId);
  }
}
 

Mittels Dependency Injection kann der Facade Service dann wiederum in die Komponenten injiziert werden, die ihn benötigen (Listing 12).

Listing 12: Verwendung des CartFacadeService in app.component.ts

import { Component } from '@angular/core';
import { CartFacadeService } from './cart-facade.service';
 
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent {
  constructor(cartFacadeService: CartFacadeService) {}
 
  addToCart() {
    this.cartFacadeService.addToCart();
  }
 
  buy() {
    this.cartFacadeService.buy();
  }
}
 

Fazit

Design Patterns stellen wiederverwendbare Lösungen für häufig auftretende Probleme im Softwaredesign dar. Manchmal werden Entwurfsmuster unzweckmäßig oder übermäßig verwenden. Diese Verwendungen werden als Antipatterns bezeichnet, sie gilt es also zu vermeiden.

Observables sind ein leistungsfähiges Muster zur Verwaltung asynchroner Datenströme. Es ist jedoch möglich, sie unsachgemäß oder übermäßig zu nutzen, was zu unerwünschter Komplexität oder schlechter Performance führt. Ein häufiges Antipattern ist das mehrfache Subskribieren von Observables, was zu redundanten API-Aufrufen führt. Eine weitere unzulässige Nutzungsweise besteht darin, dass versäumt wird, sich von der Subskription eines Observables abzumelden (unsubscribe()), was zu Memory Leaks führt.

Um Antipatterns zu vermeiden, ist es wichtig, die Stärken und Schwächen der einzelnen Entwurfsmuster zu verstehen und sie in Angular-Anwendungen angemessen einzusetzen. Darüber hinaus ist es essenziell, Best Practices für die Angular-Entwicklung zu befolgen, wie z. B. die Verwendung von Dependency Injection, die Einhaltung des Single-Responsibility-Prinzips und das Schreiben von testbarem Code.

The post Software Design Patterns in Angular appeared first on Angular Camp.

]]>
Manfred Steyer im Interview https://angular-camp.de/blog/manfred-steyer-im-interview/ Tue, 25 Jul 2023 13:04:42 +0000 https://angular-camp.de/?p=7267 Es war eine Freude Manfred Steyer – programmierender Architekt, Google Developer Expert (GDE) und Trusted Collaborator – zu einem Interview auf den Java Script Days begrüßen zu dürfen. Er beantwortete uns Fragen rund um Microfrontends und Neuerungen in Angular und gibt Einblicke in seine Workshops zu wiederverwendbaren Komponenten.

The post Manfred Steyer im Interview appeared first on Angular Camp.

]]>

Herzlich willkommen. Wir sind hier live von dem 4-in-1-Trainingsevent zu JavaScript, Angular, React, HTML und CSS. Und neben mir sitzt Manfred, unser Trainer heute. Hi Manfred. Schön, dass du da bist und uns ein paar Fragen beantwortest, die wir uns ausgedacht haben zu deinem Workshop aber auch so zum Thema Angular. Manfred, du bist ja Trainer, Berater und programmierender Architekt; alles mit Fokus auf Angular. Natürlich Google Developer Expert, Trusted Collaborator im Angular-Team und du sprichst auf vielen Konferenzen und Seminaren. Du gibst zum Beispiel auch das Angular Camp unter anderem, das man unter unserem Entwickler Akademie Pseudonym kennt und bringst vielen Leuten Angular bei. Gibt es denn etwas, was du noch lernen möchtest?

Ich sage immer ich bin ein wenig wie ein streunender Hund. Ich habe da keine wirklichen Pläne, keinen Masterplan für die nächsten Jahre. Sondern ich schnappe einfach immer das, was mich gerade interessiert. Das ist auch gewissermaßen eine Luxussituation, weil das funktioniert eigentlich jetzt schon so seit ich in der Arbeitswelt bin, dass ich mich halt in Sachen reinstürze die mich gerade begeistern. Und weil ich eben dann entsprechend begeistert bin, kann ich dann das Wissen auch hoffentlich gut weitergeben.

ABTAUCHEN IM DEEP DIVE

Im Fortgeschrittenen Camp tauchen Sie ab unter die Oberfläche einer modernen Angular-Anwendung.
→ Nächster Termin: 2. - 4. Dezember, Berlin

Du bist unser Angular-Experte. Welche Herausforderungen stellen sich denn Experten bei der Erstellung von wiederverwendbaren Komponenten?

Also es ist irgendwie lustig, weil man glaubt vielleicht Angular zu kennen. Aber gerade dann, wenn man wiederverwendbare Komponenten schreibt, eine Komponenten library umsetzt, ein Designsystem implementiert, dann kommt man mit ganz neuen Aspekten von Angular in Kontakt die man so vielleicht noch gar nicht so richtig gesehen hat. So Sachen, wie Händels oder strukturelle Direktiven oder wie Templates und View Container haben wir gerade besprochen. Das sind Sachen die brauche ich typischerweise nicht oder maximal aus der Blackbox-Sicht heraus, wenn ich bestehende Komponenten zu Features zusammenfüge. Aber wenn ich die Komponenten selber schreibe, einen Date Ambika oder wie gesagt mein Design System, muss ich auch in diese Untiefen vordringen. Und da muss man sich einmal hineindenken, denn das ist nicht immer unbedingt ganz simpel. Wenn man mal drinnen ist, dann macht es natürlich Sinn, aber man muss sich wirklich reindenken.

Du stellst in deinem Workshop auch weiterführende Konzepte für wiederverwendbare Komponenten vor, wie zum Beispiel Komponenten Bibliotheken. Kannst du uns noch ein anderes Beispiel nennen?

Also die Bibliotheken sind eine Möglichkeit, um Komponenten zu kapseln und wiederverwendbar zu machen. Ich kombiniere das dann auch gerne mit Werkzeugen die zum Beispiel für die Library den Changelog automatisch generieren, weil so einen Changelog zu schreiben, was hat sich geändert von Version X auf Y ist ja immer ziemlich fad und das lässt sich tatsächlich wunderschön automatisieren. Zum Beispiel indem man ein LinkedIn für die Git Commit Messages hat und dann kann man aus dem LinkedIn und anderen einen schönen Changelog, wie man ihn auch von Angular direkt kennt, ableiten. Wenn es direkt um die Komponenten geht da geht’s um so Themen wie Direktiven – Attribut Direktiven, strukturelle Direktiven, Templates, View-Container, ViewChildren, die Kommunikation zwischen Komponenten.

Spannend. Und in deinem zweiten Workshop ging es um große Angular Anwendungen und Struktur in diese zu bringen. Welche Themen hast du dort behandelt?

Also das Problem ist bei großen Anwendungen, die von vielen Entwicklern aktiv entwickelt oder gewartet werden, wo ich vielleicht sogar die Situation habe, dass ich die über Jahre hinweg weiter warten muss und wo vielleicht sogar mehrere Teams arbeiten, da muss ich irgendwie sicherstellen, dass ich links was ändern kann, ohne rechts was kaputt zu machen. Und um das Ziel zu erreichen haben wir uns unter anderem mit Domain Driven Design beschäftigt. Das bietet mal Lösungen auf der logischen Ebene. Die Lösungen haben wir übertragen auf ein NX Monorepo, also ein großes Quellcode Repository, dass aus kleinen Fragmenten besteht die gemeinsam eben das gesamte System ergeben. Das heißt mit dem Monorepo kann ich eine große Lösung in kleine Häppchen untergliedern, kann dann sogar bei NX festlegen welches Häppchen auf welches andere Häppchen Zugriff hat. Somit vermeidet man, dass jeder mit jedem kommuniziert und dass ich links was ändere und damit rechts etwas kaputt mache, obwohl das gar nicht beabsichtigt ist. Dieses „Verschlimmbessern“, wie manche Leute auch sagen. Basierend auf dem haben wir uns dann mit Micro-Frontends beschäftigt. Micro-Frontends sind die eigentlich ja nichts anderes wie die Idee von Domain Driven Design auf die nächste Ebene gebracht. Plötzlich hat man pro Untergliederung eine eigene Anwendung. Somit können verschiedene Teams möglichst autonom arbeiten. Jedes Team hat seine Anwendung oder Anwendungen und kann autonomen dran arbeiten. Wenn sie fertig sind, dann werden die deployed. Also die Teams müssen sich weniger untereinander abstimmen, was immer dann super ist, wenn man merkt bei meinen vielen Teams wird der Abstimmungsaufwand einfach zu groß. Somit bekomme ich dann die Agilität von kleinen Teams zurück, obwohl ich große Lösungen schreibe.

Klingt gut, nach einem sehr spannendem Thema. Du hast gerade schon viel erklärt, wann Micro Frontends Sinn machen. Wann machen Sie denn nicht Sinn?

Also ich würde sagen nicht Sinn machen sie, wenn ich nur ein Team habe oder wenn ich vielleicht eine kleine Anzahl an Teams habe, die eigentlich auch über ein Monorepo zusammenarbeiten könnten. Ist das nicht der Fall, habe ich mehrere Teams und vertragen sich die nicht in einem einzigen Monorepo, weil die einen anderen Hintergrund haben, weil die Experten für unterschiedliche Domänen sind, weil die vielleicht ganz woanders sitzen, dann sind Micro Frontends sehr charmant. Dann kann das Team eben autark arbeiten, sogar eigene Entscheidungen treffen – Architekturentscheidungen aber auch Technologieentscheidungen. Das heißt schlussendlich könnte ich sogar in einer Micro-Frontend Architektur mehrere Frameworks haben. Mache ich nicht aus Jux und Tollerei. Aber es macht langfristig Sinn weil wir alle wissen, Technologien kommen und gehen; Nach ich würde mal sagen sieben Jahren muss sich eine Technologie entweder drastisch neu erfinden oder sie ist weg vom Fenstern und aus dem Grund tut schon gut wenn ich nach sieben Jahren mal das nächste Modul, die nächste Domäne mit einer aktuelleren Technologie programmieren kann. Somit kann man vom Text wegmigrieren zu einer Moderneren hin. Also Sinn macht es vor allem dann, um auf die Frage noch einmal zurückzukommen, wenn ich mehrere Teams habe, und als Nebeneffekt bekommt man, dass man peu à peu den Technologiestack migrieren kann.

Ok. Und dann habe ich jetzt noch eine letzte Frage an dich. Ich hatte vorhin schon erwähnt, du bist Trusted Collaborator im Angular-Team. Du hast also exklusive Einblicke. Kannst du uns sagen, ob es dann irgendwelche Neuerungen gibt, die demnächst auf uns zukommen in der Angular Welt?

Also was man derzeit auch sieht – da braucht man gar nicht exklusive Einblicke – das Angular Team arbeitet an zwei großen Themen derzeit. Das eine Thema ist Signale und das andere Thema ist Hydration. Bei Signalen/Signals geht es um einen neuen reaktiven Building Blog – so ähnlich wie ein RX-Chairs, aber viel einfacher – und weil das eine reaktive Bildung ist kann mir dieser Building Blog Bescheid geben wenn sich gebundene Daten ändern. Also ich habe vielleicht eine Adresse, die stelle ich dar im Browser und plötzlich ändert sich die Adresse, weil ich umgezogen bin und jetzt könnte das Signal Angular sagen: Pass auf, da hat sich was geändert und deswegen weiß Angular, es muss jetzt genau den Teil der Adresse aktualisieren. Also ich kann die Aktualisierung im Browser viel zielgerichteter machen. In der Vergangenheit sind immer ganze Komponenten aktualisiert worden und in der Vergangenheit war auch RX-Chairs nicht für jeden unbedingt immer simpel. Mit Signalen ändert sich beides. Es ist reaktiv, aber simpel und sehr zielgerichtet und nebenbei bekomme ich damit auch Altlasten Weg wie zum Beispiel Zone Chairs. Das ist von Anfang an bei Angular dabei und irgendwie so an der Grenze zwischen genial und ein wenig speziell und das kriegt man damit auch raus. Also Signale ist das eine. Das andere ist Hydration. Hydration ist eher interessant, wenn ich öffentliche Website schreibe, wo es um die Start Performance geht. Bei öffentlichen Websites muss relativ schnell was zum Schauen da sein, ansonsten springen die Benutzer ab. Da gibt es sehr schöne Statistiken wie sich der Umsatz nach unten entwickelt, wenn sich die Ladezeiten nach oben entwickeln – gerade bei anonymen Benutzern. Mit Hydration schafft man es, dass man zuerst serverseitig vorrendert. Das heißt ich liefere eine Seite aus und sehe sofort was – sehe sofort den Inhalt. Die Seite ist allerdings noch nicht interaktiv, weil sie erstmal statisch ist. Aus dem Grund lädt dann Angular peu à peu einzelne Komponenten nach damit die zum Leben erweckt werden. Das klappt prinzipiell schon länger. Das Stichwort war das Server-Side Rendering. Das Problem beim klassischen Server-Side Rendering war, ich habe auf einmal den gesamten Programmcode heruntergeladen. Das heißt ich habe ganz lange warten müssen, bis meine Seite, die ich schon gesehen habe, wirklich interaktiv war. Und das ändert sich jetzt mit Hydration, mit Progressive und Partial Hydration. Progressive Hydration bedeutet, das Ding überlegt sich welche Teile in welcher Reihenfolge heruntergeladen und mit Leben erfüllt werden. Vielleicht gibt es ein paar die ganz wichtig sind – der „Kaufen“ Button – und vielleicht gibt es ein paar Teile, die zwar auch wichtig sind, aber dennoch nachrangig sind. Bei einem Shop ist das vielleicht der Produktvorschlag – was könnte ich mir sonst noch kaufen. Da kann es dann eine Priorisierung geben und somit habe ich das Beste aus beiden Welten. Ich sehe relativ schnell was und die wichtigen Teile werden auch sehr schnell interaktiv, weil die zuerst geladen werden und nicht alles gemeinsam geladen wird. Das wäre progressive Hydration. Also nach und nach die Teile mit Leben erfüllen. Vielleicht gibt es dann irgendwann basierend darauf auch sowas wie Partial Hydration, wo sich ein Mechanismus überlegt was ich eigentlich gar nicht benötige. Wenn ich statischen Text habe den muss ich nicht zum Leben erwecken und Teile, die ich nie in den sichtbaren Bereich scrolle, muss ich auch nicht zum Leben erwecken. Die könnte man weglassen oder man könnte das rauszögern bis eben gescrollt wird. Und da arbeitet das Angular Team eben gerade fleißig dran und auch diesen Use Case abzudecken. Übrigens beide sind Use Cases, die derzeit auch von anderen Frameworks verfolgt werden. Da sieht man Angular ist bemüht am Puls der Zeit zu bleiben und diese ganzen Trends auch aufzunehmen.

Danke für das Interview und für die Beantwortung. Du bist bestimmt nächstes Jahr auch wieder mit dabei bzw. schon dieses Jahr im Oktober in Berlin. Also gerne vorbeikommen Manfred ist auch da.

Newsletter

Jetzt anmelden & regelmäßig wertvolle Insights in Angular sowie aktuelle Weiterbildungen erhalten!

The post Manfred Steyer im Interview appeared first on Angular Camp.

]]>
Eigene Standalone APIs für Angular https://angular-camp.de/blog/eigene-standalone-apis-fuer-angular/ Thu, 16 Mar 2023 09:03:59 +0000 https://angular-camp.de/?p=7189 Gemeinsam mit Standalone Components hat das Angular-Team die sogenannten Standalone APIs eingeführt. Sie bieten eine geradlinige Lösung zum Einrichten von Bibliotheken und kommen ohne Angular-Module aus. Populäre Beispiele von Bibliotheken, die diese Idee bereits aufgreifen, sind der HttpClient, der Router, aber auch NgRx. Diese Bibliotheken basieren auf einigen Mustern, die auch für eigene Vorhaben nützlich sind. Außerdem bieten sie den Konsument:innen unserer Bibliotheken vertraute Strukturen und Verhaltensweisen. In diesem Artikel stelle ich drei solcher Muster vor, die ich aus den genannten Bibliotheken abgeleitet habe. Der Quellcode mit den dazu verwendeten Beispielen findet sich unter [1].

The post Eigene Standalone APIs für Angular appeared first on Angular Camp.

]]>
Beispiel

Um die einzelnen Muster zu veranschaulichen, kommt hier eine einfache Logger-Bibliothek zum Einsatz (Abb. 1). Der LogFormatter formatiert die Nachrichten, bevor sie der Logger veröffentlicht. Es handelt sich dabei um eine abstrakte Klasse, die als DI-Token zum Einsatz kommt. Die Konsument:innen der Logger-Bibliothek können die Formatierung anpassen, indem sie eine eigene Implementierung bereitstellen. Alternativ können sie sich mit einer von der Bibliothek bereitgestellten Standardimplementierung zufriedengeben.

Abb. 1: Struktur einer beispielhaften Logger-BibliothekAbb. 1: Struktur einer beispielhaften Logger-Bibliothek

Der LogAppender ist ein weiteres austauschbares Konzept, das sich um das Anhängen der Nachricht an ein Protokoll kümmert. Die Standardimplementierung schreibt die Nachricht lediglich auf die Konsole.

Während es nur einen einzigen LogFormatter geben kann, unterstützt die Bibliothek mehrere LogAppender. Beispielsweise könnte ein erster LogAppender die Nachricht auf die Konsole schreiben, während ein zweiter sie auch an den Server sendet. Um das zu ermöglichen, werden die einzelnen LogAppender über Multiprovider registriert. Alle registrierten LogAppender gibt der Injector in Form eines Arrays zurück. Da sich ein Array nicht als DI-Token verwenden lässt, nutzt das Beispiel stattdessen ein InjectionToken:

export const LOG_APPENDERS =
  new InjectionToken<LogAppender[]>("LOG_APPENDERS");

Eine abstrakte LoggerConfig, die ebenfalls als DI-Token fungiert, definiert die möglichen Konfigurationsoptionen (Listing 1).

Listing 1

export abstract class LoggerConfig {
  abstract level: LogLevel;
  abstract formatter: Type<LogFormatter>;
  abstract appenders: Type<LogAppender>[];
}
 
export const defaultConfig: LoggerConfig = {
  level: LogLevel.DEBUG,
  formatter: DefaultLogFormatter,
  appenders: [DefaultLogAppender],
};

Die Standardwerte für diese Konfigurationsoptionen befinden sich in der Konstante defaultConfig. Das LogLevel in der Konfiguration versteht sich als Filter für Lognachrichten. Es ist vom Typ enum und weist zur Vereinfachung lediglich die Werte DEBUGINFO und ERROR auf:

export enum LogLevel {
  DEBUG = 0,
  INFO = 1,
  ERROR = 2,
}

Der Logger veröffentlicht nur Nachrichten, die den hier angegebenen oder einen höheren LogLevel aufweisen. Der LoggerService selbst erhält die LoggerConfig, den LogFormatter und ein Array mit LogAppender via DI und protokolliert damit die erhaltenen Nachrichten (Listing 2).

Listing 2

@Injectable()
export class LoggerService {
  private config = inject(LoggerConfig);
  private formatter = inject(LogFormatter);
  private appenders = inject(LOG_APPENDERS);
 
  log(level: LogLevel, category: string, msg: string): void {
    if (level < this.config.level) {
      return;
    }
    const formatted = this.formatter.format(level, category, msg);
    for (const a of this.appenders) {
      a.append(level, category, formatted);
    }
  }
 
  error(category: string, msg: string): void {
    this.log(LogLevel.ERROR, category, msg);
  }
 
  info(category: string, msg: string): void {
    this.log(LogLevel.INFO, category, msg);
  }
 
  debug(category: string, msg: string): void {
    this.log(LogLevel.DEBUG, category, msg);
  }
}

Newsletter

Jetzt anmelden & regelmäßig wertvolle Insights in Angular sowie aktuelle Weiterbildungen erhalten!

Die goldene Regel

Bevor wir einen Blick auf die Muster werfen, möchte ich noch meine goldene Regel für die Registrierung von Services erwähnen: Verwenden Sie @Injectable({providedIn: ‚root‘}) wann immer möglich! Besonders in Anwendungen, aber auch in zahlreichen Situationen in Bibliotheken reicht diese Vorgehensweise vollkommen aus. Sie ist einfach, treeshakable und funktioniert sogar mit Lazy Loading. Letztgenannter Aspekt ist weniger ein Verdienst von Angular als des zugrunde liegenden Bundler. Alles, was nur in einem lazy Bundle zum Einsatz kommt, ist auch dort untergebracht.

Muster: Provider Factory

Eine Provider Factory ist eine Funktion, die sämtliche Services für eine wiederverwendbare Bibliothek zurückliefert. Sie kann auch Konfigurationsobjekte als Services registrieren oder Service-Implementierungen austauschen.

Die zurückgelieferten Services befinden sich in einem Provider-Array, das die Factory mit dem Typ EnvironmentProviders ummantelt. Diese vom Angular-Team konzipierte Vorgehensweise stellt sicher, dass eine Anwendung die Provider nur bei sogenannten Environment Injectors registrieren kann. Dabei handelt es sich primär um den Injector für den Root-Scope sowie um Injectors, die Angular für einzelne Anwendungsbereiche über das Routing einrichtet. Die Provider-Factory in Listing 3 veranschaulicht das. Sie nimmt eine LoggerConfig entgegen und richtet die einzelnen Services für den Logger ein.

Listing 3

export function provideLogger(
  config: Partial<LoggerConfig>
): EnvironmentProviders {
  // using default values for   // missing properties
  const merged = { ...defaultConfig, ...config };
 
  return makeEnvironmentProviders([
    {
      provide: LoggerConfig,
      useValue: merged,
    },
    {
      provide: LogFormatter,
      useClass: merged.formatter,
    },
    merged.appenders.map((a) => ({
      provide: LOG_APPENDERS,
      useClass: a,
      multi: true,
    })),
  ]);
}

Fehlende Konfigurationswerte entnimmt die Factory der Standardkonfiguration. Die von Angular angebotene Funktion makeEnvironmentProviders verpackt das Provider-Array in eine Instanz von EnvironmentProviders. Diese Factory ermöglicht es den Konsumenten, den Logger ähnlich wie auch den HttpClient oder den Router einzurichten (Listing 4).

Listing 4

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(),
    provideRouter(APP_ROUTES),
    [...]
    provideLogger(loggerConfig),
  ]
}

BRINGEN SIE LICHT INS ANGULAR-DUNKEL

Die ersten Schritte in Angular geht man am besten im Basic Camp.
→ Nächster Termin: 9. - 11. Dezember, online

Muster: Feature

Das Feature-Muster erlaubt das Aktivieren und Konfigurieren optionaler Funktionalität. Kommt diese Funktionalität nicht zum Einsatz, entfernt sie der Build-Prozess mittels Treeshaking. Das optionale Feature wird durch ein Objekt mit einem providers-Array repräsentiert. Außerdem weist das Objekt eine Eigenschaft kind auf, die das Feature einer bestimmten Kategorie untergliedert. Diese Kategorisierung ermöglicht die Validierung der gemeinsam konfigurierten Features. Beispielsweise können sich Features gegenseitig ausschließen. Ein Beispiel dafür finden wir im HttpClient: Er verbietet die Nutzung eines Features zur Konfiguration des XSRF-Handlings, wenn die Konsument:innen gleichzeitig ein Feature zu dessen Deaktivierung aktiviert haben.

Die hier benutzte Logger Library verwendet ein ColorFeature, das es ermöglicht, Meldungen je nach LoggerLevel in verschiedenen Farben auszugeben (Abb. 2).

Abb. 2: Struktur des ColorFeaturesAbb. 2: Struktur des ColorFeatures

Zur Kategorisierung von Merkmalen kommt ein enum zum Einsatz:

export enum LoggerFeatureKind {
  COLOR,
  OTHER_FEATURE,
  ADDITIONAL_FEATURE
}

Objekte des Typs LoggerFeature repräsentieren die einzelnen Features:

export interface LoggerFeature {
  kind: LoggerFeatureKind;
  providers: Provider[];
}

Zur Bereitstellung des ColorFeature kommt eine weitere Factory zum Einsatz (Listing 5).

Listing 5

export function withColor(config?: Partial<ColorConfig>): LoggerFeature {
  const internal = { ...defaultColorConfig, ...config };
 
  return {
    kind: LoggerFeatureKind.COLOR,
    providers: [
      {
        provide: ColorConfig,
        useValue: internal,
      },
      {
        provide: ColorService,
        useClass: DefaultColorService,
      },
    ],
  };
}

Die aktualisierte Provider Factory provideLogger übernimmt mehrere Features über einen optionalen zweiten Parameter, der als Rest-Array definiert ist (Listing 6).

Listing 6

export function provideLogger(
  config: Partial<LoggerConfig>,
  ...features: LoggerFeature[]
): EnvironmentProviders {
  const merged = { ...defaultConfig, ...config };
 
  // Inspecting passed features
  const colorFeatures =
    features?.filter((f) => f.kind === LoggerFeatureKind.COLOR)?.length ?? 0;
 
  // Validating passed features
  if (colorFeatures > 1) {
    throw new Error("Only one color feature allowed for logger!");
  }
 
  return makeEnvironmentProviders([
    {
      provide: LoggerConfig,
      useValue: merged,
    },
    {
      provide: LogFormatter,
      useClass: merged.formatter,
    },
    merged.appenders.map((a) => ({
      provide: LOG_APPENDERS,
      useClass: a,
      multi: true,
    })),
 
    // Providing services for the     // features
    features?.map((f) => f.providers),
  ]);
}

Die Provider Factory nutzt die Eigenschaft kind, um die übergebenen Features zu untersuchen und zu validieren. Wenn alles in Ordnung ist, nimmt es die Provider des Features in das EnvironmentProviders-Objekt auf. Der DefaultLogAppender holt sich den ColorService, der vom ColorFeature per Dependency Injection bereitgestellt wird (Listing 7).

Listing 7

export class DefaultLogAppender implements LogAppender {
  colorService = inject(ColorService, { optional: true });
 
  append(level: LogLevel, category: string, msg: string): void {
    if (this.colorService) {
      msg = this.colorService.apply(level, msg);
    }
    console.log(msg);
  }
}

Da Features optional sind, übergibt der DefaultLog-Appender die Option {optional: true} an inject. Das verhindert eine Exception in Fällen, in denen das Feature und somit der ColorService nicht bereitgestellt wurden. Außerdem muss der DefaultLogAppender auf Nullwerte prüfen.

Dieses Muster kommt im Router vor, z. B. um das Preloading zu konfigurieren oder um Tracing zu aktivieren. Der HttpClient nutzt es zum Bereitstellen von Interceptors, zum Konfigurieren von JSONP und zum Konfigurieren/Deaktivieren des XSRF-Token-Handlings [2].

ABTAUCHEN IM DEEP DIVE

Im Fortgeschrittenen Camp tauchen Sie ab unter die Oberfläche einer modernen Angular-Anwendung.
→ Nächster Termin: 2. - 4. Dezember, Berlin

Muster: Configuration Factory

Configuration Factories erweitern das Verhalten vorhandener Services. Sie können dazu weitere Konfigurationsoptionen, aber auch zusätzliche Services bereitstellen. Zur Veranschaulichung soll hier eine erweiterte Version unseres LoggerService dienen. Sie erlaubt es, für jede Logkategorie einen zusätzlichen LogAppender zu definieren:

@Injectable()
export class LoggerService {
  readonly categories: Record<string, LogAppender> = {};
  […]
}

Um einen LogAppender für eine Kategorie zu konfigurieren, führen wir eine Configuration Factory mit dem Namen provideCategory ein (Listing 8).

Listing 8

export function provideCategory(
  category: string,
  appender: Type<LogAppender>
): EnvironmentProviders {
  // Internal/ Local token for registering the service
  // and retrieving the resolved service instance
  // immediately after.
  const appenderToken = new InjectionToken<LogAppender>("APPENDER_" + category);
 
  return makeEnvironmentProviders([
    {
      provide: appenderToken,
      useClass: appender,
    },
    {
      provide: ENVIRONMENT_INITIALIZER,
      multi: true,
      useValue: () => {
        const appender = inject(appenderToken);
        const logger = inject(LoggerService);
 
        logger.categories[category] = appender;
      },
    },
  ]);
}

Diese Factory erstellt einen Provider für die Klasse LogAppender. Der Aufruf von inject gibt uns eine Instanz davon und löst ihre Abhängigkeiten auf. Das Token ENVIRONMENT_INITIALIZER verweist auf eine Funktion, die Angular beim Initialisieren des jeweiligen Environment Injector anstößt. Sie registriert den LogAppender beim LoggerService (Listing 9).

Listing 9

export const FLIGHT_BOOKING_ROUTES: Routes = [
 
  {
    path: '',
    component: FlightBookingComponent,
    providers: [
      // Setting up an NgRx      // feature slice
      provideState(bookingFeature),
      provideEffects([BookingEffects]),
 
      // Provide LogAppender for      // logger category
      provideCategory('booking', DefaultLogAppender),
    ],
    children: [
      {
        path: 'flight-search',
        component: FlightSearchComponent,
      },
      [...]
    ],
  },
];

Dieses Muster findet man zum Beispiel in NgRx, um Feature-Slices zu registrieren. Auch das vom Router angebotene Feature withDebugTracing [3] nutzt dieses Muster, um das Observable events im Router Service zu abonnieren.

Fazit

Standalone APIs erlauben das Einrichten von Bibliotheken ohne Angular-Module. Ihre Nutzung ist zunächst einmal einfach: Konsument:innen müssen zunächst lediglich nach einer Provider Factory mit dem Namen provideXYZ Ausschau halten. Zusätzliche Features lassen sich ggf. mit Funktionen aktivieren, die sich am Namensschema withABC orientieren.

Die Implementierung solcher APIs ist jedoch nicht immer ganz trivial. Genau dabei helfen die hier vorgestellten Muster. Da sie aus Bibliotheken des Angular- und NgRx-Teams abgeleitet sind, spiegeln sie Erfahrungswerte und Entwurfsentscheidungen aus erster Hand wider.

 

The post Eigene Standalone APIs für Angular appeared first on Angular Camp.

]]>
Authentifizierung für Microfrontends und Frontend-Modulithen https://angular-camp.de/blog/authentifizierung-fuer-microfrontends-und-frontend-modulithen/ Thu, 15 Sep 2022 07:24:13 +0000 https://angular-camp.de/?p=7113 Authentifizierungs-Gateways übernehmen transparent die Authentifizierung von Benutzern. Das gestaltet die Implementierung einfacher, aber auch sicherer. Dank Tokenaustausch haben sie für jede Domäne das richtige Access-Token parat.

The post Authentifizierung für Microfrontends und Frontend-Modulithen appeared first on Angular Camp.

]]>
Microfrontend-Architekturen untergliedern eine große Lösung in mehrere Frontends. Das hilft beim Reduzieren der Komplexität und steigert die Flexibilität. Allerdings möchte sich der Benutzer zur Laufzeit nicht bei jedem einzelnen Microfrontend erneut anmelden müssen. Tokenbbasierte Authentifizierung kann hier helfen. Aber wie genau lässt sich dieses Konzept für Microfrontend-Architekturen implementieren?

In diesem Artikel zeige ich zunächst ein paar Varianten auf und diskutiere deren Konsequenzen. Basierend darauf wählen wir eine Variante, die sich gerade bei Microfrontend-Architekturen, aber auch bei anderen großen Lösungen, z. B. Modulithen (modulare Monolithen), besonders gut zu eignen scheint. Dabei veranschauliche ich den Einsatz eines Authentifizierungs-Gateways – eine generische Lösung, die alle schwierigen Aspekte der Authentifizierung kapselt und sowohl die (Micro-)Frontends als auch die adressierten APIs entlastet. Außerdem veranschaulicht das Beispiel die Nutzung des Tokenaustauschs, sodass wir für jede Domäne ein eigenes Access-Token nutzen können.

Als Identity Provider kommt Keycloak zum Einsatz. Dieselben Konzepte lassen sich aber auch mit anderen Lösungen, die die Standards OAuth 2 und OpenID Connect unterstützen, umsetzen. Der Quellcode des Gateways findet sich unter [1] und der Quellcode der Microfrontends unter [2] (Kasten: „Andere Identity Provider“).

Andere Identity Provider

Unter [1] findet sich auch eine Gateway-Konfiguration, die nicht nur erfolgreich mit Keycloak, sondern auch mit anderen Identity-Lösungen getestet wurde. Eine davon ist das Cloud-basierte Azure Active Directory. Hier basiert der Tokenaustausch nicht wie bei Keycloak auf [2], sondern lose auf [3]. Unterm Strich sind beide Implementierungen zwar sehr ähnlich, zumal der Tokenaustausch über eine POST-Anfrage erfolgt. Im Detail unterscheiden sich die Parameter jedoch. Deswegen wurde der hierfür verwendete Service austauschbar gestaltet.

Auth0 bietet derzeit keine Möglichkeit zum Tokenaustausch an. Hier müsste man sich anders behelfen. Eine prinzipielle Umsetzung des Gateways mit einer Beispielkonfiguration für Auth0 findet sich jedoch unter auch [1].

Der mittlerweile kommerzielle Identity Server [4] kommt mit einer eigenen Gateway-Lösung, die die hier besprochenen Use-Cases abdeckt. Hierbei ist von einem Backend for Frontend (BFF) [5] die Rede. Ein Tokenaustausch lässt sich hier als Erweiterung [6] mit ein paar Zeilen Code einbringen.

Newsletter

Jetzt anmelden & regelmäßig wertvolle Insights in Angular sowie aktuelle Weiterbildungen erhalten!

Optionen

Große Geschäftsanwendungen verwalten in den wenigsten Fällen Ihre Benutzerkonten selbst, sondern interagieren mit existierenden Identity Providern wie Keycloak oder Active Directory. Üblicherweise stellt der Identity Provider Securitytokens aus, die den Client über die Identität des Benutzers informieren sowie Zugriff auf das Backend geben.

Für die Nutzung von Tokens in Microfrontend-Architekturen ergeben sich mehrere Möglichkeiten (Abb. 1). Zum einen können die Tokens direkt im Browser oder in einer Sitzung im Backend verwaltet werden. Die direkte Verwaltung im Browser ist sehr geradlinig. Allerdings ist sie auch anfällig für Angriffe, zumal der Browser keine sichere Möglichkeit zum Verstauen von sensiblen Informationen wie Tokens bietet. Ein Angreifer könnte die Tokens somit via XSS entwenden und daraufhin im Namen des Benutzers auftreten.

steyer_authentifizierung_1.tif_fmt1.jpg
Abb. 1: Optionen für den Einsatz von Securitytokens

Zum andern stellt sich die Frage, wie feingranular die Rechte sein sollen, die sich aus einem Token ableiten. Im einfachsten Fall teilen sich alle Microfrontends ein Token, das Zugriff auf sämtliche Ressourcen im Backend gewährt. Auf diese Weise lässt sich Single-Sign-on für sämtliche Microfrontends sehr einfach umsetzen. Allerdings erhalten somit auch Angreifer, die das Token erfolgreich entwenden, Vollzugriff auf alle Services.

Alternativ dazu könnte der Identity Provider zunächst lediglich ein sehr feingranulares Token ausstellen, aus dem wenig Rechte hervorgehen. Braucht ein Microfrontend mehr Rechte, könnte es dieses feingranulare Token gegen ein Token für Services in seiner Subdomäne eintauschen.

Die nächsten Abschnitte gehen etwas genauer auf diese vier Varianten ein. Zuvor bietet der nachfolgende Abschnitt einen Überblick über die Nutzung von OAuth 2 und OpenID Connect zur Ausstellung von Tokens.

OAuth 2 und OpenID Connect für SPA

Große Geschäftsanwendungen verwalten in den wenigsten Fällen Ihre Benutzerkonten selbst, sondern interagieren mit existierenden Identity Providern wie Active Directory. Üblicherweise stellt der Identity Provider Securitytokens aus, mit denen der Client über die Identität des Benutzers informiert sowie Zugriff auf das Backend bekommt.

Damit der Identity Provider austauschbar bleibt, empfiehlt sich der Einsatz von standardisierten Protokollen. In der heutigen Welt der REST-artigen Services haben sich die Protokolle OAuth 2 und OpenID Connect durchgesetzt. Ersteres stellt dem Client ein Access-Token aus. Damit greift der Client im Namen des Benutzers auf das Backend zu. Das auf OAuth 2 aufbauende OpenID Connect versorgt den Client mit Informationen zum Benutzer. Das erfolgt über ein sogenanntes Identity-Token (ID-Token) und/oder bei Bedarf über eine HTTP-Anfrage bei einem normierten User-Info-Endpoint (Abb. 2). Der Client leitet hierzu den Benutzer zum Authorization-Server weiter. Dort muss sich der Benutzer zu erkennen geben, zum Beispiel mit Benutzername und Passwort oder Windows-Authentifizierung. Viele Authorization Server unterstützen auch verschiedene Arten der 2-Faktor-Authentifizierung.

steyer_authentifizierung_2.tif_fmt1.jpg
Abb. 2: OAuth 2 und OpenId Connect und Vogelperspektive

Nach erfolgreicher Authentifizierung muss der Benutzer dem Client explizit die Erlaubnis erteilen, in seinem Namen auf das Backend – von den Standards als Resource-Server bezeichnet – zugreifen zu dürfen. OAuth 2 spricht hierbei von Consent, also der Einwilligung oder Zustimmung des Benutzers. Gerade Geschäftsanwendungen überspringen diesen Punkt häufig. Hier geht man davon aus, dass das Unternehmen diese Einwilligung zum Zugriff auf Unternehmensressourcen bereits im Vorfeld erteilt hat.

Anschließend leitet der Authorization Server den Benutzer wieder zum Client um. Im Rahmen dieser Umleitung erhält der Client einen sogenannten Authorization-Code in Form eines URL-Parameters. Hat der Client Zugriff auf die HTTP-Payload, kann der Authorization Server den Parameter auch dort verstauen. Diesen Code tauscht der Client nun über eine verschlüsselte HTTPS-Verbindung gegen die Tokens.

ABTAUCHEN IM DEEP DIVE

Im Fortgeschrittenen Camp tauchen Sie ab unter die Oberfläche einer modernen Angular-Anwendung.
→ Nächster Termin: 2. - 4. Dezember, Berlin

Vom Implicit Flow zu Code-Flow und PKCE

Früher war es bei Single Page Applications üblich, die Tokens direkt in Form von URL-Parametern an den Client zu übersenden. Diesen vereinfachten Ablauf bezeichnen OAuth 2 und OpenID Connect als Implicit Flow. Damit wollte man den eingeschränkten Möglichkeiten in Browseranwendungen Rechnung tragen.

Aktuelle Best-Practice-Dokumente [7] raten davon jedoch ab, und mit der nächsten OAuth-Version 2.1 wird der Implicit Flow auch als veraltet gelten. Grund hierfür sind zahlreiche Attacken, die auf den Implicit Flow möglich sind. Allen voran Attacken, die darauf beruhen, dass URL-Parameter häufig sowohl in der Browser-History als auch in serverseitigen Logs gespeichert werden.

Stattdessen empfiehlt sich zumindest für neue Anwendungen der Einsatz des Authorization-Code-Flows. Dieser entspricht der zuvor erfolgten Beschreibung (Abb. 2) sowie der ursprünglichen Idee hinter OAuth 2.

Als Ergänzung empfehlen die aktuellen Best-Practice-Dokumente auch den Einsatz eines Proof Keys for Code Exchange [8], kurz PKCE (ausgesprochen „pixie“). Salopp gesprochen handelt es sich dabei um einen weiteren Code, der den Diebstahl des Access-Codes verhindern soll. Der Client generiert diesen Code und sendet ihn typischerweise als SHA256-Hash beim Initiieren des Flows in Form eines URL-Parameters an den Authorization Server. Beim Einlösen des Access-Codes via HTTPS präsentiert der Client den Code im Klartext und beweist somit, dass er den Flow initiiert hat und somit der rechtmäßige Besitzer des Access-Codes ist.

Clientseitiges Token-Handling

Die Implementierung der einzelnen durch OAuth 2 und OpenID Connect beschriebenen Flows in Browseranwendungen ist an und für sich kein Problem. Bibliotheken wie angular-oauth2-oidc [9] kümmern sich seit Jahren um diese Aufgabe und unterstützen mittlerweile sowohl den Implicit Flow als auch Code-Flow und PKCE (Listing 1).

Listing 1: Nutzung der Bibliothek Angular-oauth2-oidc in einer Angular-Anwendung

 

// Eckdaten zur Kommunikation mit dem Auth-Server bekannt geben
this.oauthService.configure(authCodeFlowConfig);
 
// Login starten (Umleitung auf Auth-Server)
this.oauthService.loadDiscoveryDocumentAndLogin();

 

Die eigentliche Herausforderung für die Anwendung entsteht jedoch erst nach dem Erhalt der Tokens: Die Tokens müssen irgendwo verstaut werden. Egal ob man sich für eine Eigenschaft im Programmcode, für den Session Storage oder den langlebigeren Local Storage entscheidet – sobald einem Angreifer eine XSS-Attacke gelingt, kann er die Tokens auslesen und im Namen des Benutzers auftreten.

Glücklicherweise gibt es mittlerweile einige Ansätze zum Abwehren von XSS-Attacken, und Securityaudits sind auch immer häufiger an der Tagesordnung. Um das Risiko bei einem Diebstahl weiter einzudämmen, ist es üblich, Tokens eine sehr kurze Lebenszeitspanne zu gewähren. Access-Tokens, die zum Beispiel nach 5 bis 20 Minuten ablaufen, sind keine Seltenheit. Das führt allerdings zum nächsten Problem: Der Client muss sich regelmäßig ein neues Token beim Identity Provider besorgen – und das möglichst ohne erneute Benutzerinteraktion.

Für diesen Token-Refresh gibt es leider keine gute Möglichkeit im Browser. Ein häufig anzutreffender Workaround sind HTTP-only-Cookies, die es dem Identity Provider ermöglichen, sich an den aktuellen Benutzer zu erinnern. Das Anfordern der neuen Tokens erfolgt bei diesem sogenannten Silent-Refresh häufig über ein verstecktes iFrame, sodass der Benutzer davon nichts mitbekommt und die aktuelle SPA geladen bleiben kann.

Leider stellt die Nutzung von Cookies in iframes auch ein mögliches Angriffsszenario dar, weswegen immer mehr Browser uns hier einen Strich durch die Rechnung machen. Eine andere Option ist der Einsatz von Refresh-Tokens, die es dem Client erlauben, neue Tokens ohne Benutzerinteraktion anzufordern. Allerdings müssen auch Refresh-Tokens irgendwo verstaut werden und ein Diebstahl solcher Tokens erlaubt es dem Angreifer, über lange Zeit hinweg im Namen des Benutzers aufzutreten.

Deswegen verbieten die Spezifikationen von OAuth 2 und OpenID Connect auch den Einsatz von Refresh-Tokens im Browser. Ein aktuelles Best-Practice-Dokument der OAuth-2.0-Arbeitsgruppe lockert diese Einschränkungen jedoch in bestimmten Fällen. Um die damit verbundenen Probleme jedoch prinzipiell zu lösen, spricht sich dasselbe Dokument aber auch sehr stark für die Handhabung von Tokens im Backend aus.

BRINGEN SIE LICHT INS ANGULAR-DUNKEL

Die ersten Schritte in Angular geht man am besten im Basic Camp.
→ Nächster Termin: 9. - 11. Dezember, online

Serverseitiges Token-Handling

Um es Angreifern zu erschweren, Tokens zu entwenden, bietet es sich an, Tokens lediglich in einer serverseitigen Benutzersitzung zu verwalten. Eine sehr geradlinige Möglichkeit dazu ist der Einsatz eines Authentifizierungs-Gateways (Abb. 3).

steyer_authentifizierung_3.tif_fmt1.jpg
Abb. 3: Authentifizierungs-Gateway kümmert sich um OAuth 2 und OIDC

Diese Spielart, die ich in Ausgabe 2.2022 genauer beschrieben habe, erlaubt es u. a., die vielen Details des Token-Handlings in eine generische Lösung auszulagern. Die Idee ist, sämtliche Zugriffe auf die SPA, aber auch auf die eingebundenen APIs durch das Authentifizierungs-Gateway zu tunneln.

Beim ersten Zugriff leitet das Authentifizierungs-Gateway den Benutzer zur Authentifizierung an den Identity Provider weiter. Die so erhaltenen Tokens verstaut das Gateway in einer Benutzersitzung. Um sich an den Benutzer zu erinnern, stellt das Gateway ein HTTP-only-Cookie aus. Solche Cookies lassen sich nicht via JavaScript lesen und somit auch nicht bei XSS-Attacken entwenden.

Bei allen weiteren Zugriffen auf das API ergänzt das Gateway die HTTP-Anfrage um das Access-Token. Außerdem kümmert es sich bei Bedarf um den Token-Refresh mittels Refresh-Tokens. Dem Client stellt das Gateway einen einfachen Endpunkt zur Verfügung, der Informationen zum Benutzer liefert. Weitere einfache Endpunkte erlauben es dem Client, den Benutzer abzumelden bzw. erneut anzumelden. Somit ist der Implementierungsaufwand für den Client minimal (Listing 2).

Listing 2: Angular-Code zur Kommunikation mit Authentifizierungs-Gateway

 

@Injectable({
  providedIn: 'root'
})
export class AuthService {
 
  constructor(private http: HttpClient) { }
 
  loadUserInfo(): Observable<unknown> {
    return this.http.get<unknown>('/userinfo');
  }
 
  login(): void {
    location.href = '/login';
  }
 
  logout(): void {
    location.href = '/logout';
  }
 
}

 

Aber auch die einzelnen Services haben mit dem Token-Handling an sich sehr wenig zu tun. Sie prüfen lediglich die eingehenden Access-Tokens.

Tokenaustausch: Eine Hand wäscht die andere

Natürlich möchte sich der Benutzer nicht bei jedem Microfrontend unserer Gesamtlösung separat anmelden. Somit ist es verlockend, ein einziges Token zu teilen. Das heißt aber nicht, dass dieses Token Zugriff auf sämtliche Ressourcen aller Microfrontends erlauben soll. Ein solcher Master Key wäre natürlich ein gefundenes Fressen für Angreifer und das wollen wir ihnen nicht gönnen.

Deswegen wäre es sinnvoller, zunächst ein Token mit wenig Rechten anzufordern. Dieses Token kann dann jedes Microfrontend gegen ein weiteres Token tauschen, das lediglich Zugriff auf seine Domäne erlaubt. Abbildung 4 zeigt ein Authentifizierungs-Gateway, das so einen Tokenaustausch durchführt. Mittlerweile existieren gleich mehrere Standards, die in der Welt von OAuth 2 einen Tokenaustausch ermöglichen. Beispiele dafür sind [10] und [11].

steyer_authentifizierung_4.tif_fmt1.jpg
Abb. 4: Authentifizierungs-Gateway tauscht Token

Beispielanwendung

Wie die vorangegangenen Diskussionen zeigen, ist der Einsatz von serverseitigem Token-Handling und jeweils eines eigenen Tokens pro Domäne bzw. Microfrontend sehr verlockend. Genau dieses Szenario veranschaulicht die hier gezeigte Beispielanwendung. Es handelt sich dabei um eine Shell mit zwei Microfrontends – das eine dreht sich um Flugbuchungen und das andere verwaltet Passagiere (Abb. 5).

 

steyer_authentifizierung_5.tif_fmt1.jpgAbb. 5: Beispielanwendung

Das Beispiel tunnelt sämtliche Zugriffe auf die Microfrontends und ihre APIs durch ein Gateway. Beim ersten Zugriff fordert das Gateway ein ID-Token, ein sehr eingeschränktes Access-Token sowie ein Refresh-Token an. Sobald das Microfrontend für die Flugbuchung auf sein API zugreift, tauscht das Gateway sein Access-Token gegen ein Access-Token, das Zugriff auf dieses API gewährt. Analog geht das Gateway beim Zugriff auf das Passagier-API durch das Passagier-Microfrontend vor.

Keycloak in Container starten

Als Identity Provider nutze ich in diesem Beispiel die populäre und freie Identity-Lösung Keycloak [12] aus der Feder von Red Hat. Als dieser Text geschrieben wurde, lag Keycloak in der Version 17 vor. Diese Version erlaubt den Tokenaustausch in Anlehnung an den Standard [11]. Die Umsetzung in Version 17 ist jedoch noch eine Technology Preview, die sich beim Start des Servers über ein Kommandozeilenargument bzw. über eine entsprechende Einstellung in der Konfigurationsdatei aktivieren lässt. Für Entwicklungszwecke kann Keycloak in einem Docker-Container gestartet werden. Zur besseren Lesbarkeit wurde der entsprechende Aufruf in Listing 3 auf mehrere Zeilen umgebrochen. Lassen Sie uns einen genaueren Blick auf die einzelnen Parameter werfen:

Listing 3: Keycloak im Docker-Container starten

 

docker run 
  -p 7777:8080
  --volume /c/Users/Manfred/[…]/data:/opt/keycloak/data
  -e KEYCLOAK_ADMIN=admin
  -e KEYCLOAK_ADMIN_PASSWORD=admin
  quay.io/keycloak/keycloak:17.0.1 start-dev
  -Dkeycloak.profile.feature.token_exchange=enabled
  -Dkeycloak.profile.feature.admin_fine_grained_authz=enabled
  • Der Parameter -p mappt den Container-internen Port 8080, auf dem standardmäßig Keycloak läuft, auf den Port 7777 des Hostrechners. Keycloak ist also nach dem Start über http://localhost:7777 erreichbar.

  • –volume mappt den Ordner, in dem Keycloak seine Datenbank ablegt, auf einen lokalen Ordner. Somit bleiben die getätigten Einstellungen erhalten. Standardmäßig nutzt Keycloak eine datei-basierte H2-Datenbank [13]. Für Entwicklungszwecke ist das auch ausreichend. Für den Produktionseinsatz lassen sich zahlreiche andere Datenbanken konfigurieren.

  • Die mit -e definierten Umgebungsvariablen legen Benutzername und Passwort des eingerichteten Administrators fest.

  • Der Parameter start-dev startet Keycloak im Entwicklungsmodus.

  • Der Parameter -Dkeycloak.profile.feature.token_exchange aktiviert die Preview-Implementierung des Tokenaustauschs. Um Details des Tokenaustausches konfigurieren zu können, aktiviert -Dkeycloak.profile.feature.admin_fine_grained_authz u. a. die entsprechenden Optionen in der Administrationskonsole.

Clients und Tokenaustausch in Keycloak konfigurieren

Nach dem Start von Keycloak lassen sich einzelne Clients in der Browser-basierten Administrations-Konsole konfigurieren. Für das hier gezeigte Beispiel benötigen wir die drei Clients gateway, flight-api und passenger-api. Die Einstellungen für das Gateway finden sich in Tabelle 1.

Einstellung Wert
Client ID gateway
Access Type confitential
Valid Redirect URIs http://localhost:8080/signin-oidc

http://localhost:8080/signout-callback-oidc

Base URL http://localhost:8080
Advanced Settings | Access Token Lifespan 10 Minutes
Advanced Settings | Proof Key for Code Exchange Code Challenge Method S256

Tabelle 1: Keycloak-Konfiguration des Gateways

 

Alle hier gezeigten Einstellungen beschränken sich auf jene, die von den Standardeinstellungen abweichen und für das besprochene Szenario von Bedeutung sind. Besonders wichtig sind die Valid Redirect URIs. Nur an die hier angegebenen URLs macht Keycloak einen Redirect. Das soll verhindern, dass sich Angreifer in den Flow einklinken, um z. B. in den Besitz des Access-Codes zu kommen. Neben den hier gezeigten Einstellungen erhält das Gateway im Registerblatt Credentials ein Client-Secret. Mit diesem gibt es sich später Keycloak gegenüber zu erkennen.

Die Einstellungen für die beiden APIs fallen ein wenig einfacher aus (Tabelle 2). Der Access-Type bearer-only gibt an, dass diese Clients lediglich Tokens empfangen und somit keinen Flow zum Abrufen von Tokens via OAuth 2 bzw. OpenID Connect initiieren. Aus diesem Grund entfällt auch die Angabe von Redirect-URIs sowie die Aktivierung von PKCE. Auch ein Client-Secret ist für diese Clients nicht notwendig.

Einstellung Wert
Client ID flight-api bzw. passenger-api
Access Type bearer-only
Advanced Settings | Access Token Lifespan 10 Minutes

Tabelle 2: Keycloak-Konfiguration des Flight-API und Passenger-API

Nun müssen wir es dem Gateway noch erlauben, erhaltene Access-Tokens gegen Access-Tokens für den Zugriff auf die beiden APIs einzutauschen. Dazu ist die Konfiguration des flight-api und des passenger-api zu erweitern. Die nötigen Einstellungen finden sich im Registerblatt Permissions, das in Keycloak 17 nur sichtbar ist, wenn Sie die oben diskutierte Preview-Implementierung aktivieren (Abb. 6).

steyer_authentifizierung_6.tif_fmt1.jpgAbb. 6: Permissions für APIs

Hier ist die Option Permissions Enabled zu aktivieren. Ein Klick auf den Link token-exchange führt zu einer Seite, die das Einrichten einer Policy für den Tokenaustausch erlaubt. Für das hier beschriebene Vorhaben ist eine Policy einzurichten, die einen Tokenaustausch erlaubt. Dabei handelt es sich um eine sogenannte Client-Policy. Um sie einzurichten, ist im Drop-down-Feld auf der rechten Seite der Eintrag Client auszuwählen (Abb. 7). Daraufhin erscheint ein Detaildialog (Abb. 8). Die Policy erhält einen beliebigen eindeutigen Namen. Außerdem ist im Drop-down-Feld der Client gateway auszuwählen.

steyer_authentifizierung_7.tif_fmt1.jpgAbb. 7: Client-Policy
steyer_authentifizierung_8.tif_fmt1.jpgAbb. 8: Policy für Tokenaustausch

Wechseln Sie nach dem Speichern der Policy auf die vorherige Seite und stellen Sie sicher, dass die neue Policy unter Apply Policy aufscheint (Abb. 7). Dazu müssen Sie ggf. die Policy aus dem Drop-down-Feld auswählen, nachdem Sie dort den Anfangsbuchstaben Ihres Namens erfasst haben. Diese Einstellung ist für beide APIs vorzunehmen.

Neben den Clients benötigen wir noch den einen oder anderen Benutzer, der sich auch in der Administrationskonsole einrichten lässt. Es empfiehlt sich, auch optionale Felder wie Vorname, Nachname oder E-Mail-Adresse auszufüllen. Wir werden diese Werte später im ausgestellten ID-Token wiederfinden. Ähnlich wie bei den Clients existiert für Benutzer ein eigenes Registerblatt Credentials. Hier lassen sich Passwörter hinterlegen (Kasten: „Demo: Keycloak in der Cloud“).

Demo: Keycloak in der Cloud

Alternativ können wir zum Testen der hier diskutierten Lösung auch einen Keycloak-Server in der Cloud betreiben. Sie finden alle nötigen Eckdaten in dem Abschnitt dieses Artikels, in dem es um die Konfiguration und den Start des Gateways geht.

Implementierung mit YARP und ASP.NET Core

Die Umsetzung des Gateways erfolgt mit YARP. Die Abkürzung YARP steht für Yet Another Reverse Proxy [14]. Das Besondere daran ist, dass Microsoft YARP als Middleware für ASP.NET Core entwickelt hat. Das bedeutet, dass man nicht nur einen vollwertigen Reverse Proxy mit den typischen Möglichkeiten wie Lastausgleich, Monitoring und Health-Checks bekommt, sondern dass sich auch sämtliche existierende Middlewarelösungen für ASP.NET Core sowie eigene Erweiterungen nutzen lassen. Zum Beispiel lässt sich Microsofts OpenID-Connect-Implementierung für ASP. NET Core mit ein paar Zeilen Code hinzufügen.

Das gesamte Gateway ist somit vereinfacht ausgedrückt eine Ansammlung von Standardmiddlewarekomponenten aus der Feder von Microsoft sowie ein paar Erweiterungen, die sich unter anderem um den Tokenaustausch sowie das Anhängen von Tokens an API-Aufrufe kümmern.

Wer nichts mit ASP.NET Core am Hut hat, kann die gesamte Lösung auch als Blackbox betrachten und sie in einem Container laufen lassen. Ein dockerfile hierfür liegt dem Beispiel unter [1] bei. Außerdem findet sich dort auch eine docker-compose.yml, die das Austauschen der Gateway-Konfiguration vereinfacht.

Konfiguration des Gateways

Die Konfiguration des Gateways befindet sich in der Datei appsettings.json (Listing 4).

Listing 4: YARP und Middlewarekomponenten konfigurieren

 

"Gateway": {
  "SessionTimeoutInMin": "60",
  "Url": "http://localhost:8080",
  "TokenExchangeStrategy": "default"
},
"Apis": [
  {
    "ApiPath": "/flight-api/",
    "ApiAudience": "flight-api"
  },
  {
    "ApiPath": "/passenger-api/",
    "ApiAudience": "passenger-api"
  }
],
"OpenIdConnect": {
  "Authority": "http://164.92.183.220:7777/realms/master",
  "ClientId": "gateway",
  "ClientSecret": "WmsokbzFvqWRWIijKLgLktMFVnQR1TUn",
  "Scopes": "openid profile email offline_access"
},

 

Da sich derzeit die verschiedenen Identity-Lösungen auf verschiedene Standards für den Tokenaustausch stützen und diese teilweise auch lose interpretieren, nutzt das Gateway hierfür einen austauschbaren Service. Dieser lässt sich über die Eigenschaft TokenExchangeStrategy konfigurieren. Die hier verwendete Einstellung default nutzt das unter [2] beschriebene Verfahren, das heute schon von Keycloak in seinen Grundzügen unterstützt wird. Dieses Verfahren scheint sich für das verfolgte Vorhaben am besten zu eignen. Aus diesem Grund ist davon auszugehen, dass es auch andere Produkte früher oder später unterstützen werden.

Weitere mögliche Werte sind AzureAd für die Interpretation von [3] durch Azure Active Directory und none für Fälle, in denen kein Tokenaustausch stattfinden soll bzw. die genutzte Identity-Lösung diese Möglichkeit gar nicht bietet.

Der Abschnitt Apis definiert für alle Pfade, die YARP auf ein API weiterleitet, die Client-ID des jeweiligen API als ApiAudience. Diese Information gibt das Gateway beim Tokenaustausch an. Das Ergebnis ist ein neues Access-Token, das Zugriff auf das jeweilige API erlaubt.

Die verwendete Keycloak-Version erlaubt noch nicht die Angabe von Scopes für dieses neue Access-Token. Das soll sich jedoch mit einer künftigen Version ändern. Ist das mal der Fall, können Sie dem Gateway pro API den gewünschten Scope über die Eigenschaft ApiScope mitteilen:

 

"ApiPath": "/flight-api/",
  "ApiAudience": "flight-api",
  "ApiScope": "read write delete"

 

Unter OpenIdConnect finden sich die Einstellungen für die OpenID-Connect-Middleware. Als Authority kommt der URL der verwendeten Keycloak-Instanz zum Einsatz. Das Beispiel verweist auf eine Keycloak-Instanz, die wir zum Testen über die Cloud anbieten. Sowohl die hier hinterlegte ClientId als auch das ClientSecret wurden in Keycloak für das Gateway konfiguriert. Der Scope drückt die Rechte aus, die der Client im Namen des Benutzers wahrnehmen möchte. Mit den Einträgen openid profile und email drückt der Client aus, dass er Benutzerdaten via OpenID Connect lesen möchte, und offline_access führt zur Ausstellung eines Refresh-Tokens.

Die appsettings.json enthält auch Konfigurationseinträge für YARP. Darunter finden sich Routen, die auf einzelne APIs weitergeleitet werden (Listing 5).

Listing 5: Routen und Cluster

 

"ReverseProxy": {
  "Routes": {
    "flightApiRoute": {
      "ClusterId": "flightApiCluster",
      "AuthorizationPolicy": "authPolicy",
      "Match": {
        "Path": "flight-api/{**remainder}"
      },
      "Transforms": [
        {
          "PathPattern": "/api/{**remainder}"
        }
      ]
    },
    [...]
  },
  "Clusters": {
    "flightApiCluster": {
      "Destinations": {
        "destination1": {
          "Address": "http://demo.angulararchitects.io"
        }
      }
    },
    [...]
  }
}

 

Der gezeigte Fall definiert eine Route für alle Pfade, die mit flight-api beginnen. Die Einstellung transforms ändert dieses Präfix auf den vom Flight API tatsächlich verwendeten Präfix api. Anschließend erfolgt die Weiterleitung an den weiter unten definierten Cluster, der wiederum auf die Adresse des Flight API verweist. Als Cluster bezeichnet YARP eine Menge von Adressen, hinter denen sich jeweils eine Instanz desselben Diensts befindet. Auf diese Weise lässt sich Load Balancing realisieren.

Gateways starten und Konfiguration austauschen

Standardmäßig nutzt das Gateway die Konfiguration in der Datei appsettings.json. Allerdings lässt sich eine davon abweichende Konfigurationsdatei beim Start sowohl über einen Kommandozeilenparameter als auch über eine Umgebungsvariable angeben. Ersteres kann bei lokalen Tests nützlich sein. Der folgende Aufruf nutzt zum Beispiel die beiliegende Keycloak-Demokonfiguration im Ordner conf:

dotnet run conf/appsettings.keycloak.json

Vor dem ersten Start müssen auch noch die Abhängigkeiten geladen werden:

dotnet restore

Für Testzwecke können Sie das Benutzerkonto jane, dessen Passwort ebenfalls jane lautet, verwenden. Weitere Demokonfigurationen, u. a. für Azure Active Directory, Auth0 oder Identity Server, finden sich auch in diesem Ordner. Alternativ dazu lässt sich die zu nutzende Konfigurationsdatei über die Umgebungsvariable GATEWAY_CONFIG angeben. Diese Möglichkeit ist u. a. beim Betrieb in einem Container interessant:

docker build -t auth-gateway .
 
docker run 
  -it --rm -p 8080:8080 
  -e GATEWAY_CONFIG=conf/appsettings.keycloak.json 
  --name auth-gateway auth-gateway 

Zur besseren Lesbarkeit wurde der Aufruf von docker run hier wieder umgebrochen. Etwas komfortabler gestaltet sich die Nutzung von Docker Compose. Die dafür beiliegende docker-compose.yml beinhaltet bereits alle nötigen Informationen, wie die zu nutzende Konfigurationsdatei und die Ports, die beim Aufruf von docker run von Hand angegeben werden müssen. Außerdem bildet sie den lokalen Ordner conf auf den entsprechenden Ordner im Container ab. Nach einer Änderung einer der Konfigurationsdateien ist ein Neustart des Gateways trotzdem notwendig, jedoch muss der Container nicht neu gebaut werden. Um das Gateway mit Docker Compose zu starten, nutzen Sie die folgende Anweisung:

docker compose up 

Ein Blick hinter die Kulissen des Gateways

Das YARP-basierte Gateway ist sehr leichtgewichtig aufgebaut. Im Wesentlichen registriert es lediglich Services bei einem DI-Container sowie Middlewarekomponenten, die ihren Teil zu allen Anfragen und Antworten beitragen (Listing 6).

Listing 6: YARP und Middlewarekomponenten konfigurieren

var builder = WebApplication.CreateBuilder(args);
 
//
// 1. Register Services for DI
//
 
builder.Services.AddReverseProxy()
  .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
 
builder.Services.AddAuthentication([...])
  .AddCookie([...])
  .AddSession([...])
  [...]
  .AddOpenIdConnect(options => { [...] });
 
 
var app = builder.Build();
 
//
// 2. Add Middleware Components
//
 
[...]
app.UseAuthentication();
app.UseAuthorization();
[...]
app.MapReverseProxy([…]);
[...]
 
//
// 3. Start Gateway
//
 
app.Run("http://+:8080");

Die von YARP benötigten Services registriert die Erweiterungsmethode AddReverseProxy beim DI-Container. Die zu nutzenden Einstellungen entnimmt das Beispiel der Sektion ReverseProxy in der Konfigurationsdatei.

Services für weitere im Lieferumfang von ASP.NET Core enthaltene Features fügen die Erweiterungsmethoden AddAuthenticationAddCookieAddSession und AddOpenIdConnect hinzu. Diese Methode nehmen zahlreiche hier nicht abgebildete Konfigurationsparameter. Darunter befinden sich zum Beispiel die Eckdaten für die Kommunikation mit dem Authorization Server.

Nach dem Aufruf der Methode Build kreiert das Beispiel eine Pipeline mit Middlewarekomponenten, die ASP.NET Core für jede ausgehende Anfrage und eingehende Antwort durchläuft. Die Erweiterungsmethode MapReverseProxy fügt am Ende der Pipeline die Middlewarekomponente für YARP hinzu. Die Parameter dieser Methode bieten auch Einsprungpunkte für Erweiterungen. Auf deren Basis stellt das Gateway sicher, dass zum richtigen Zeitpunkt ein Tokenaustausch stattfindet bzw. die entsprechenden Access-Tokens an API-Anfragen angehängt werden. Die Methode Run startet schlussendlich das Gateway.

Um den programmatischen Einsatz des Gateways zu vereinfachen, versteckt die hier besprochene Implementierung viele dieser Aufrufe hinter eigenen Erweiterungsmethoden (Listing 7).

Listing 7: Erweiterungsmethoden des Gateways

var builder = WebApplication.CreateBuilder(args);
 
[...]
 
//
// 1. Register Services for DI
//
 
builder.AddGateway(config, disco);
 
var app = builder.Build();
 
//
// 2. Add Middleware Components
//
 
app.UseGateway();
 
//
// 3. Start Gateway
//
 
if (string.IsNullOrEmpty(config.Url)) {
  app.Run();
}
else {
  app.Run(config.Url);
}

Einsatz zur Laufzeit

Das Logging des Gateways erlaubt es, die implementierte Funktionsweise nachzuvollziehen. Nach dem Start des Gateways bietet es Zugriff auf die Shell, die die einzelnen Microfrontends lädt. Entsprechend der gezeigten Konfiguration ist das Gateway über http://localhost:8080 erreichbar.

Die einzelnen angeforderten Access-Tokens protokolliert das Gateway im Debugmodus auf der Konsole. Es liegt auf der Hand, dass diese Funktion in der Produktion nicht verwendet werden soll. Bei den Tokens handelt es sich um JSON Web Tokens (JWT), die sich z. B. unter https://jwt.io dekodieren und inspizieren lassen. Dabei fällt auf, dass das erste angeforderte Access-Token lediglich Zugriff auf das Benutzerkonto gibt:

{
  "aud": "account",
  [...]
}

Beim Zugriff auf die APIs erfolgt der Tokenaustausch. Die so erhaltenen Tokens erlauben zusätzlich den Zugriff auf das jeweilige API:

{
  "aud": [
    "account",
    "flight-api"
  ],
  [...]
}

Fazit

Leider ist der Browser kein sicherer Ort für das Verstauen von Tokens. Auch für den Token-Refresh existieren clientseitig keine wirklich guten Optionen. Deswegen empfiehlt mittlerweile auch die OAuth-2-Arbeitsgruppe, das Handling der Tokens auf die Serverseite zu verlagern.

Dank eines Authentifizierungs-Gateways lässt sich diese Idee generisch implementieren, sodass sich weder der Client noch das Backend umfangreich damit belasten müssen.

Bei Microfrontends und großen Modulithen ist es allerdings zu wenig, sich nur ein einziges Token zu teilen. Vielmehr braucht man hier mehrere feingranulare. Deswegen empfiehlt es sich, zunächst nur ein Token mit sehr eingeschränkten Rechten anzufordern. Die einzelnen Microfrontends bzw. Frontend-Module können dieses Token ohne weitere Benutzerinteraktion gegen ein weiteres Token eintauschen, das den Zugriff auf ihr API bzw. ihre Domäne gewährt. Auch diese Aufgabe lässt sich in einem Authentifizierungs-Gateways automatisieren.

The post Authentifizierung für Microfrontends und Frontend-Modulithen appeared first on Angular Camp.

]]>
Wir sehen uns in der Zukunft! https://angular-camp.de/blog/wir-sehen-uns-in-der-zukunft/ Tue, 22 Mar 2022 09:16:15 +0000 https://angular-camp.de/?p=7082 Barrels sind nicht nur ein guter Ersatz für Angular-Module, sondern sie erlauben auch die Unterscheidung zwischen öffentlichen APIs und Implementierungsdetails. Bibliotheken in Nx-basierten Workspaces ermöglichen daneben auch das Einschränken von Zugriffen und somit das Erzwingen der festgelegten Frontend-Architektur.

The post Wir sehen uns in der Zukunft! appeared first on Angular Camp.

]]>
In der letzten Ausgabe haben wir die für künftige Angular-Versionen geplanten Standalone Components besprochen. Sie kommen gänzlich ohne die ohnehin kontrovers diskutierten Angular-Module aus und gestalten somit die gesamte Anwendung leichtgewichtiger (Kasten: „Standalone Components“). Nun stellt sich die Frage, wie man eine bestehende Angular-Lösung auf eine Zukunft ohne Angular-Module vorbereitet. Ich zeige dazu vier Optionen.

Standalone Components

Um eine Komponente als Standalone Component zu kennzeichnen, ist künftig die Eigenschaft standalone auf true zu setzen. Außerdem sind weitere benötigte Standalone Components im imports-Array zu hinterlegen:

[…]
import { Component } from './standalone-shim';
import { NavbarComponent, Sidebar-Component } from './shell';
 
@Component({
  standalone: true,
  selector: 'app-root',
  imports: [
    NavbarComponent, 
    SidebarComponent,
    HomeComponent,
    AboutComponent,
    HttpClientModule,
    RouterModule.forRoot([…])
  ],
  template: `[…]`
})
export class AppComponent {
}

Mit Imports lassen sich aber auch ganze Angular-Module referenzieren. Das erlaubt das Einbinden bestehender Building Blocks, die auf der Basis von Angular-Modulen geschrieben wurden.

Option 1: Vogel-Strauß-Strategie

Lassen Sie uns mit der einfachsten Option beginnen: die Vogel-Strauß-Strategie. Man stecke den Kopf in den Sand und ignoriere das gesamte Umfeld. Auch wenn das süffisant klingt, spricht eigentlich nichts dagegen. Niemand zwingt uns, Anwendungen auf Standalone Components umzustellen. Angular wird auch weiterhin Angular-Module unterstützen. Schließlich basiert das gesamte Ökosystem darauf. Somit können Sie getrost Standalone Components ignorieren oder diese neue Möglichkeit lediglich in neuen Anwendungen bzw. Anwendungsteilen nutzen.

Option 2: Angular-Module einfach wegwerfen

Auch diese Strategie wirkt auf den ersten Blick selbstgefällig: Sie entfernen einfach sämtliche Angular-Module aus Ihrem Quellcode. Das muss auch nicht in einem Rutsch erfolgen, denn Standalone Components spielen wunderbar mit Angular-Modulen zusammen. Angular-Module lassen sich in Standalone Components importieren und vice versa. Ein Beispiel für Ersteres finden Sie im Kasten „Standalone Components“, eines für Zweiteres in Listing 1.

Listing 1: Standalone Components und NgModules

import { NgModule } from '@angular/core';
import { SomeStandaloneComponent } from './some-standalone.component';
 
@NgModule({
  imports: [SomeStandaloneComponent],
  exports: [...],
  declarations: [...],
  providers: [...],
})
export class SomeModule { }

Möglich macht diese gegenseitige Kompatibilität das mentale Modell hinter Standalone Components. Demnach ist eine Standalone Component eine Kombination aus einer Komponente und einem Modul. Auch wenn die tatsächliche technische Umsetzung keine dedizierten Angular-Module einrichtet, hilft diese Vorstellung beim Brückenschlag zwischen den beiden Welten. Außerdem erklärt sie, warum Angular Module und Standalone Components sich wechselseitig importieren können.

Newsletter

Jetzt anmelden & regelmäßig wertvolle Insights in Angular sowie aktuelle Weiterbildungen erhalten!

Option 3: Angular-Module durch Barrels ersetzen

Bei Barrels handelt es sich um ECMAScript-Dateien, die zusammengehörige Building-Blocks (re-)exportieren:

// /shell/index.ts
export * from './navbar/navbar.component';
export * from './sidebar/sidebar.component';

Somit lassen sich Elemente, die in der Regel gemeinsam verwendet werden, gruppieren. Andere Teile der Anwendung können alle über ein Barrel exportierten Elemente mit einer einzigen Anweisung importieren:

import * as shell from '../shell';

Nennt sich das Barrel index.js bzw. index.ts, reicht es, lediglich den Ordner des Barrels zu importieren. Diese Vorgehensweise hat neben dem Gruppieren auch den Vorteil, dass sich Barrels zum Definieren öffentlicher APIs nutzen lassen. Wenn andere Programmteile nur auf das Barrel zugreifen, können wir alle anderen Building-Blocks als Implementierungsdetails betrachten. Da von diesen Implementierungsdetails keine anderen Anwendungsteile abhängig sind, lassen sie sich einfacher ändern. Hierbei handelt es sich um eine einfache, aber auch effektive Maßnahme für stabile Softwarearchitekturen.

In einem weiteren Schritt könnte jedes Barrel auch ein Path Mapping über die tsconfig.json erhalten. In diesem Fall kann die Anwendung über schöne Namen, die den Namen von npm-Paketen gleichen, auf das Barrel zugreifen:

import * as shell from '@demo/shell';

Allerdings kommen Barrels auch mit Herausforderungen. Beispielsweise sind sie häufig die Ursache für zyklische Abhängigkeiten. Abbildung 1 veranschaulicht das anhand der Datei b.ts, die zum einen vom Barrel (index.ts) referenziert wird und zum anderen auf das Barrel zugreift.

steyer_standalone_1.tif_fmt1.jpgAbb. 1: Zyklische Abhängigkeiten via Barrel

Dieses Problem lässt sich mit zwei einfachen Regeln, die es konsequent einzuhalten gilt, von vornherein vermeiden:

  • Ein Barrel darf nur Elemente aus seinem Hoheitsgebiet veröffentlichen. Das Hoheitsgebiet erstreckt sich über den Ordner des Barrels sowie dessen Unterordner.

  • Innerhalb eines Hoheitsgebietes referenzieren sich Dateien über relative Pfade ohne Nutzung des Barrels.

Diese Regeln klingen zwar auf dem ersten Blick etwas abstrakt, allerdings gestaltet sich deren Umsetzung einfacher, als man denken würde. Um den zuvor aufgezeigten Zyklus zu vermeiden, greift in Abbildung 2 beispielsweise b.ts direkt auf die Datei a.ts zu, die sich im gleichen Hoheitsgebiet befindet. Auf den Umweg über das eigene Barrel wird verzichtet.

steyer_standalone_2.tif_fmt1.jpgAbb. 2: Zyklische Abhängigkeiten vermeiden

Dieses Problem lässt sich mit Linting in den Griff kriegen. Eine Linting-Regel müsste unerlaubte Zugriffe erkennen und anprangern. Das populäre Werkzeug Nx kommt mit einer solchen Regel, mit der sich auch noch weitere nicht gewünschte Zugriffe verhindern lassen. Schauen wir uns diese Idee genauer an.

BRINGEN SIE LICHT INS ANGULAR-DUNKEL

Die ersten Schritte in Angular geht man am besten im Basic Camp.
→ Nächster Termin: 9. - 11. Dezember, online

Option 4: Nx Workspace mit Bibliotheken und Linting-Regeln

Das Werkzeug Nx [1] basiert auf dem Angular CLI und bringt viele Annehmlichkeiten für die Entwicklung großer Unternehmenslösungen. Nx erlaubt das Untergliedern eines großen Projekts in verschiedene Anwendungen und Bibliotheken. Jede Bibliothek hat ein öffentliches API, das ein Barrel mit dem Namen index.ts festlegt. Nx spendiert allen Bibliotheken ein Path Mapping und bringt zudem eine Linting-Regel, die das Umgehen des Barrels verhindert und andere Einschränkungen zulässt.

Diese Linting-Regel erlaubt das Erzwingen einer festgelegten Frontend-Architektur. Das Nx-Team empfiehlt z. B., eine große Anwendung vertikal nach fachlichen Domänen und horizontal nach den technischen Kategorien der Bibliotheken zu untergliedern (Abb. 3).

steyer_standalone_3.tif_fmt1.jpgAbb. 3: Architekturmatrix

Featurebibliotheken beinhalten zum Beispiel Smart Components, die Anwendungsfälle realisieren, während UI-Bibliotheken wiederverwendbare Dumb Components beherbergen. Domain-Bibliotheken kapseln das clientseitige Domänenmodell sowie Services, die darauf operieren. Utility-Bibliotheken gruppieren allgemeine Hilfsfunktionen.

Mit den genannten Linting-Regeln lässt sich nun sicherstellen, dass jede Schicht nur auf darunterliegende Schichten zugreifen darf. Zudem lassen sich Zugriffe auf andere Domänen verhindern. Bibliotheken aus dem Bereich Booking dürfen somit nicht auf Bibliotheken in Boarding zugreifen. Möchte man bestimmte Konstrukte domänenübergreifend nutzen, sind sie z. B. im Bereich shared zu platzieren. Verletzen Programmteile eine dieser Regeln, gibt der Linter augenblicklich Feedback (Abb. 4).

steyer_standalone_4.tif_fmt1.jpgAbb. 4: Feedback zu einer Linting-Regel

Die hierfür von Nx genutzte Ordnerstruktur spiegelt die gezeigte Architekturmatrix wider: Die Unterordner in libs repräsentieren die Domänen. Die darin zu findenden Bibliotheken bekommen ein Präfix wie feature- oder domain-. Diese Präfixe spiegeln die technischen Kategorien und somit die Layer wider (Abb. 5).

steyer_standalone_5.tif_fmt1.jpgAbb 5: Aufbau eines Nx Workspace

Diese vierte Option hat sich schon länger im Zusammenspiel mit Angular-Modulen zur Strukturierung großer Lösungen bewährt. Jede Bibliothek bekommt dazu genau ein Angular-Modul zugewiesen. Sobald Angular-Module jedoch optional sind, können die Angular-Module weggelassen werden. In diesem Fall dienen nur noch die Bibliotheken der Strukturierung: Ihre Barrels gruppieren zusammengehörige Building Blocks wie Standalone Components, und dank der genannten Linting-Regeln können wir unsere Architekturen erzwingen.

Fazit

Niemand muss wegen Standalone Components in Panik ausbrechen. Es handelt sich dabei nur um eine Option, und die gewohnten Angular-Module werden auch künftig unterstützt. Das bedeutet auch, dass sich bestehender Code ohne Probleme wiederverwenden lässt.

Möchte man aber die Vorteile von Standalone Components nutzen, reicht es zuerst aus, die bestehenden Angular-Module zu entfernen und die einzelnen Building Blocks direkt zu importieren. Zum Gruppieren von zusammengehörigen Konzepten bietet sich der Einsatz von Barrels an. Sie erlauben auch eine Unterscheidung zwischen internen Implementierungsdetails und öffentlichen APIs.

Das auf dem CLI basierende Werkzeug Nx bringt weitere Annehmlichkeiten: Lösungen lassen sich mit Bibliotheken untergliedern, wobei jede Bibliothek nicht nur ein Barrel zum Festlegen des öffentlichen API erhält, sondern auch ein Path Mapping für einen komfortablen Zugriff. Außerdem erlaubt Nx auch das Einschränken von Zugriffen zwischen Bibliotheken und somit das Sicherstellen der festgelegten Frontend-Architektur.

Genau deswegen wird Nx bereits heute sehr gerne für große Angular-Lösungen genutzt. Das heute noch pro Bibliothek benötigte Angular-Modul kann beim Umstellen auf Standalone Components entfernt werden. Als Ersatz fungiert dann das Barrel, das die einzelnen Standalone Components direkt veröffentlicht.

 

Links & Literatur

[1] https://www.angulararchitects.io/aktuelles/tutorial-first-steps-with-nx-and-angular-architecture/

The post Wir sehen uns in der Zukunft! appeared first on Angular Camp.

]]>
Blitzschnelle Angular Builds https://angular-camp.de/blog/blitzschnelle-angular-builds/ Thu, 08 Apr 2021 12:28:58 +0000 https://angular-camp.de/?p=6950 Nx erkennt geänderte Programmteile und baut auch nur diese erneut. Alle anderen Systembestandteile werden aus einem Cache bezogen. Damit lässt sich der Build-Vorgang enorm beschleunigen.

The post Blitzschnelle Angular Builds appeared first on Angular Camp.

]]>
Moderne Frontends werden immer größer und das wirkt sich auch auf die Build-Zeiten aus. Mit inkrementellen Builds lassen sie sich in den Griff bekommen. Somit müssen nur jene Teile, die von Änderungen betroffen sind, neu gebaut werden. Der Rest kommt aus einem Cache. Diese Strategie, die bei Google schon seit Jahren erfolgreich eingesetzt wird, benötigt entsprechende Werkzeuge. Nx [1] ist ein solches. Es ist frei verfügbar und basiert auf der Angular CLI. In diesem Artikel wird gezeigt, wie sich Nx zum inkrementellen Kompilieren von Angular-Anwendungen einsetzen lässt. Das verwendete Beispiel findet sich in meinem GitHub-Account [2].

Fallstudie

Die hier verwendete Fallstudie basiert auf einem Nx-Workspace. Er ist in eine Anwendung flights und drei Bibliotheken untergliedert (Abb. 1).

steyer_nx_incremental_1.tif_fmt1.jpgAbb. 1: Fallstudie

BRINGEN SIE LICHT INS ANGULAR-DUNKEL

Die ersten Schritte in Angular geht man am besten im Basic Camp.
→ Nächster Termin: 9. - 11. Dezember, online

 

Nx ist in der Lage, jede Bibliothek separat zu kompilieren. Bibliotheken, die sich nicht geändert haben, kann es einem Cache entnehmen. Das ist der Schlüssel für den inkrementellen Build. Kommt Domain-driven Design zum Einsatz, könnte der Nx-Workspace jede Domäne durch solch eine Gruppe mit Bibliotheken repräsentieren. In diesem Fall kann Nx mit Zugriffseinschränkungen eine lose Kopplung zwischen Domänen sicherstellen [3]. Zum Einrichten eines solchen Workspaces steht der folgende Befehl zur Verfügung:

npm init nx-workspace flights

Er lädt die neueste Version von Nx herunter und generiert damit einen Workspace. Im Zuge dessen sind ein paar Fragen zu beantworten (Abb. 2).

steyer_nx_incremental_2.tif_fmt1.jpgAbb. 2: Erzeugung eines neuen Nx-Workspace für Angular-Projekte

Diese Anweisung erzeugt auch innerhalb des Workspaces eine erste Angular-Anwendung. Außerdem initialisiert Nx für den Workspace ein lokales Git Repository. Dessen History nutzt es später, um herauszufinden, welche Dateien geändert wurden. Für einige Vergleiche nutzt Nx auch den Hauptzweig. Sein Name kann in der nx.json-Datei festgelegt werden:

"affected": {
  "defaultBase": "main"
},

Mit dem Angular CLI lassen sich nun die Bibliotheken erzeugen:

ng g lib domain --directory luggage --buildable
ng g lib feature-checkin --directory luggage --buildable

Die Schalter directory und buildable kommen von Nx. Ersterer gibt das Verzeichnis an, in dem die Bibliothek zu erstellen ist. Beim Einsatz von DDD spiegelt es die jeweilige Domäne wider. Letzterer gibt an, dass die Bibliothek gebaut werden kann. Das ist notwendig für inkrementelle Builds. Als Alternative zu –buildable kann auch –publishable verwendet werden (Kasten: „–publishable“).

–publishable

Als Alternative zu –buildable lässt sich auch der Schalter –publishable verwenden. Er generiert ein paar weitere Dateien, die es erlauben, die Bibliothek nach dem Bauen auch via npm zu veröffentlichen. Beim Einsatz von –publishable ist seit Nx 10 zusätzlich der Schalter –import-path zu nutzen. Mit ihm lässt sich der Name des npm-Pakets angeben. Das ist notwendig, da die von Nx intern verwendeten Namen nicht zwangsweise als npm-Paketnamen verwendet werden dürfen.

Das Erzeugen von Bibliotheken und Domänen lässt sich mit dem Nx-Plug-in @angular-architects/ddd [5] automatisieren. Es erzeugt auch die nötigen Verweise zwischen den Bibliotheken und konfiguriert die oben erwähnten Zugriffseinschränkungen.

Inkrementelle Builds

Um in den Genuss von inkrementellen Builds zu kommen, ist anstatt des Angular CLI das Nx CLI zu verwenden. Es lässt sich via npm installieren:

npm i -g nx

Der Aufruf zum Kompilieren gleicht jenem des Angular CLI:

nx build luggage --with-deps

Neu ist jedoch der Schalter with-deps. Er veranlasst Nx dazu, jede Bibliothek separat zu kompilieren und das Ergebnis zu cachen. Wird diese Anweisung ein weiteres Mal ausgeführt, erhält man blitzschnell das Ergebnis aus dem Cache. Liegen hingegen Änderungen an einigen Bibliotheken vor, werden zumindest die nicht geänderten Bibliotheken aus dem Cache bezogen. Um das zu veranschaulichen, bietet es sich an, den aktuellen Stand zu committen:

git add *
git commit -m 'init'

Wird nun zum Beispiel die Bibliothek luggage-feature-checkin geändert, kann Nx das durch einen Blick in die Git History herausfinden. Diese Erkenntnis lässt sich auch mit

nx affected:dep-graph

visualisieren (Abb. 3).

steyer_nx_incremental_3.tif_fmt1.jpgAbb. 3: Abhängigkeitsgraph mit betroffenen Bibliotheken

Wie dieser Graph zeigt, ermittelt Nx nicht nur die geänderten Systembestandteile, sondern auch die, die von diesen Änderungen betroffen sind. Das sind alle, die von den geänderten abhängig sind. All diese gilt es nun, neu zu bauen. Ein nochmaliger Aufruf von

nx build luggage --with-deps

kümmert sich darum (Abb. 4).

steyer_nx_incremental_4.tif_fmt1.jpgAbb. 4: Inkrementeller Build

Wie der aus Platzgründen etwas gekürzte Screenshot zeigt, baut Nx tatsächlich nur die beiden betroffenen Systembestandteile.

ABTAUCHEN IM DEEP DIVE

Im Fortgeschrittenen Camp tauchen Sie ab unter die Oberfläche einer modernen Angular-Anwendung.
→ Nächster Termin: 2. - 4. Dezember, Berlin

Output-Cache

Der genutzte Cache lässt sich in nx.json verwalten. Genaugenommen wird hier ein sogenannter Task-Runner konfiguriert, der an einen Cache delegiert (Listing 1).

Listing 1

"tasksRunnerOptions": {
  "default": {
    "runner": "@nrwl/workspace/tasks-runners/default",
    "options": {
      "cacheableOperations": ["build", "lint", "test", "e2e"]
    }
  }
},

Die Eigenschaft cacheableOperations listet alle Anweisungen, deren Ergebnisse gecacht werden sollen. Die Eigenschaft runner verweist auf den zu nutzenden Task-Runner. Der hier gezeigte und standardmäßig eingerichtete Runner nutzt einen lokalen dateisystembasierten Cache. Er verwaltet seine Einträge im Projekt unter node_modules/.cache/nx (Abb. 5).

steyer_nx_incremental_5.tif_fmt1.jpgAbb. 5: Lokaler Cache

Nun ist es natürlich wünschenswert, den Cache mit anderen Teammitgliedern zu teilen. Hierzu stellt das Team hinter Nx die kommerzielle Nx-Cloud zur Verfügung. Sie existiert als Cloud-Lösung, lässt sich mittlerweile aber auch lokal installieren. Alternativ dazu kann man den Ordner mit dem Cache auch über einen Symlink auf ein Netzlaufwerk verweisen lassen. Da Nx Open Source ist, lassen sich auch eigene Cacheimplementierungen schreiben. Ein Beispiel dafür ist Apployees-Nx [4], (Kasten: „Apployees-Nx“). Dieses Projekt erlaubt den Einsatz von Datenbanken wie MongoDB oder Redis zum Verwalten des Nx-Cache. Erste Tests brachten ein vielversprechendes Ergebnis mit sich. Allerdings muss man an dieser Stelle auch erwähnen, dass das API der Task-Runner noch nicht final und somit möglicherweise Änderungen unterworfen ist.

Apployees-Nx

Beim Einsatz von Apployees-Nx und Redis ist zu beachten, dass die anzugebende Verbindungszeichenfolge einen Benutzernamen enthalten muss:

rediss://<dummy-user>:<password>@<host>:<port>/<db-name>

Da Redis allerdings keine Benutzernamen verwendet, kann hier ein beliebiger Dummywert angegeben werden.

Newsletter

Jetzt anmelden & regelmäßig wertvolle Insights in Angular sowie aktuelle Weiterbildungen erhalten!

Fazit

Durch das separate Kompilieren von Bibliotheken und den Einsatz eines Cache ist Nx in der Lage, inkrementelle Builds durchzuführen. Das beschleunigt den gesamten Build-Vorgang. Damit das möglich ist, müssen wir jedoch unsere Anwendungen in Bibliotheken untergliedern. Das wirkt sich auch auf die Struktur der gesamten Anwendung positiv aus: Jede Bibliothek hat klare Grenzen und kann Implementierungsdetails vor anderen Bibliotheken verbergen. Nur das veröffentlichte API muss abwärtskompatibel bleiben, um Breaking Changes zu vermeiden. Dieser Ansatz passt auch wunderbar zu den Ideen von DDD [3]. Pro Domäne wird eine Gruppe mit Bibliotheken erzeugt. Zwischen den einzelnen Gruppen, aber auch zwischen den Bibliotheken einer Gruppe kann Nx mit Zugriffseinschränkungen eine lose Kopplung erzwingen. Neben einem lokalen Cache unterstützt Nx auch Netzwerkcaches. Somit wird sichergestellt, dass jeder Programmstand im gesamten Team nur ein einziges Mal kompiliert werden muss.

The post Blitzschnelle Angular Builds appeared first on Angular Camp.

]]>
Interview mit Manfred Steyer https://angular-camp.de/blog/interview-mit-manfred-steyer/ Wed, 10 Feb 2021 10:05:15 +0000 https://angular-camp.de/?p=6928 Manfred Steyer beantwortet Fragen zu den neusten Entwicklungen in der Angular Welt. Welche Highlights gab es in 2020 und was kommt 2021 auf Angular-Begeisterte zu?

The post Interview mit Manfred Steyer appeared first on Angular Camp.

]]>
„Hallo Manfred und danke, dass du dir die Zeit für dieses Gespräch nimmst. Lass uns doch erst einmal einen Blick zurückwerfen: Was war denn für dich als Softwarearchitekt und Angular-Experte die wichtigste technologische Neuerung 2020?

Manfred Steyer: Das war auf jeden Fall Module Federation, weil es ganz neue Use Cases erlaubt, die meine Kunden schon seit langem brauchen. Bisher musste man für die Umsetzung von Micro-Frontend-Architekturen, bei denen unabhängige sowie separat deployte Anwendungen zur Laufzeit geladen werden, ordentlich in die Trickkiste greifen. Mit Module Federation haben wir endlich eine geradlinige Lösung dafür.

Im Herbst 2020 ist ja auch Angular 11 erschienen. Wie würdest du das Release beschreiben?

Manfred Steyer: Ich würde sagen, Version 11 ist eine weitere Abrundung. Die Entwicklung bei Angular ist ja wirklich sehr evolutionär – genau das, was man auch im Enterprise-Umfeld, wo Angular stark ist, braucht: Keine großen Umbrüche, dafür ein kleines Bündel an neuen Features alle 6 Monate.

Ganz ohne Neuerungen kam Angular 11 aber ja auch nicht aus. Was ist denn für dich spannend daran?

Steyer: Die wichtigste Neuerung für mich und viele meiner Kunden ist, dass das Angular CLI nun webpack 5 unterstützt – zumindest ist das ein experimentelles Feature. webpack 5 ist wichtig, weil es mit Module Federation eine sehr solide und innovative Lösung für die Umsetzung von Micro Frontends bietet. Hierzu haben wir in der Vergangenheit viele Tricks und Workarounds einsetzen müssen. Die brauchen wir zum Glück nicht mehr.

Und was denkst du, wie es da 2021 weitergehen wird?

Manfred Steyer: Die webpack-5-Integration wird hoffentlich mit dem CLI 12, das im Frühling 2021 kommt, produktionsreif sein. Aktuell handelt es sich dabei nur um ein experimentelles Opt-In. Das Ganze ist also derzeit eher etwas fürs Prototyping.

Du hast die Weiterentwicklung von Angular 11 ja gerade eben als „evolutionär“ bezeichnet. Wenn man sich das gesamte JavaScript-Ökosystem anschaut, würdest du sagen, dass es da inzwischen auch eher ruhiger wird oder stehen wir vor der nächsten Disruption?

Manfred Steyer: Ich erlebe die Web-Welt derzeit als eher evolutionär. In der Zeit vor 2015 habe ich sie dagegen als sehr (fr-)agil wahrgenommen. Mittlerweile haben sich gewisse defacto-Standards herauskristallisiert und das bringt Stabilität.

Das soll aber nicht heißen, dass es keine neuen und innovativen Entwicklungen gibt. Neben Module Federation hat zum Beispiel das Team hinter Nx einige coole Sache eingeführt, wie inkrementelle Builds und Tests sowie einen Build-Cache. Die Idee ist zwar nicht neu – beispielsweise würde Bazel dasselbe leisten – aber es ist gefühlt das erste Mal, dass sowas als quasi Turn-Key-Solution für einen Main-Stream-Stack wie Angular oder React zur Verfügung steht.

Ich denke aber auch an die React Server Components, die sehr wohl das Potential haben, frischen Wind und eventuell sogar einen Paradigmenwechsel zu etablieren.

Wenn du dir etwas für die JavaScript-Welt wünschen dürftest, was wäre das?

Manfred Steyer: Irgendwie wäre es cool, wenn mehr Browser die Technologien, die man mit Progressive Web Apps assoziiert, implementieren würden. Chrome ist da ja Vorreiter, aber Safari steht auf der Bremse.

Im Übrigen freue ich mich auf den Tag, wo ich Angular-Anwendungen ohne Angular-Module (die ursprünglich gar nicht geplant waren und alles etwas komplizierter machen) entwickeln kann. Dasselbe gilt für Angular-Anwendungen, die ohne Zone.js auskommen.

Du bist natürlich als Angular-Experte bekannt; uns interessiert aber auch immer der Blick über den Tellerrand. Gibt es ein Tech-Thema jenseits von Angular, mit dem du dich 2021 gern beschäftigen möchtest?

Manfred Steyer: Ich habe mir angewöhnt, jedes Jahr zur Horizonterweiterung ein Spaß-Projekt mit Technologien zu machen, die sonst gar nicht in meinem Fokus liegen. In den letzten Jahren waren das z. B. ein Pascal-Compiler, der nach Web Assembly kompiliert oder eine eigene 3D-Engine auf Basis von Canvas2D.

Für 2021 habe ich schon ein paar Ideen, wie Deep Learning mit TensorFlow oder Stream-Processing mit Spark. Eigentlich möchte ich auch mal mein eigenes neuronales Netzwerk von der Pike auf, also ohne irgendwelche Bibliotheken, implementieren.

Vielen Dank für das Gespräch!

Die Fragen stellte Ann-Cathrin Klose.

The post Interview mit Manfred Steyer appeared first on Angular Camp.

]]>