Die Verwaltung von Authentifizierung und Autorisierung ist ein entscheidender Bestandteil jeder modernen Webanwendung. In diesem Abschnitt geht es um die Implementierung eines einfachen Authentifizierungsservices, der in einer Angular-Anwendung verwendet werden kann. Die hier dargestellte Lösung nutzt ein In-Memory Authentifizierungssystem, das jedoch nur zu Testzwecken geeignet ist. In einer echten Produktion sollte ein Server-basierter Authentifizierungsprozess verwendet werden, um sicherzustellen, dass die Sicherheitsstandards eingehalten werden.

Um zu verstehen, wie Authentifizierung und Autorisierung in Angular verwaltet werden, müssen wir uns mit der Implementierung eines Authentifizierungsservices auseinandersetzen, der in der Lage ist, JWTs (JSON Web Tokens) zu setzen, abzurufen und zu löschen. Hierfür wird zunächst eine Service-Klasse erstellt, die grundlegende Methoden zur Handhabung des Tokens bereitstellt.

Der Authentifizierungsservice sollte grundlegende Methoden wie setToken, getToken und clearToken umfassen. Diese Methoden sorgen dafür, dass das JWT im lokalen Speicher des Browsers gespeichert, abgerufen oder gelöscht wird. Die Methode setToken speichert das Token, getToken gibt das gespeicherte Token zurück und clearToken entfernt es, wenn der Benutzer sich abmeldet.

Ein einfaches Beispiel für diese Methoden sieht wie folgt aus:

typescript
protected setToken(jwt: string) {
this.cache.setItem('jwt', jwt);
}
getToken(): string { return this.cache.getItem('jwt') ?? ''; } protected clearToken() {
this.cache.removeItem('jwt');
}

Login und Logout mit Token-Verwaltung

Wenn ein Benutzer sich einloggt, muss das Token gesetzt und gespeichert werden. Ebenso muss bei der Abmeldung das Token gelöscht werden. Ein Login könnte wie folgt aussehen:

typescript
login(email: string, password: string): Observable<any> { this.clearToken(); const loginResponse$ = this.authProvider(email, password) .pipe( map(value => { this.setToken(value.accessToken); const token = decode(value.accessToken); return this.transformJwtToken(token); }),
tap((status) => this.authStatus$.next(status)),
// Weiterer Code für die Verarbeitung der Antwort... ); return loginResponse$; }

Beim Logout kann das Token gelöscht werden, um sicherzustellen, dass der Benutzer nicht mehr authentifiziert ist. Dabei ist es wichtig, dass der Authentifizierungsstatus des Benutzers sofort aktualisiert wird:

typescript
logout(clearToken?: boolean) { if (clearToken) { this.clearToken(); } setTimeout(() => this.authStatus$.next(defaultAuthStatus), 0); }

Sicherheit und Validierung von Tokens

Jeder API-Aufruf, der vom Client gemacht wird, sollte das JWT im Header der Anfrage enthalten. Es ist wichtig, jede API so zu sichern, dass das erhaltene Token überprüft und validiert wird. Zum Beispiel könnte die API, die das Benutzerprofil zurückgibt, zunächst das Token prüfen, um sicherzustellen, dass der Benutzer authentifiziert ist. Ein weiterer Datenbankaufruf ist erforderlich, um zu überprüfen, ob der Benutzer auch autorisiert ist, die angeforderten Daten zu sehen.

Falls ein Benutzer mit einem abgelaufenen Token eine Anfrage stellt, wird eine Antwort mit dem Statuscode 401 (Unauthorized) zurückgegeben. Um eine gute Benutzererfahrung zu gewährleisten, sollte der Benutzer automatisch aufgefordert werden, sich erneut anzumelden, um die Arbeit ohne Datenverlust fortzusetzen.

Eine korrekte Implementierung der Server-seitigen Logik sorgt für die wahre Sicherheit, während die Client-seitige Implementierung in erster Linie darauf abzielt, eine gute Benutzererfahrung zu ermöglichen.

In-Memory AuthService Implementierung

Für Tests und die Entwicklung eines einfachen Authentifizierungsflows kann ein In-Memory AuthService erstellt werden, der keine echte Serverkommunikation erfordert. Ein Beispiel für die Implementierung des InMemoryAuthService in Angular könnte wie folgt aussehen:

  1. Installation einer JWT-Dekodierungsbibliothek und einer fiktiven JWT-Verschlüsselungsbibliothek:

bash
$ npm install fake-jwt-sign
  1. Erweiterung des abstrakten AuthService:

typescript
@Injectable({
providedIn: 'root', }) export class InMemoryAuthService extends AuthService { constructor() { super();
console.warn('Sie verwenden den InMemoryAuthService. Verwenden Sie diesen Dienst nicht in der Produktion.');
}
// Weitere Implementierungen... }
  1. Implementierung eines simulierten Authentifizierungsproviders, der ein gefälschtes JWT zur Authentifizierung erzeugt:

typescript
protected authProvider(email: string, password: string): Observable<IServerAuthResponse> { email = email.toLowerCase(); if (!email.endsWith('@test.com')) { return throwError('Login fehlgeschlagen! Die E-Mail muss mit @test.com enden.'); }
const authStatus = { isAuthenticated: true, userId: this.defaultUser._id, userRole: this.determineRole(email) } as IAuthStatus;
const authResponse = { accessToken: sign(authStatus, 'secret', { expiresIn: '1h', algorithm: 'none' }) }; return of(authResponse); }

Implementierung des Login-Mechanismus

Der Login-Prozess wird vereinfacht, indem eine feste E-Mail-Adresse und ein Passwort verwendet werden. Wenn die Authentifizierung erfolgreich ist, wird der Benutzer in die Manager-Ansicht weitergeleitet. Es ist wichtig, dass der Authentifizierungsdienst korrekt funktioniert, bevor eine detaillierte UI-Komponente entwickelt wird. Die Login-Funktion könnte folgendermaßen aussehen:

typescript
loginAsManager() {
this.authService.login('manager@test.com', 'password') .subscribe(response => {
this.router.navigate(['/manager']);
}); }

Dieser einfache Ansatz ermöglicht es, die Authentifizierungslogik zu testen, ohne sich um die Implementierung einer komplexen Benutzeroberfläche kümmern zu müssen.

Weitere wichtige Aspekte

Der dargestellte Authentifizierungsmechanismus dient nur zu Testzwecken und sollte niemals in einer produktiven Umgebung verwendet werden. In einer realen Anwendung ist es erforderlich, dass der Authentifizierungsdienst mit einem echten Backend kommuniziert, um die Benutzeridentität zu verifizieren und das JWT zu validieren. Die Verwendung einer Bibliothek wie Firebase oder einer benutzerdefinierten API für die Authentifizierung ist entscheidend für die Umsetzung einer sicheren und skalierbaren Lösung.

Es ist ebenso wichtig, dass der Authentifizierungsprozess sicherstellt, dass nicht nur die Authentifizierung, sondern auch die Autorisierung des Benutzers überprüft wird, bevor er Zugriff auf sensible Daten erhält. Das bloße Vorhandensein eines gültigen Tokens garantiert nicht, dass der Benutzer auch die entsprechenden Berechtigungen für den Zugriff auf bestimmte Ressourcen hat.

Wie man Continuous Integration (CI) in der Softwareentwicklung implementiert: Best Practices und Empfehlungen

Die Implementierung von Continuous Integration (CI) ist ein entscheidender Schritt, um sicherzustellen, dass Software in jeder Phase ihrer Entwicklung zuverlässig und fehlerfrei bleibt. Automatisierte Tests und die regelmäßige Ausführung von Build- und Testprozessen sind wesentliche Bestandteile dieser Praxis. Hier wird erläutert, wie man CI in einem Angular-Projekt aufsetzt und dabei bewährte Methoden wie die Verwendung von Cypress für End-to-End-Tests und GitHub Flow berücksichtigt.

Um CI effizient zu nutzen, ist es notwendig, die verschiedenen Werkzeuge und Prozesse zu verstehen, die zur Automatisierung von Tests und Build-Prozessen eingesetzt werden. Ein Beispiel aus der Praxis zeigt, wie man mithilfe von Cypress und CircleCI eine zuverlässige Pipeline für die Softwareentwicklung aufbaut.

Zunächst wird das Testen der Anwendung während der Entwicklung durch den Befehl $ npx ng e2e durchgeführt. Dieser Befehl startet End-to-End-Tests in einem lokalen Entwicklungsumfeld. Für den CI-Prozess wird jedoch eine erweiterte Konfiguration verwendet, die sicherstellt, dass Tests in einer Continuous Integration Umgebung ausgeführt werden. Dies ist besonders wichtig, um zu verhindern, dass fehlerhafte Software in die Produktion gelangt.

Im konkreten Fall eines Projekts wie "local-weather-app" zeigt der Testcode in der Datei cypress/e2e/app.cy.ts wie man Cypress zur Überprüfung der Hauptmerkmale der Anwendung einsetzen kann. Die Prüfung des Titels der Anwendung, wie in folgendem Beispiel:

typescript
it('hat den richtigen Titel', () => {
cy.byTestId('title').should('have.text', 'LocalCast Weather')
})

Dieser Test prüft, ob das Element mit der data-testid="title" den erwarteten Text enthält. Cypress bietet mit dieser Testmethode eine robuste Möglichkeit, Elemente auf der Seite zu finden, selbst wenn deren Position sich verändert. Die Verwendung von data-testid ist eine der besten Methoden, um Tests stabil und wartbar zu gestalten.

Die korrekte Integration von CI in den Entwicklungsworkflow ist von entscheidender Bedeutung, um sicherzustellen, dass Änderungen kontinuierlich überprüft werden, bevor sie in die Produktion gelangen. Die Konfiguration von CI wird durch Dienste wie CircleCI erleichtert, die eine einfache Möglichkeit bieten, Build-Umgebungen zu erstellen und Tests zu automatisieren. CircleCI bietet eine Vielzahl von vorgefertigten Build-Umgebungen, die leicht angepasst werden können. Zudem unterstützt CircleCI Docker-Container, was die Skalierbarkeit und Flexibilität bei der Integration erhöht.

Die grundlegende Einrichtung von CircleCI umfasst folgende Schritte: Zunächst muss ein CircleCI-Konto erstellt und ein neues Projekt hinzugefügt werden. Danach wird eine Konfigurationsdatei (config.yml) erstellt, die den Build- und Testprozess für das Projekt definiert. Ein typischer Abschnitt dieser Datei könnte wie folgt aussehen:

yaml
version: 2.1 orbs: browser-tools: circleci/browser-tools@1 cypress: cypress-io/cypress@3 commands: install: steps: - checkout - restore_cache: keys:
- node_modules-{{ checksum "package-lock.json" }}
- run: npm install - save_cache: key: node_modules-{{ checksum "package-lock.json" }} paths: - node_modules build_and_test: steps: - run: npx ng build --configuration production - run: npx ng test --watch=false --code-coverage

Sobald die CI-Konfiguration abgeschlossen ist, kann die Integration von GitHub Flow helfen, den Codefluss besser zu kontrollieren. GitHub Flow ist ein leichtgewichtiger, branch-basierter Workflow, der besonders für Teams geeignet ist, die regelmäßig Deployments durchführen. Der Workflow umfasst mehrere Schritte:

  1. Branching: Erstellen eines neuen Branches für jede Änderung oder Funktion.

  2. Commits: Mehrere Commits können in diesem Branch vorgenommen werden.

  3. Pull Request: Der Code wird zur Überprüfung und Diskussion gestellt.

  4. Review: Code-Änderungen werden von Teammitgliedern geprüft und gegebenenfalls angepasst.

  5. Deploy: Der Code wird auf einem Test- oder Staging-Server getestet.

  6. Merge: Der Code wird nach erfolgreichem Test in den Hauptbranch integriert.

GitHub Flow stellt sicher, dass alle Änderungen strukturiert und kontrolliert in das Hauptprojekt integriert werden. Indem der Hauptbranch vor direkten Pushes geschützt wird, können Fehler vermieden und die Qualität des Codes sichergestellt werden.

Es ist auch empfehlenswert, Branchenschutzregelungen in GitHub zu aktivieren. Dies umfasst unter anderem die Notwendigkeit, Pull Requests vor dem Mergen zu genehmigen, die Anforderung von Code-Reviews und das Bestehen von CI-Tests, bevor der Code in den Hauptbranch übernommen wird. Solche Regeln helfen, eine hohe Codequalität zu gewährleisten und verhindern, dass fehlerhafte oder unvollständige Funktionen in die Produktion gelangen.

Wichtig ist auch, dass der CI/CD-Prozess flexibel genug bleibt, um mit verschiedenen Teamgrößen und Projekten zu skalieren. CircleCI und GitHub bieten dafür umfassende Dokumentationen und eine Vielzahl an Integrationsmöglichkeiten, die es Teams ermöglichen, ihre eigene CI/CD-Pipeline anzupassen und zu erweitern.

Zusammenfassend lässt sich sagen, dass die Einführung von CI und GitHub Flow einen klaren Vorteil für die Qualitätssicherung und das schnelle, fehlerfreie Ausliefern von Software bietet. Der Schlüssel zum Erfolg liegt dabei in der konsequenten Anwendung von Best Practices und der Automatisierung aller relevanten Schritte, von der Code-Erstellung bis hin zur Bereitstellung in der Produktion.