This post was updated to Angular v6 and Angular CLI 6 in June 2018. We are currently working on a new, updated Angular tutorial to bring the content up to date again. Thank you for your patience!

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

Unsere App: Daily Deals

Daily Deals App

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:4200bereit. 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
  • app.component.html — enthält die HTML-Ansicht für unsere Root-Komponente
  • app.component.ts — enthält die TypeScript-Logik für unsere Root-Komponentenklasse
  • app.module.ts — definiert die globalen App-Abhängigkeiten
  • app-routing.module.ts — definiert die App-Routen

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 app.component.css und app.component.html . Wir definieren alles, was wir für unsere Root-Komponente in der Datei app.component.ts benötigen.
  • Erstellen Sie eine Public-Deals-Komponente, indem Sie ng g c public-deals --no-spec ausführen. Diese Komponente kümmert sich um das Abrufen und Anzeigen der öffentlichen Angebote.
  • Erstellen Sie eine Private-Deals-Komponente, indem Sie ng g c private-deals --no-specausführen. Diese Komponente kümmert sich um das Abrufen und Anzeigen der privaten Angebote.
  • Erstellen Sie die Datei callback.component.ts durch Ausführen von ng g c callback --it --is --flat --no-spec.
  • Erstellen Sie eine Angebotsdatei, indem Sie die Datei ng g class deal --no-spec. ausführen. In dieser Datei wird unsere Angebotsklasse gespeichert, die Angular die Struktur eines Angebots mitteilt.
  • Erstellen Sie eine Datei deal.service.ts indem Sie ng g s deal --no-spec. ausführen. Hier fügen wir die Funktionen hinzu und rufen die Angebotsdaten von unserer API ab.

Hinweis: g ist ein Shortcut für generate und c und s sind Shortcuts für component bzw. service. Daher entspricht ng g c ng generate component. Das Kennzeichen --no-spec gibt an, dass *.spec.ts-Dateien nicht generiert werden sollen. --it und --is stehen für "inline template" und "inline styles" und --flat gibt an, dass ein enthaltener Ordner nicht erstellt werden soll.

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"></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.

Creating Auth0 API

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.

API with No Auth Token

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="/"></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

Auth0 universal login

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.

Daily Deals Authenticated

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.

Consent Dialog

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.

Exclusive Daily Deals

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.