TL;DR Angular wurde endlich veröffentlicht. In diesem Tutorial erfahren Sie, wie Sie Apps mit Angular erstellen und wie Sie eine tokenbasierte Authentifizierung für Angular-Apps richtig hinzufügen. Sehen Sie sich das Codebeispiel aus unserem Github-Repository an.
Angular erreicht endlich den bedeutenden Meilenstein der 2.0-Version die endgültige Version von Angular verfügt nur über wenige Änderungen. In Release Candidate 5 (RC5), nur einige Wochen vor der endgültigen Veröffentlichung verfügbar gemacht, gab es wichtige Änderungen und Ergänzungen, wie @NgModule decorator, Ahead-of-Time (AOT) Compiler und mehr.
Im heutigen Tutorial werden wir einige dieser neuen Funktionen nutzen, um eine komplette Angular-App zu erstellen. Komponenten, @NgModule, Route Guards, Services und mehr sind nur einige der Themen, auf die wir eingehen. Schließlich implementieren wir die tokenbasierte Authentifizierung mit Auth0.
Das Angular-Ökosystem
AngularJS 1.x wurde als robustes Framework für den Aufbau von Single-Page-Applikationen (SPAs, Single Page Applications) angesehen. Seine Fähigkeiten glänzten in einigen Bereichen, in anderen nicht so sehr, aber insgesamt ermöglichte es Entwicklern, leistungsfähige Applikationen zu entwickeln.
Während AngularJS (1.x) ein Framework ist, handelt es sich bei Angular um eine komplette Plattform für die Entwicklung moderner Applikationen. Neben der zentralen Angular-Bibliothek wird die Plattform mit einer leistungsfähigen Befehlszeilenschnittstelle (CLI, Command Line Interface) namens Angular CLI ausgeliefert, die es Entwicklern ermöglicht, ihre Apps einfach zu erweitern und das Build-System zu steuern. Der Angular Platform Server bietet serverseitiges Rendering auf Angular-Applikationen. Angular Material ist die offizielle Implementierung von Google Material Design, mit dem Entwickler hervorragende Applikationen mit Leichtigkeit erstellen können.
“Während AngularJS ein Framework ist, handelt es sich bei @angular um eine komplette Plattform für die Entwicklung moderner Apps.”
Tweet This
Unsere App: Daily Deals
Die App, die wir heute erstellen, wird als Daily Deals bezeichnet. Die App Daily Deals zeigt Angebote und Rabatte für verschiedene Produkte an. Es gibt eine Liste der öffentlich verfügbaren Angebote und eine Liste der privaten Angebote, die nur registrierte Mitglieder sehen können. Die privaten Angebote sind exklusiv für registrierte Mitglieder und sollten hoffentlich besser sein.
Bereitstellung der Daily Deals
Wir müssen die Angebote irgendwoher bekommen. Wir erstellen ein sehr einfaches Node.js-Backend um die Angebote bereitzustellen. Wir haben eine öffentlich zugängliche Route mit öffentlichen Angeboten und eine geschützte Route, die nur von authentifizierten Benutzern aufgerufen werden kann. Für den Moment machen wir beide Routen öffentlich und kümmern uns später um die Authentifizierung. Werfen Sie einen Blick auf unsere Implementierung unten:
'use strict'; // Load dependencies const express = require('express'); const app = express(); const cors = require('cors'); const bodyParser = require('body-parser'); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); app.use(cors()); // Public route app.get('/api/deals/public', (req, res)=>{ let deals = [ // Array of public deals here ]; res.json(deals); }) // Private route app.get('/api/deals/private', (req,res)=>{ let deals = [ // Array of Private Deals here ]; res.json(deals); }) app.listen(3001); console.log('Serving deals on localhost:3001');
Sowohl unser Server als auch die Angular-App, die wir erstellen, erfordern Node.js und NPM. Achten Sie daher darauf, dass diese installiert sind, bevor Sie fortfahren. Sehen Sie sich das Github-Repository an, um unsere Liste von täglichen Angeboten zu erhalten oder erstellen Sie eine eigene. Das Modell für jedes Angebot lautet wie folgt:
{ id: 1234, name: 'Name of Product', description: 'Description of Product', originalPrice: 19.99, // Original price of product salePrice: 9.99 // Sale price of product }
Wenn Sie mit den öffentlichen und privaten Angeboten zufrieden sind, starten Sie den Server, indem Sie den
node server
aktivieren und navigieren Sie zu localhost:3001/api/deals/public
und localhost:3001/api/deals/private
, um sich zu vergewissern, dass die Liste der Angebote, die Sie hinzugefügt haben, vorhanden ist. Als Nächstes richten wir das Angular-Front-End ein.Angular-Front-End-Einrichtung
Eine der besten Möglichkeiten eine neue Angular-App zu erstellen, ist die offizielle Angular-CLI. Die CLI kann sich um das Gerüst der ersten Applikation kümmern, zusätzliche Komponenten hinzufügen, sich um das Build-System kümmern und vieles mehr. In diesem Tutorial wird das Gerüst der ursprünglichen App mit der CLI gebaut.
Wenn sie nicht bereits installiert ist, machen Sie Folgendes:
npm install @angular/cli -g
Dadurch wird Angular CLI global installiert. Mit dem Befehl
ng
interagieren wir mit der CLI. Um eine neue Applikation zu erstellen, wählen Sie ein Verzeichnis und führen Folgendes aus:ng new ng2auth --routing --skip-tests
Dadurch wird eine neue Angular-Applikation mit Routing und keinen anfänglichen Testdateien für die Root-Komponente erstellt. Die App wird in einem eigenen Ordner im aktuellen Verzeichnis erstellt, und die CLI lädt alle erforderlichen NPM-Pakete herunter und legt alles Grundlegende für uns fest.
Sobald
ng new
fertig ist, geben Sie das neue Verzeichnis ein und führen den Befehl ng serve
aus. Das auf WebPack basierte Build-System kümmert sich um die Kompilierung unserer App von TypeScript zu JavaScript und stellt unsere App auf localhost:4200
bereit. Der Befehl ng serve
startet ebenfalls einen Live-Synchronisierungsprozess, sodass bei jeder Änderung unsere App automatisch neu kompiliert wird.Lassen Sie uns jetzt
localhost:4200
ansehen, um sicherzustellen, dass bis jetzt alles so funktioniert, wie es sollte. Wenn Sie die Nachricht "App works!" sehen, haben Sie alles richtig gemacht. Als Nächstes sehen wir uns an, wie unsere Angular-App gebaut wird.Der Befehl
ng new
hat der Angular-App das Gerüst gegeben und zahlreiche Dateien hinzugefügt. Viele dieser Dateien können momentan ignoriert werden, z. B. der e2e-Ordner, der unsere End-to-End-Tests enthalten würde. Öffnen Sie das src-Verzeichnis. Im src-Verzeichnis finden wir einige vertraute Dateien wie index.html, styles.css usw. Öffnen Sie das App-Verzeichnis.Das App-Verzeichnis enthält den Großteil unserer Applikation. Standardmäßig sind folgende Dateien vorhanden:
app.component.css
- enthält die CSS-Stile für unsere Root-Komponente
— enthält die HTML-Ansicht für unsere Root-Komponenteapp.component.html
— enthält die TypeScript-Logik für unsere Root-Komponentenklasseapp.component.ts
— definiert die globalen App-Abhängigkeitenapp.module.ts
— definiert die App-Routenapp-routing.module.ts
Jede Angular-Komponente, die wir schreiben, hat zumindest die Datei
*.component.ts
, die anderen sind optional. Unsere App besteht aus drei Komponenten. Die Haupt- oder Root-Komponente, eine Komponente für die öffentlichen Angebote und eine für die privaten Angebote. Für unsere Root-Komponente setzen wir die Vorlage und die Stile ein. Wir nehmen folgende Änderungen vor und führen die folgenden CLI-Befehle aus:- Löschen Sie die Dateien
undapp.component.css
. Wir definieren alles, was wir für unsere Root-Komponente in der Dateiapp.component.html
benötigen.app.component.ts
- Erstellen Sie eine Public-Deals-Komponente, indem Sie
ausführen. Diese Komponente kümmert sich um das Abrufen und Anzeigen der öffentlichen Angebote.ng g c public-deals --no-spec
- Erstellen Sie eine Private-Deals-Komponente, indem Sie
ausführen. Diese Komponente kümmert sich um das Abrufen und Anzeigen der privaten Angebote.ng g c private-deals --no-spec
- Erstellen Sie die Datei
durch Ausführen voncallback.component.ts
.ng g c callback --it --is --flat --no-spec
- Erstellen Sie eine Angebotsdatei, indem Sie die Datei
. ausführen. In dieser Datei wird unsere Angebotsklasse gespeichert, die Angular die Struktur eines Angebots mitteilt.ng g class deal --no-spec
- Erstellen Sie eine Datei
indem Siedeal.service.ts
. ausführen. Hier fügen wir die Funktionen hinzu und rufen die Angebotsdaten von unserer API ab.ng g s deal --no-spec
Hinweis:
ist ein Shortcut fürg
undgenerate
undc
sind Shortcuts fürs
bzw.component
. Daher entsprichtservice
ng g c
. Das Kennzeichenng generate component
gibt an, dass--no-spec
-Dateien nicht generiert werden sollen.*.spec.ts
und--it
stehen für "inline template" und "inline styles" und--is
gibt an, dass ein enthaltener Ordner nicht erstellt werden soll.--flat
HTTP Client Module hinzufügen
Wir werden HTTP-Anfragen an unsere API in unserer Angular-App stellen. Dazu müssen wir das richtige Modul zur Datei
app.module.ts
hinzufügen. Lassen Sie uns das HttpClientModule
importieren und folgendermaßen dem Import-Array von @NgModule hinzufügen:// app.module.ts ... import { HttpClientModule } from '@angular/common/http'; @NgModule({ declarations: [ ... ], imports: [ ..., HttpClientModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
Bootstrap CSS hinzufügen
Wir verwenden Bootstrap, um unsere App zu gestalten, sodass wir das CSS in
<head>
unserer Datei index.html
folgendermaßen integrieren:<!-- src/index.html --> ... <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"> ...
Erstellen der Root-Komponente
Jede Angular-App muss eine Root-Komponente haben. Wir können sie nennen wie wir wollen, wichtig ist, dass wir eine haben. In unserer App ist die Datei
app.component.ts
unsere Root-Komponente. Werfen wir einen Blick auf die Implementierung dieser Komponente.// app.component.ts import { Component } from '@angular/core'; @Component({ selector: 'app-root', template: ` <div class="container"> <nav class="navbar navbar-default"> <div class="navbar-header"> <a class="navbar-brand" routerLink="/dashboard">{{ title }}</a> </div> <ul class="nav navbar-nav"> <li> <a routerLink="/deals" routerLinkActive="active">Deals</a> </li> <li> <a routerLink="/special" routerLinkActive="active">Private Deals</a> </li> </ul> <ul class="nav navbar-nav navbar-right"> <li> <a>Log In</a> </li> <li> <a>Log Out</a> </li> </ul> </nav> <div class="col-sm-12"> <router-outlet></router-outlet> </div> </div> `, styles: [ `.navbar-right { margin-right: 0px !important}` ] }) export class AppComponent { title = 'Daily Deals'; constructor() {} }
Wir haben unsere Root-Komponente erstellt. Wir haben eine Inline-Vorlage und einige Inline-Stile hinzugefügt. Wir haben noch nicht alle Funktionen hinzugefügt, sodass jeder Benutzer alle Links und die Login- und Logout-Schaltflächen sehen kann. Wir warten mit der Umsetzung noch etwas. Außerdem wird das Element
<router-outlet>
angezeigt. Hier werden unsere gerouteten Komponenten angezeigt.Routing
Da wir unsere App mit dem Kennzeichen
--routing
initialisiert haben, ist die Architektur für das Routing bereits eingerichtet. Jetzt aktualisieren wir es so, dass unsere Deals-Komponente standardmäßig angezeigt wird. Außerdem werden alle für unsere App erforderlichen Routen eingerichtet.Öffnen Sie die Datei
app-routing.module.ts
und fügen Sie Folgendes hinzu:// app-routing.module.ts import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CallbackComponent } from './callback.component'; import { PublicDealsComponent } from './public-deals/public-deals.component'; import { PrivateDealsComponent } from './private-deals/private-deals.component'; const routes: Routes = [ { path: '', redirectTo: 'deals', pathMatch: 'full' }, { path: 'deals', component: PublicDealsComponent }, { path: 'special', component: PrivateDealsComponent }, { path: 'callback', component: CallbackComponent } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
Wir können einfach zu
localhost:4200
im Browser navigieren und die App ansehen. Wir sehen noch nicht viel, nur die obere Navigationsleiste und eine Nachricht, die besagt, dass die Angebots-Komponente funktioniert.Deal Type
Mit TypeScript können wir die Struktur oder den Typ unserer Objekte definieren. Dies hat viele nützliche Zwecke. Zum einen können wir alle Daten eines Objekts über IntelliSense abrufen, wenn wir die Struktur eines Objekts definieren. Wir können unsere Komponenten außerdem noch einfacher testen, indem wir die Datenstruktur oder die Art des Objekts kennen, mit dem wir arbeiten.
Für unsere App erstellen wir einen solchen Typ. In der Datei
deal.ts
definieren wir einen Deal-Typ. Sehen wir uns an, wie wir dies erreichen können.// deal.ts export class Deal { id: number; name: string; description: string; originalPrice: number; salePrice: number; }
Jetzt können wir Objekte in unserer Angular-App als Deal-Typ deklarieren. Diese Objekte erhalten alle Eigenschaften und Methoden des Deal-Typs. Wir definieren hier nur Eigenschaften und werden keine Methoden haben.
Komponenten für Public und Private Deals
Die Komponenten der öffentlichen und privaten Angebote sind ähnlich. Der einzige Unterschied zwischen den beiden Implementierungen besteht darin, dass Angebote der öffentlichen API angezeigt werden, bei der anderen werden Angebote der privaten API angezeigt. Der Kürze halber zeigen wir hier nur eine der Komponenten-Implementierungen. Wir implementieren die
public-deals.component.ts
.// public-deals.component.ts import { Component, OnInit, OnDestroy } from '@angular/core'; import { Subscription } from 'rxjs'; import { Deal } from '../deal'; // We haven't defined these services yet import { AuthService } from '../auth.service'; import { DealService } from '../deal.service'; @Component({ selector: 'app-public-deals', // We'll use an external file for both the CSS styles and HTML view templateUrl: 'public-deals.component.html', styleUrls: ['public-deals.component.css'] }) export class PublicDealsComponent implements OnInit, OnDestroy { dealsSub: Subscription; publicDeals: Deal[]; error: any; // Note: We haven't implemented the Deal or Auth Services yet. constructor( public dealService: DealService, public authService: AuthService ) { } // When this component is loaded, we'll call the dealService and get our public deals. ngOnInit() { this.dealsSub = this.dealService .getPublicDeals() .subscribe( deals => this.publicDeals = deals, err => this.error = err ); } ngOnDestroy() { this.dealsSub.unsubscribe(); } }
Wir verwenden ein RxJS-Abonnement, um das Observable zu abonnieren, das von unserer HTTP-Anforderung erstellt wird (dies muss im Deal Service definiert werden, welches wir in Kürze erstellen). Außerdem ergreifen wir einige Maßnahmen, sobald ein Wert verfügbar ist, um entweder das
publicDeals
-Mitglied festzulegen oder einen Fehler
zu definieren. Wir müssen den Lifecycle-Hook OnDestroy
mit einer ngOnDestroy()
-Methode hinzufügen, der das Abonnement storniert, wenn die Komponente gelöscht wird, um Speicherlecks zu verhindern.Als Nächstes erstellen wir die Ansicht der Public-Deals-Komponente. Dies wird in der Datei
public-deals.component.html
durchgeführt. Unsere Ansicht ist eine Mischung aus HTML- und Angular-Sugar. Werfen wir einen Blick auf unsere Implementierung.<h3 class="text-center">Daily Deals</h3> <!-- We are going to get an array of deals stored in the publicDeals variable. We'll loop over that variable here using the ngFor directive --> <div class="col-sm-4" *ngFor="let deal of publicDeals"> <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title">{{ deal.name }}</h3> </div> <div class="panel-body"> {{ deal.description }} </div> <div class="panel-footer"> <ul class="list-inline"> <li>Original</li> <li class="pull-right">Sale</li> </ul> <ul class="list-inline"> <li><a class="btn btn-danger">${{ deal.originalPrice | number }}</a></li> <li class="pull-right"><a class="btn btn-success" (click)="dealService.purchase(deal)">${{ deal.salePrice | number }}</a></li> </ul> </div> </div> </div> <!-- We are going to use the authService.isLoggedIn method to see if the user is logged in or not. If they are not logged in we'll encourage them to log in, otherwise, if they are authenticated, we'll provide a handy link to private deals. We haven't implemented the authService yet, so don't worry about the functionality just yet --> <div class="col-sm-12" *ngIf="!authService.isLoggedIn"> <div class="jumbotron text-center"> <h2>Get More Deals By Logging In</h2> </div> </div> <div class="col-sm-12" *ngIf="authService.isLoggedIn"> <div class="jumbotron text-center"> <h2>View Private Deals</h2> <a class="btn btn-lg btn-success" routerLink="/special">Private Deals</a> </div> </div> <!-- If an error occurs, we'll show an error message --> <div class="col-sm-12 alert alert-danger" *ngIf="error"> <strong>Oops!</strong> An error occurred fetching data. Please try again. </div>
Abschließend fügen wir einen benutzerdefinierten Stil hinzu. Fügen Sie der Datei
public-deals.component.css
Folgendes hinzu:.panel-body { min-height: 100px; }
Dadurch wird sichergestellt, dass die einzelnen Produkte anständig auf unserer Seite angezeigt werden.
Die Komponente der privaten Angebote sieht ähnlich aus. Wir möchten jedoch auch bedingte Logik hinzufügen, um sicherzustellen, dass private Angebote nur authentifizierten Benutzern zugänglich sind. Wir erstellen diese Logik aber erst später.
Öffnen Sie die Datei
private-deals.component.html
und fügen Sie Folgendes hinzu:<ng-template [ngIf]="authService.isLoggedIn"> <h3 class="text-center">Special (Private) Deals</h3> <div class="col-sm-4" *ngFor="let deal of privateDeals"> <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title">{{ deal.name }}</h3> </div> <div class="panel-body"> {{ deal.description }} </div> <div class="panel-footer"> <ul class="list-inline"> <li>Original</li> <li class="pull-right">Sale</li> </ul> <ul class="list-inline"> <li><a class="btn btn-danger">${{ deal.originalPrice | number }}</a></li> <li class="pull-right"><a class="btn btn-success" (click)="dealService.purchase(deal)">${{ deal.salePrice | number }}</a></li> </ul> </div> </div> </div> </ng-template> <div class="col-sm-12"> <div class="jumbotron text-center"> <h2>View Public Deals</h2> <a class="btn btn-lg btn-success" routerLink="/deals">Public Deals</a> </div> </div> <div class="col-sm-12 alert alert-danger" *ngIf="error"> <strong>Oops!</strong> An error occurred fetching data. Please try again. </div>
Auf Deals API zugreifen
Zuvor haben wir in diesem Tutorial eine sehr einfache API geschrieben, die zwei Routen freigab. Jetzt schreiben wir einen Angular-Service, der mit diesen beiden Endpunkten interagiert. Dies erfolgt in der Datei
deal.service.ts
. Die Implementierung ist wie folgt:// deal.service.ts import { Injectable } from '@angular/core'; import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { throwError, Observable } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { Deal } from './deal'; @Injectable() export class DealService { // Define the routes we are going to interact with private publicDealsUrl = 'http://localhost:3001/api/deals/public'; constructor(private http: HttpClient) { } // Implement a method to get the public deals getPublicDeals() { return this.http .get<Deal[]>(this.publicDealsUrl) .pipe( catchError(this.handleError) ); } // Implement a method to get the private deals getPrivateDeals() { return this.http .get<Deal[]>(this.privateDealsUrl) .pipe( catchError(this.handleError) ); } // Implement a method to handle errors if any private handleError(err: HttpErrorResponse | any) { console.error('An error occurred', err); return throwError(err.message || err); } purchase(item) { alert(`You bought the: ${item.name}`); } }
Jetzt können Sie sehen, wo sich die Methode
getPublicDeals()
in der Datei public-deals.component.ts
befindet. Außerdem haben wir eine Methode getPrivateDeals()
erstellt, die unsere Liste mit privaten Angeboten abruft. Implementieren Sie diese Methode in der Datei private-deals.component.ts
. Abschließend behandeln wir Fehler und implementieren die Methode purchase()
, die in beiden Deal-Komponenten verwendet wird.Nachdem dieser Service erstellt wurde, müssen wir ihn in die Datei
app.module.ts
importieren und wie folgt bereitstellen:// app.module.ts import { DealService } from './deal.service'; ... @NgModule({ ... providers: [ DealService ], ...
Jetzt steht der Service in der gesamten App zur Verfügung.
Authentifizierung zur Angular-App hinzufügen
Navigieren Sie zu
localhost:4200
, wonach Sie automatisch zur Deals-Seite weitergeleitet werden sollten. Beachten Sie, dass Sie auch zur Route /special
navigieren und sich die exklusiven Angebote ansehen können. Dies ist möglich, da wir noch keine Benutzerauthentifizierung hinzugefügt haben. Darum kümmern wir uns jetzt.Die meisten Apps erfordern eine Authentifizierung. Unsere Applikation heute ist nicht anders. Im nächsten Abschnitt zeige ich Ihnen, wie Sie die Authentifizierung für die Angular-Applikation hinzufügen. Wir werden Auth0 als unsere Identitätsplattform verwenden. Wir verwenden Auth0, da dies uns ermöglicht, JSON-Webtoken (JWTs) problemlos auszustellen, aber die abgedeckten Konzepte können auf ein beliebiges tokenbasiertes Authentifizierungssystem angewendet werden. Wenn Sie noch kein Auth0-Konto haben, melden Sie sich jetzt für ein kostenloses Konto an.
Klicken Sie hier auf das APIs-Menüelement und dann auf die Schaltfläche Create API. Sie müssen Ihrer API einen Namen und eine Kennung geben. Der Name kann beliebig sein, also wählen Sie ihn so beschreibend wie möglich. Die Kennung wird zur Identifizierung Ihrer API verwendet. Dieses Feld kann später nicht mehr geändert werden. In unserem Beispiel bezeichne ich die API als Daily Deals API und verwende als Kennung http://localhost:3001. Wir lassen den Anmeldealgorithmus bei RS256 und klicken auf die Create API Schaltfläche.
Im Moment ist das alles, was wir jetzt tun müssen. Lassen Sie uns jetzt mit dieser neu erstellten API unseren Server sichern.
Sicherung des Servers
Bevor wir die Authentifizierung am Front-End in unserer Angular-Applikation implementieren, sichern wir unseren Backend-Server.
Zunächst installieren wir Abhängigkeiten:
npm install express-jwt jwks-rsa --save
Öffnen Sie die Datei
server.js
im Serververzeichnis und nehmen Sie folgende Änderungen vor:// server.js 'use strict'; const express = require('express'); const app = express(); // Import the required dependencies const jwt = require('express-jwt'); const jwks = require('jwks-rsa'); const cors = require('cors'); const bodyParser = require('body-parser'); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); app.use(cors()); // We are going to implement a JWT middleware that will ensure the validity of our token. We'll require each protected route to have a valid access_token sent in the Authorization header const authCheck = jwt({ secret: jwks.expressJwtSecret({ cache: true, rateLimit: true, jwksRequestsPerMinute: 5, jwksUri: "https://{YOUR-AUTH0-DOMAIN}.auth0.com/.well-known/jwks.json" }), // This is the identifier we set when we created the API audience: '{YOUR-API-AUDIENCE-ATTRIBUTE}', issuer: "{YOUR-AUTH0-DOMAIN}", // e.g., you.auth0.com algorithms: ['RS256'] }); app.get('/api/deals/public', (req, res)=>{ let deals = [ // Array of public deals ]; res.json(deals); }) // For the private route, we'll add this authCheck middleware app.get('/api/deals/private', authCheck, (req,res)=>{ let deals = [ // Array of private deals ]; res.json(deals); }) app.listen(3001); console.log('Listening on localhost:3001');
Das ist alles, was wir auf dem Server tun müssen. Starten Sie den Server neu und versuchen Sie, zu
localhost:3001/api/deals/private
zu navigieren. Sie erhalten eine Fehlermeldung mit dem Hinweis, dass der Autorisierungsheader fehlt. Unsere private API-Route ist jetzt gesichert. Jetzt wollen wir die Authentifizierung in unsere Angular-App implementieren.Authentifizierung zum Front-End hinzufügen
Loggen Sie sich in Ihr Auth0-Management-Dashboard ein. Wir wollen jetzt einige Änderungen an unserer App vornehmen. Klicken Sie dazu auf das Applikations-Element in der Seitenleiste. Suchen Sie die Testapplikation, die automatisch erstellt wurde, als wir unsere API erstellt haben. Sie sollte
Daily Deals (Test Application)
oder ähnlich heißen.Ändern Sie den Application Type zu
Single Page Application
. Fügen Sie dann http://localhost:4200/callback
zum Feld Allowed Callback URLs hinzu.Fügen Sie als Nächstes
http://localhost:4200
zum Feld Allowed Logout URLs hinzu.Klicken Sie abschließend auf den Link Advanced Settings im unteren Bereich und wählen Sie den Tab OAuth aus. Stellen Sie sicher, dass der JsonWebToken Signature Algorithm auf
RS256
gesetzt ist.Notieren Sie sich die Client-ID. Wir brauchen sie für die Konfiguration der Authentifizierung der Angular-App.
Auth0.js-Bibliothek
Jetzt müssen wir die
auth0-js
-Bibliothek installieren. Das können wir im Root-Ordner der Angular-App vornehmen:npm install auth0-js --save
Auth0 Environment Config
Öffnen Sie die Datei
src/environments/environment.ts
und fügen Sie der Konstante die Eigenschaft auth
mit folgenden Informationen hinzu:// environment.ts export const environment = { production: false, auth: { clientID: 'YOUR-AUTH0-CLIENT-ID', domain: 'YOUR-AUTH0-DOMAIN', // e.g., you.auth0.com audience: 'YOUR-AUTH0-API-IDENTIFIER', // e.g., http://localhost:3001 redirect: 'http://localhost:4200/callback', scope: 'openid profile email' } };
Diese Datei stellt die Authentifizierungs-Konfigurationsvariablen zur Verfügung, sodass wir Auth0 verwenden können, um unser Front-End zu sichern. Achten Sie darauf,
YOUR-AUTH0-CLIENT-ID
, YOUR-AUTH0-DOMAIN
und YOUR-AUTH0-API-IDENTIFIER
mit Ihren eigenen Informationen der Auth0-Applikation und API-Einstellungen zu aktualisieren.Authentifizierungsdienst
Als Nächstes erstellen wir einen Authentifizierungsdienst, den wir in unserer App verwenden können:
ng g s auth/auth --no-spec
Dadurch wird ein neuer Ordner unter
src/app/auth
mit einer Datei auth.service.ts
darin erstellt.Öffnen Sie diese Datei und ändern Sie sie wie folgt:
// auth.service.ts import { Injectable } from '@angular/core'; import * as auth0 from 'auth0-js'; import { environment } from './../../environments/environment'; import { Router } from '@angular/router'; (window as any).global = window; @Injectable() export class AuthService { // Create Auth0 web auth instance auth0 = new auth0.WebAuth({ clientID: environment.auth.clientID, domain: environment.auth.domain, responseType: 'token', redirectUri: environment.auth.redirect, audience: environment.auth.audience, scope: environment.auth.scope }); // Store authentication data expiresAt: number; userProfile: any; accessToken: string; authenticated: boolean; constructor(private router: Router) { this.getAccessToken(); } login() { // Auth0 authorize request this.auth0.authorize(); } handleLoginCallback() { // When Auth0 hash parsed, get profile this.auth0.parseHash((err, authResult) => { if (authResult && authResult.accessToken) { window.location.hash = ''; this.getUserInfo(authResult); } else if (err) { console.error(`Error: ${err.error}`); } this.router.navigate(['/']); }); } getAccessToken() { this.auth0.checkSession({}, (err, authResult) => { if (authResult && authResult.accessToken) { this.getUserInfo(authResult); } }); } getUserInfo(authResult) { // Use access token to retrieve user's profile and set session this.auth0.client.userInfo(authResult.accessToken, (err, profile) => { if (profile) { this._setSession(authResult, profile); } }); } private _setSession(authResult, profile) { // Save authentication data and update login status subject this.expiresAt = authResult.expiresIn * 1000 + Date.now(); this.accessToken = authResult.accessToken; this.userProfile = profile; this.authenticated = true; } logout() { // Log out of Auth0 session // Ensure that returnTo URL is specified in Auth0 // Application settings for Allowed Logout URLs this.auth0.logout({ returnTo: 'http://localhost:4200', clientID: environment.auth.clientID }); } get isLoggedIn(): boolean { // Check if current date is before token // expiration and user is signed in locally return Date.now() < this.expiresAt && this.authenticated; } }
Nachdem der Authentifizierungsdienst erstellt wurde, müssen wir ihn in die Datei
app.module.ts
importieren und wie folgt bereitstellen:// app.module.ts import { AuthService } from './auth/auth.service'; ... @NgModule({ ... providers: [ ..., AuthService ], ...
Jetzt steht der Service für die gesamte App zur Verfügung.
Wir verwenden die Auth0-Loginpage zur Authentifizierung unserer Benutzer. Dies ist der sicherste Weg, einen Benutzer zu authentifizieren und ein Zugriffstoken auf eine OAuth-kompatible Weise zu erhalten. Nachdem wir jetzt unseren Authentifizierungsservice erstellt haben, geht es mit dem Erstellen des Authentifizierungs-Workflows weiter.
Die Angular-Authentifizierung im Ganzen
Der Angular-Router verfügt über eine leistungsstarke Funktion namens Route Guards, mit der wir programmgesteuert bestimmen können, ob ein Benutzer auf die Route zugreifen kann oder nicht. Route Guards in Angular können z.B. mit der Middleware in Express.js verglichen werden.
Wir erstellen einen Authentifizierungs-Route-Guard, der prüft, ob ein Benutzer eingeloggt ist, bevor die Route angezeigt wird. Erstellen Sie einen neuen Guard, indem Sie den folgenden CLI-Befehl ausführen:
ng g guard auth/auth --no-spec
Öffnen Sie die generierte Datei
auth.guard.ts
und nehmen Sie folgende Änderungen vor:// auth.guard.ts import { Injectable } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; import { Observable } from 'rxjs'; import { AuthService } from './auth.service'; import { Router } from '@angular/router'; @Injectable() export class AuthGuard implements CanActivate { constructor( private authService: AuthService, private router: Router ) {} canActivate( next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean { if (!this.authService.isLoggedIn) { this.router.navigate(['/']); return false; } return true; } }
Um diesen Route Guard in unseren Routen zu implementieren, öffnen wir die Datei
app-routing.module.ts
. Hier fügen wir den Auth-Guard-Service hinzu und aktivieren ihn auf unserer geheimen Route. Lassen Sie uns einen Blick auf die Implementierung werfen.// app-routing.module.ts ... // Import the AuthGuard import { AuthGuard } from './auth/auth.guard'; const routes: Routes = [ ..., { path: 'special', component: PrivateDealsComponent, // Add this to guard this route canActivate: [ AuthGuard ] }, ... ]; @NgModule({ ..., // Add AuthGuard to the providers array providers: [AuthGuard], ... }) export class AppRoutingModule { }
Das ist alles. Unsere Route ist jetzt auf der Routing-Ebene geschützt.
Wenn Sie sich erinnern, haben wir für den
AuthService
einen Stub in unseren Deal-Komponenten eingeschlossen. Da der Authentifizierungsdienst jetzt implementiert ist, funktioniert unsere Platzhalterfunktion. Wir sehen das richtige Verhalten auf der Grundlage des Benutzerzustands.Wir müssen allerdings unsere Root-Komponente aktualisieren, da wir keine authentifizierungsspezifischen Funktionen eingebaut haben. Ich habe das absichtlich so gemacht, damit wir der Reihe nach durch das Beispiel gehen konnten und genau das machen wir jetzt.
// app.component.ts import { Component } from '@angular/core'; import { AuthService } from './auth/auth.service'; @Component({ selector: 'app-root', template: ` <div class="container"> <nav class="navbar navbar-default"> <div class="navbar-header"> <a class="navbar-brand" routerLink="/">{{ title }}</a> </div> <ul class="nav navbar-nav"> <li> <a routerLink="/deals" routerLinkActive="active">Deals</a> </li> <li> <a routerLink="/special" *ngIf="authService.isLoggedIn" routerLinkActive="active">Private Deals</a> </li> </ul> <ul class="nav navbar-nav navbar-right"> <li> <a *ngIf="!authService.isLoggedIn" (click)="authService.login()">Log In</a> </li> <li> <a (click)="authService.logout()" *ngIf="authService.isLoggedIn">Log Out</a> </li> </ul> </nav> <div class="col-sm-12"> <router-outlet></router-outlet> </div> </div> `, styles: [ `.navbar-right { margin-right: 0px !important}` ] }) export class AppComponent { title = 'Daily Deals'; constructor(public authService: AuthService) {} }
Wir haben den
AuthService
importiert und in unserem Konstruktor öffentlich verfügbar gemacht (er muss öffentlich sein, damit die Vorlage die Methoden verwenden kann).Wir haben
*ngIf="authService.isLoggedIn
in unserem Link zu privaten Angeboten hinzugefügt, damit sie nicht wiedergegeben werden, wenn der Benutzer nicht eingeloggt ist. Außerdem haben wir die Logik von *ngIf zu unseren Login- und Logout-Links hinzugefügt, um je nach Authentifizierungsstatus des Benutzers den entsprechenden Link anzuzeigen. Wenn der Benutzer jetzt auf den Anmelde-Link klickt, wird er auf der Auth0-Domain zu einer Login-Page weitergeleitet. Er gibt seine Zugangsdaten hier ein, und wenn sie richtig sind, wird er zurück zur App geleitet.Callback-Komponente
Wir kodieren jetzt die Callback-Komponente, die wir am Anfang des Tutorials erstellt haben. Diese Komponente wird aktiviert, wenn die Route
localhost:4200/callback
aufgerufen wird. Dadurch wird die Umleitung von Auth0 verarbeitet und sichergestellt, dass wir die richtigen Daten im Hash nach erfolgreicher Authentifizierung erhalten haben. Zu diesem Zweck wird die Komponente von dem zuvor erstellten AuthService
verwendet. Werfen wir einen Blick auf die Implementierung:// callback.component.ts import { Component, OnInit } from '@angular/core'; import { AuthService } from './auth/auth.service'; @Component({ selector: 'app-callback', template: ` <p> Loading... </p> `, styles: [] }) export class CallbackComponent implements OnInit { constructor(private authService: AuthService) { } ngOnInit() { this.authService.handleLoginCallback(); } }
Sobald ein Benutzer authentifiziert ist, wird Auth0 ihn zurück zu unserer Anwendung leiten und die Route
/callback
aufrufen. Auth0 fügt außerdem das Zugriffstoken an diese Anforderung an. Die CallbackComponent stellt sicher, dass das Token und das Profil ordnungsgemäß verarbeitet und gespeichert werden. Wenn alles stimmt, d. h., wir haben ein Zugriffstoken erhalten, werden wir zurück zur Homepage geleitet und sind eingeloggt.Deal Service aktualisieren
Es ist ein endgültiges Update erforderlich. Wenn Sie versuchen, auf die Route
/special
zuzugreifen, erhalten Sie nicht die Liste der geheimen Angebote, selbst wenn Sie angemeldet sind. Das liegt daran, dass das Zugriffstoken nicht an das Backend übergeben wird. Wir müssen den Deal Service aktualisieren.Um unser Zugriffstoken zu integrieren, müssen wir den Aufruf für
/api/deals/private
aktualisieren. Wir müssen HttpHeaders importieren, um einen Autorisierungsheader mit dem Bearer-Schema an unsere Anforderung anzuhängen. Außerdem müssen wir unseren AuthService
importieren, um Zugang zum accessToken
zu erhalten. Sehen wir uns an, wie wir dies in unserer Applikation umsetzen.// deal.service.ts ... // Import HttpHeaders import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http'; // Import AuthService import { AuthService } from './auth/auth.service'; @Injectable() export class DealService { ... private privateDealsUrl = 'http://localhost:3001/api/deals/private'; constructor( ..., private authService: AuthService ) { } ... // Implement a method to get the private deals getPrivateDeals() { return this.http .get<Deal[]>(this.privateDealsUrl, { headers: new HttpHeaders().set('Authorization', `Bearer ${this.authService.accessToken}`) }) .pipe( catchError(this.handleError) ); } ... }
Mit dem Token vom Authentifizierungsservice fügen wir einen Autorisierungsheader zur Anfrage
getPrivateDeals()
hinzu. Wenn nun ein Aufruf an die private Route in unserer API erfolgt, wird das authService.accessToken
automatisch an den Aufruf angehängt. Probieren wir es im nächsten Abschnitt aus, um sicherzustellen, dass es funktioniert.Alles zusammensetzen
Das war’s. Jetzt können wir unsere App testen. Wenn Ihr Node.js-Server nicht ausgeführt wird, achten Sie darauf, ihn zuerst zu starten. Gehen Sie zu
localhost:4200
. Sie sollten automatisch auf localhost:4200/deals
umgeleitet werden und die öffentlichen Angebote sehen können.Klicken Sie als nächstes auf den Login-Bildschirm. Sie werden zur Auth0-Domain weitergeleitet, und das Login-Widget wird angezeigt. Loggen Sie sich ein oder melden Sie sich an und Sie werden wieder zur Callback-Route und dann auf die Deals-Seite zurückgeleitet. Jetzt wird die Benutzeroberfläche etwas anders aussehen. Das Hauptmenü hat eine neue Option für Private Deals, und die Nachricht unten zeigt auch einen Link zu den privaten Angeboten. Statt des Login-Links in der Navigationsleiste wird ein Logout-Link angezeigt. Klicken Sie abschließend auf den Private-Deals-Link, um die Liste der exklusiven Privatangebote anzuzeigen.
Hinweis: Da wir
localhost
für unsere Domain verwenden, wird, wenn sich ein Benutzer zum ersten Mal einloggt oder sich der Scope ändert, ein Dialogfeld angezeigt, in dem der Benutzer gefragt wird, ob er Zugriff auf die API gewähren möchte. Dieses Zustimmungsdialogfeld wird nicht angezeigt, wenn Sie keine localhost-Domain verwenden und die App eine First-Party-App ist.Sie haben gerade eine Angular-App erstellt und authentifiziert. Glückwunsch!
Fazit
Angular ist da und bereit. Es hat lange gedauert, aber endlich ist es da und ich könnte nicht aufgeregter sein. In diesem Tutorial haben wir uns einige der Möglichkeiten angesehen, wie Sie Komponenten und Services für Angular schreiben können. Wir haben die tokenbasierte Authentifizierung mit Auth0 implementiert, aber damit Kratzen wir nur an der Oberfläche.
Angular bietet eine Vielzahl von großartigen Funktionen wie Pipes, i18n und vieles mehr. Mit Auth0 können Sie Ihre Angular-Apps nicht nur mit moderner Authentifizierung sichern, sondern auch erweiterte Funktionen wie Multifaktor-Authentifizierung, Anomalieerkennung, Enterprise Federation, Single Sign-On (SSO) und mehr nutzen. Melden Sie sich noch heute an, damit Sie sich ganz auf die Entwicklung von Funktionen konzentrieren können, die es nur in Ihrer App gibt.
About the author
Ado Kukic
Former Auth0 Employee