Datenübertragung von Anfang bis Ende

Streaming: Datenübertragung vom Client zum Server bis zur Datenbank
Keine Kommentare

Streaming ist für Entwickler keine neue Technologie. Zahlreiche Kernmodule wie Node.js basieren auf Datenströmen. Auch clientseitig wird beispielsweise mit der Bibliothek RxJS gearbeitet. Dieses flexible Prinzip kann nicht nur im kleinen Frontend für die Übertragung von Daten vom Server genutzt werden, sondern auch auf gesamter Strecke zwischen Client und Server bis hin zur Datenbank.

Datenströme sind ein Thema, das Entwickler schon sehr lange beschäftigt. Klassische Beispiele sind Pipes. Hier kommen Daten aus einer Quelle, einem beliebigen Kommando, und werden an ein weiteres Kommando weitergereicht. Diese Pipeline kann beliebig lang werden. Jedes Element der Pipeline kümmert sich dabei um einen ganz bestimmten Aspekt. Das Konzept von Datenströmen begegnet uns nicht nur auf Systemebene, sondern hat auch an zahlreichen Stellen in der Webentwicklung Einzug gehalten. Prominentestes Beispiel: Das HTTP-Protokoll. Client und Server haben die Möglichkeit, Nachrichten zu streamen. Dabei können nach einer initialen Anfrage mehrere Pakete, die zur gleichen Nachricht gehören, übertragen werden. Noch einen Schritt weiter geht Node.js. Die Plattform bietet ein Stream-Modul, mit dem sich les- und schreibbare Datenströme implementieren lassen. Dieses Modul bildet die Grundlage für zahlreiche weitere Kernmodule, aber auch Pakete von Drittanbietern. Sie können beispielsweise Daten in eine Datei streamen oder den Inhalt einer Datei über einen Stream verarbeiten.

Clientseitig finden Sie gerade bei den modernen reaktiven Frameworks immer wieder Ansätze von Datenströmen zur Verarbeitung von Informationen. Das offensichtlichste Beispiel ist hier die Bibliothek RxJS. Mit ihr können Sie asynchrone Datenströme modellieren und mithilfe von Operatoren verarbeiten. Diese Bibliothek bildet einen wichtigen Baustein von Angular. Hier sind Teile des Frameworks wie Reactive Forms, der HTTP Client oder der Router mit RxJS umgesetzt und erlauben, dass Sie auf Ereignisse innerhalb der Applikation reagieren. An dieser Stelle stellen sich die folgenden Fragen: Wenn schon ein Großteil der beteiligten Komponenten einer Webapplikation Streaming unterstützt, wie können Sie dieses Prinzip in Ihrer Applikation ausnutzen, was müssen Sie beachten und welche Vor- und Nachteile bietet das Streaming? Mit diesen Aspekten beschäftigen wir uns auf den folgenden Seiten.

Die Idee: Streaming vom Client bis zur Datenbank

Die Hauptaufgabe von Frontend-Bibliotheken und Frameworks wie Angular, React und Vue ist es, Daten zu präsentieren und einem Benutzer Interaktionsmöglichkeiten zu bieten. Sobald sich die Daten ändern, sorgt die Software dafür, dass diese Änderungen möglichst effizient zur Darstellung gebracht werden. Das Ziel ist hierbei eine nahezu verzögerungsfreie Visualisierung. Die Hersteller der Lösungen betreiben großen Aufwand, um ein Maximum an Performance zu erreichen. Dieses Potenzial kann jedoch nicht ausgeschöpft werden, wenn die Änderungen nicht oder nur zeitverzögert ankommen. Also ist die Idee, einen durchgängigen Datenstrom vom Client über den Server bis hin zur Datenbank zu implementieren. Die einzelnen Bausteine für eine solche Umsetzung liegen in Form der verschiedenen Technologien und Bibliotheken bereits seit mehreren Jahren vor, werden jedoch nur selten konsequent von Ende zu Ende eingesetzt.

Eine normale Single-Page-Applikation besteht im Kern aus einem Frontend, einem Backend und einer Datenbank. Die Ausgestaltung dieser Elemente unterscheidet sich natürlich von Applikation zu Applikation und wird von den verwendeten Sprachen und Bibliotheken stark beeinflusst. Im konkreten Fall setzen wir für das Frontend auf React, das Backend wird in Node.js umgesetzt und die Datenbank ist PostgreSQL (Abb. 1).

Abb. 1: Überblick

Abb. 1: Überblick

Das Frontend: Reaktive Frontends mit React

Die meisten Single-Page-Applikationen lassen sich auf ein einfaches und grundlegendes Muster reduzieren. Der Zweck einer solchen Applikation ist es meist, Daten zu erzeugen, sie zu präsentieren, dem Benutzer die Möglichkeit zu bieten, die Daten zu modifizieren und auch wieder zu löschen, in Kürze: CRUD. Moderne Frontend-Lösungen bieten zum Umgang mit dieser Art der Aufgabenstellung eine Vielzahl von Hilfsmitteln. Und so ist es letztendlich eine Frage Ihres persönlichen Geschmacks, auf welche konkrete Implementierung Sie setzen. Der grundlegende Aufbau ähnelt sich. Den Kern des Frontends in unserem Beispiel bildet eine React-Applikation. Diese besteht aus einem Baum von Komponenten. Diese Bausteine stellen die einzelnen Teile der Applikation dar und finden sich in der Regel als sichtbare Elemente der grafischen Oberfläche wieder. Komponenten können von einem einfachen Button bis hin zu einer ganzen Sammlung einzelner Komponenten, wie bei einem Formular, alles sein. Interagiert ein Benutzer über Maus, Tastatur oder Touch mit der Oberfläche, werden Events ausgelöst. Ein wichtiges Merkmal solcher Events ist, dass sie nicht nur einmal, sondern kontinuierlich auftreten können. Sie können im weitesten Sinn die Benutzerinteraktion also als einen kontinuierlichen Event Stream ansehen, mit dem Sie umgehen müssen. Auf die Events reagieren Sie mit Funktionen, Event Handler, die im Kontext der Komponente ausgeführt werden. Der Zustand der Komponente wird in ihrem lokalen State festgehalten. Sobald sich der State ändert, wird die Komponente neu gezeichnet und die Änderungen werden dem Benutzer angezeigt. Dieses Prinzip funktioniert jedoch nicht nur auf der Ebene einzelner Komponenten, sondern auch mit ganzen Komponententeilbäumen in Ihrer Applikation. Hier folgt React einem Prinzip, das als „Lifting State Up“ bezeichnet wird. Teile oder gar der gesamte lokale State einer Komponente wird in eine Elternkomponente verschoben und über Props an die Kindkomponente übergeben. Die Props einer Komponente sind vergleichbar mit den Parametern einer Funktion. Sie können das Verhalten und Aussehen einer Komponente beeinflussen. Das Verschieben des States nach oben im Baum wird erforderlich, wenn mehrere Komponenten auf bestimmte Aspekte des States zugreifen müssen und Sie sicherstellen möchten, dass die Informationen in Ihrer Applikation konsistent sind. Auch bei einer Änderung der Props wird die Komponente neu gezeichnet. Grundsätzlich ist das Frontend einer Single-Page-Applikation so konzipiert, dass es auf den Benutzer reagiert.

Die Problemstellung: Kontinuierliche Datenströme bis zum Server

Ihren reaktiven Charakter kann eine React-Applikation natürlich nicht vollständig leugnen. Es ist jedoch häufig so, dass sich die Arbeit mit den Eingaben eines Benutzers auf die reine Implementierung von Event Handlern zur Manipulation des States beschränkt. An dieser Stelle gibt es deutlich elegantere Lösungsansätze, bei denen der Streamingcharakter des Frontends deutlich besser erhalten bleibt und sogar noch etwas mehr herausgearbeitet werden kann.

International PHP Conference

How Much Framework?

by Arne Blankerts (thePHP.cc)

Building a Cloud-Friendly Application

by Larry Garfield (Platform.sh)

Crafting Maintainable Laravel Applications

by Jason McCreary (Pure Concepts, LLC)

JavaScript Days 2020

Wie ich eine React-Web-App baue

mit Elmar Burke (Blendle) und Hans-Christian Otto (Suora)

Architektur mit JavaScript

mit Golo Roden (the native web)


Die Probleme enden jedoch an dieser Stelle noch nicht, sondern werden eher mehr, je weiter Sie in die Richtung der Serverschnittstelle gehen. In den meisten Fällen kommt HTTP als Kommunikationsprotokoll zwischen Client und Server zum Einsatz. Die Kommunikation erfolgt in einer mehr oder weniger strikten Form von RESTful APIs. Ein entscheidendes Designmerkmal von HTTP ist jedoch sein unidirektionaler Charakter. Die Kommunikation geht vom Client aus, indem dieser beim Server anfragt. Der Server reagiert auf die Anfrage und erzeugt eine Antwort, die der Client verarbeitet. Hier entsteht auch eines der größten Probleme: Der Client muss aktiv nachfragen, um neue Informationen zu erhalten. Ein weiteres Problem ist, dass zwar React grundsätzlich einen reaktiven Ansatz verfolgt, dieser sich allerdings häufig nicht im Quellcode von Frontend-Applikationen fortsetzt.

Die Lösung: zentralisiertes State Management mit Redux und redux-observable

Gerade bei umfangreichen Applikationen, bei denen von mehreren Stellen auf die gleichen Informationen zugegriffen werden muss, bietet sich ein zentrales State Management an. Bibliotheken, die dies ermöglichen, gibt es mittlerweile für alle großen Frameworks. Beispiele sind hier NgRx für Angular und Vuex für Vue. Die populärste Lösung für React trägt den Namen Redux. Mit dieser Bibliothek können Sie den State Ihrer Applikation an einer zentralen Stelle vorhalten. Hier stellt sich jedoch die Frage, wie ein zentrales State Management bei der durchgängigen Umsetzung von Streaming in einer Applikation helfen kann. Der Vorteil einer solchen Lösung ist, dass sich die Datenhaltung von der Anzeige lösen lässt. Das bedeutet, dass Ihre Komponenten wirklich nur noch für die Visualisierung verantwortlich sind. Die Interaktion mit dem Benutzer findet zwar immer noch über Callback-Funktionen statt, diese werden allerdings nicht mehr direkt in den Komponenten implementiert, sondern stammen aus einer Zwischenschicht. In Abbildung 2 sehen Sie eine schematische Darstellung der Architektur, die Redux zugrunde liegt.

Abb. 2: Schematische Darstellung der Architektur, die Redux zugrunde liegt

Abb. 2: Schematische Darstellung der Architektur, die Redux zugrunde liegt

Im Zentrum der Applikation steht ein zentraler Store, dieser übernimmt die Datenhaltung in der Applikation. Auf diesen Store greifen die Komponenten, der View-Layer der Applikation, lesend zu. Dieser Zugriff erfolgt nicht direkt, sondern über sogenannte Containerkomponenten. Eine solche Komponente wird mit Hilfe der connect-Methode von Redux erzeugt. Die Containerkomponenten stellen die Verbindungsstelle zwischen den View-Komponenten und Redux dar und kümmern sich beispielsweise darum, dass die relevanten Teile des States der Komponente als Props zur Verfügung gestellt werden. Ändern sich die betrachteten Teile des Stores, werden auch andere Werte über die Props an die Komponente übergeben und diese wird daraufhin neu gerendert. Schreibende Änderungen werden über Action-Objekte modelliert. Eine solche Action kann sowohl zum Laden von Daten vom Server erzeugt werden als auch zum Erstellen oder Manipulieren von Informationen. Diese Action-Objekte sind einfache JavaScript-Objekte, die die gewünschte Änderung beschreiben. In einer Containerkomponente können Sie neben dem Zugriff auf den State auch auf die dispatch-Methode von Redux zurückgreifen. Diese ermöglicht es, Action-Objekte an den Reducer zu senden. Diese Funktion ermittelt anhand des bisherigen States der Applikation und der Action die neue Version des States. In den meisten Fällen handelt es sich beim Reducer um ein großes Switch-Case-Statement. Bis auf die Tatsache, dass die Action-Objekte im weitesten Sinne auch nicht viel mehr sind als Events, hat Redux noch nicht besonders viel mit klassischem Streaming zu tun. Das macht sich vor allem bemerkbar, da Redux keinerlei Vorgaben zur Serverkommunikation macht. Ein Redux Reducer ist in der Regel frei von Seiteneffekten und zu diesen wird auch die Kommunikation mit einem Server gezählt. Seiteneffekte werden in Redux über separate Middleware-Komponenten abgewickelt. Diese müssen Sie allerdings nicht selbst implementieren, sondern können auf bestehende Lösungen zurückgreifen. Eine dieser Lösungen, redux-observable, arbeitet mit der Bibliothek RxJS, einer Implementierung des Observer-Patterns, das um eine zusätzliche Operatorschicht erweitert wurde (Listing 1).

const updateTodoEpic = (action$: Observable<IAction<Todo>>) =>
  action$.pipe(
    ofType(UPDATE_TODO),
    mergeMap((action: IAction<Todo>) => {
      const fetchPromise = fetch(`/todos/${action.payload.id}`, {
        body: JSON.stringify(action.payload),
        headers: { 'Content-Type': 'application/json' },
        method: 'PUT',
      });
      return from(Promise.all([fetchPromise, dexiePromise])).pipe(
        mergeMap(response =>
          of(action.payload).pipe(map((todo: Todo) => updateTodoSuccessAction(todo))),
        ),
      );
    }),
  );

RxJS wird mittlerweile an vielen Stellen eingesetzt, so nutzt Angular beispielsweise diese Bibliothek an zentralen Stellen wie dem HTTP-Client, und auch serverseitig mit Node.js werden immer mehr Applikationen mit RxJS umgesetzt. RxJS arbeitet asynchron mit einem Strom von Events, die mithilfe der Operatoren verarbeitet werden können. Mit diesem Set-up, also React, Redux und redux-observable, ist das Frontend schon sehr gut auf einen Streamingbetrieb ausgelegt. Noch steht allerdings nur HTTP als Kommunikationsprotokoll zwischen Client und Server zur Verfügung. Hier gibt es allerdings auch wieder mehrere Varianten, um eine flexiblere Kommunikation zwischen beiden Welten zu ermöglichen.

Serverkommunikation: kontinuierliche Kommunikation mit dem Server

Damit die Clients vom Server über Änderungen benachrichtigt werden können, muss der Server in der Lage sein, aktiv Nachrichten an bestimmte oder auch an alle Clients zu senden. Mit HTTP war dies lange Zeit nur über Hacks wie beispielsweise Long Polling möglich. Eine bessere und ressourcenschonendere Lösung ist das WebSocket-Protokoll. WebSocket ist ein eigenständiges Protokoll auf Basis von TCP, es befindet sich also auf der gleichen Ebene wie HTTP und erlaubt Client und Server eine gleichberechtigte Kommunikation. Wie bei HTTP muss die Verbindung auch hier vom Client ausgehen. Dem Ganzen liegt eine HTTP-Anfrage zugrunde, nach einem Handshake wird auf das WebSocket-Protokoll gewechselt. Von da an kann auch der Server dem Client Nachrichten senden. An dieser Stelle können Sie nun entscheiden, ob Sie sämtliche Kommunikation auf WebSockets auslagern möchten oder die Socket-Verbindung nur als Rückkanal für den Server verwenden.

Die WebSocket-Verbindung können Sie als Event-Quelle in Ihre React-Applikation integrieren, indem Sie die WebSockets mit redux-observable verbinden. Je nach Implementierung wird dann nach einer Nachricht vom Server eine neue Action dispatcht und vom Reducer verarbeitet. Für die Implementierung von WebSockets können Sie entweder auf den Standard-WebSocket zurückgreifen, der im Browser als Schnittstelle implementiert ist, oder Sie setzen eine Bibliothek wie Socket.IO ein, die Ihnen zahlreiche Aufgaben abnimmt oder zumindest stark vereinfacht. Solche Bibliotheken haben natürlich den Nachteil, dass sie zusätzlichen Overhead für die Applikation bedeuten.

Eine weitere Alternative zum WebSocket-Protokoll für eine stehende Verbindung zwischen Client und Server sind Server-sent Events. Dieses Feature aus dem Funktionsumfang von HTML5 öffnet einen Kommunikationskanal vom Server zum Client, über den dieser aktiv Nachrichten zum Client senden kann. Für Server-sent Events müssen Sie keine zusätzlichen Pakete installieren, da dieses Feature auf Browserstandards basiert.

Egal, für welche der beiden Lösungen Sie sich entscheiden, sobald Sie Ihre Lösung in Redux integriert haben, kann der Server Ihnen Informationen darstellen, und React behandelt diese Änderungen, als hätte der Benutzer selbst diese Information erzeugt.

Das Backend – serverseitiges Streaming

Mit der Umstellung des Frontends auf WebSockets oder Server-sent Events ist das Frontend mittlerweile in der Lage, aktiv mit dem Server zu kommunizieren. Dies beinhaltet sowohl lesenden als auch schreibenden Zugriff. In einer Multiuserumgebung erhält jeder Benutzer durch die bidirektionale Verbindung mit dem Server auch Aktualisierungen. Diese Aktualisierungen werden an alle Clients übergeben, für die diese Information relevant ist. Serverseitig müssen Sie alle Schnittstellen zur Verfügung stellen, die ein Client benötigt, um arbeiten zu können. Dies beinhaltet sowohl das RESTful API für die gewöhnlichen Schnittstellenaufrufe als auch eine WebSocket-Verbindung. Für die Implementierung eines solchen Backends können Sie natürlich auf diverse Programmiersprachen und Frameworks zurückgreifen. Die meisten der etablierten Lösungen nehmen Ihnen in diesem Bereich sehr viel Arbeit ab, sodass Sie die Entscheidung für eine Backend-Technologie hauptsächlich aus persönlicher Vorliebe heraus treffen können. In unserem Fall wird das Backend, wie auch schon das Frontend, in JavaScript geschrieben werden. Daraus ergibt sich, dass als Basisplattform Node.js zum Einsatz kommt. Auf Node.js aufbauend müssen Sie sich für ein Web Application Framework entscheiden. Auf Nummer sicher gehen Sie, wenn Sie sich für Express entscheiden, ein etabliertes Framework mit großer Verbreitung und Community. Alternativ können Sie auch auf andere Lösungsansätze wie beispielsweise Nest zurückgreifen. Im Gegensatz zu Express ist Nest ein erheblich jüngeres Projekt und verfügt bei Weitem noch nicht über die Verbreitung wie Express. Das aktuelle Wachstum von Nest zeigt jedoch seit mehreren Monaten steil bergauf. Die Basis von Nest bildet nach wie vor Express, allerdings ist diese Schicht durch einen Adapter so abstrahiert, dass Sie Express gegen eine andere Lösung austauschen können. Ein Fall, der häufiger vorkommt, ist, dass Express gegen Fastify ersetzt wird. Dadurch gewinnen Sie nicht nur, wie der Name andeutet, an Geschwindigkeit, sondern auch native Unterstützung asynchroner Callbacks und HTTP/2.

Serverseitig ist der streambasierte Ansatz von HTTP ebenfalls deutlich zu spüren, so haben Sie immer die Möglichkeit, eine Response in mehrere Pakete, die sogenannten Chunks, zu unterteilen und diese getrennt voneinander zu senden. Viel wichtiger ist an dieser Stelle jedoch, dass Nest Sie grundsätzlich beim Routing, also der Formulierung der Endpunkte in Ihrer Applikation, unterstützt. Dies erfolgt in Nest über Dekoratoren, ein JavaScript-Feature, das seit langer Zeit geplant, jedoch nach wie vor nicht stabil im Sprachstandard verankert ist. Wie Angular im Frontend nutzt auch Nest TypeScript als Basissprache. In TypeScript werden Dekoratoren unterstützt, und Sie können sie verwenden, um Klassen, Eigenschaften, Methoden und zahlreiche weitere Konstrukte mit Metainformationen zu versehen (Listing 2).

@Controller('todo')
export class TodoController {
  constructor(private todoService: TodoService) {}

  @Get(':id')
  getTodo(
    @Param('id') id: string,
  ): Promise<Todo> {
    return this.todoService.getTodoById(parseInt(id, 10));
  }
}

Mit der Definition der Endpunkte und der dahinter liegenden Funktionalität haben Sie jedoch nur einen Teil der Funktionalität umgesetzt, die Sie für Ihre Stream-basierte Applikation benötigen. Wie schon im Client müssen Sie auch serverseitig dafür sorgen, dass die Clients mit neuen Informationen versorgt werden können. Nest bietet an dieser Stelle die direkte Unterstützung von WebSockets (Listing 3). Hierfür müssen Sie lediglich ein zusätzliches Paket installieren. Möchten Sie Socket.IO nutzen, benötigen Sie ein weiteres. Anschließend können Sie ein WebSocket-Gateway erzeugen, mit dessen Hilfe Sie auf eingehende Nachrichten zugreifen und Nachrichten an Ihre Clients versenden können. Verwenden Sie Socket.IO, haben Sie die Möglichkeit, die Kommunikation in Ihrer Applikation nicht nur in unterschiedliche Nachrichtentypen, sondern auch in verschiedene Channels zu unterteilen. Dadurch können Sie mit einem einzelnen Client, einer Gruppe von Clients oder allen Clients kommunizieren.

@WebSocketGateway()
export class EventsGateway {
  @WebSocketServer()
  server: Server;

  constructor(private todoService: TodoService) {}

  @SubscribeMessage('complete-todo')
  completeTodo(client: Client, data: Todo): Observable<WsResponse<boolean>> {
    return this.todoService.completeTodo(data);
  }
}

Die Datenbank – Daten in die und aus der Datenbank streamen

Den letzten Schritt bei der Implementierung unserer Applikation stellt die Datenbank dar. Üblicherweise binden Sie eine beliebige Datenbank im Backend, also in unserem Fall in Nest, an und senden Ihre Querys dorthin. In der Webentwicklung kommen die unterschiedlichsten Datenbanken zum Einsatz. Dabei wird meist zwischen relationalen und NoSQL-Datenbanken unterschieden. Unter Node gibt es für nahezu jede Datenbank einen Treiber, sodass Sie selbst exotische Datenbanken in Ihre Applikation einbinden können. Für unser Beispiel nutzen wir mit PostgreSQL allerdings alles andere als einen Exoten. Diese Datenbank zählt zu einer der am weitesten verbreiteten SQL-Datenbanken im Web. Damit Sie Ihre SQL-Querys nicht selbst von Hand verfassen müssen, empfehlen die Macher von Nest den Einsatz einer Abstraktionsschicht in Form von TypeORM. Mit dieser Bibliothek können Sie über Objektstrukturen mit Ihrer Datenbank interagieren, ohne dass Sie ein Wort SQL schreiben müssen. So mächtig eine solche Abstraktionsschicht auch sein kann, es besteht trotzdem für Sie die Möglichkeit, direkt SQL zu schreiben und dieses an die Datenbank zu schicken. Sie haben also eine komfortable Schnittstelle und für den Notfall trotzdem die volle Flexibilität, die Ihnen die Datenbank über ihre Abfragesprache zur Verfügung stellt.

An dieser Stelle haben wir die Standardfunktionalität vieler Webapplikationen implementiert. Zugegebenermaßen funktioniert der Zugriff auf die Datenbank durch TypeORM recht komfortabel, aber von Streaming kann an dieser Stelle keine Rede sein. Viele Datenbanktreiber für Node bieten Streaming-APIs an. So auch node-postgres. Mit dem zusätzlichen Paket pg-query-stream können Sie Daten aus einer PostgreSQL-Datenbank in Form eines Objekstreams auslesen.

const query = new QueryStream('SELECT id, title, status FROM todo');
const stream = client.query(query);
stream.pipe(JSONStream.stringify()).pipe(client);

Der umgekehrte Weg, also das Auslesen von Informationen aus der Datenbank, funktioniert über das pg-copy-streams-Paket. Nachdem Ihr Server die Hoheit über die Daten besitzt, können Sie beim Einfügen neuer Daten beziehungsweise beim Ändern der Daten die entsprechenden WebSocket-Channels oder über Server-sent Events die Clients benachrichtigen. Falls die Datenbank jedoch nicht exklusiv Ihrer Applikation vorbehalten ist, gibt es außerdem die Möglichkeit, dass Sie sich einen Datenbanktrigger implementieren, den Sie mit Ihrer Nest-Applikation verbinden, und in der hier implementierten Routine die betroffenen Clients benachrichtigen.

Authentifizierung

Aller Dynamik zum Trotz müssen Sie dafür sorgen, dass nicht jeder Benutzer auf alle Daten zugreifen kann. Häufig kommt bei einer Webapplikation an dieser Stelle mittlerweile eine tokenbasierte Authentifizierungsstrategie zum Einsatz. Mit der konkreten Umsetzung in Form von JSON Web Tokens (JWT) kann ein Client sich bei einer Anfrage über das Übersenden eines Tokens beim Server authentifizieren. Dieser entscheidet dann, ob der Client berechtigt ist, auf die angefragte Information zuzugreifen. Clientseitig müssen Sie an dieser Stelle kaum etwas beachten, außer das Token bei Anfragen im Authorization-Header mitzusenden. Serverseitig greift Nest für die Authentifizierung auf Passport.js, eine seit einigen Jahren etablierte Bibliothek, zurück. Passport.js unterstützt eine große Zahl verschiedener Strategien, unter ihnen auch JWT. Die Absicherung von Endpunkten erfolgt über Guards. Diese können Sie mithilfe von Dekoratoren einfügen und müssen die Konfiguration nur einmal an zentraler Stelle vornehmen. Für die Absicherung der WebSocket-Kommunikation können Sie ebenfalls auf Guards zurückgreifen.

@Post()
@UseGuards(AuthGuard('jwt'))
createTodo(@Body() todo: Todo): Promise<Todo> {
  return this.todoService.createTodo(body);
}

Ein unbeschränkt gültiges Token stellt ein Sicherheitsrisiko dar, da es einem Angreifer möglich ist, sich als ein regulärer Benutzer auszugeben, falls er in den Besitz eines Tokens kommt. Um diesem Risiko einen Riegel vorzuschieben, wird meist mit kurzlebigen Access Tokens gearbeitet. Ein Benutzer verfügt über ein Access Token, das er für den Zugriff verwendet und das nach kurzer Zeit, beispielsweise fünf Minuten, ausläuft und ein länger gültiges Refresh Token, mit dem er sich nach Ablauf des Access Tokens ein neues Access Token ausstellen lassen kann. Für die Umsetzung dieser Strategie in Client und Server existieren zahlreiche Beispiele, sodass Sie auch hier keine Probleme haben sollten.

Sicherheit

Neben der Authentifizierung ist es unumgänglich, die Verbindungen zwischen den einzelnen beteiligten Systemen zu verschlüsseln, um einem Angreifer die Möglichkeit zu nehmen, die Kommunikation mitzulesen. Sowohl für HTTP als auch für WebSocket existieren eine unverschlüsselte und eine verschlüsselte Variante. Für den Entwicklungsbetrieb ist die unverschlüsselte Variante noch in Ordnung, sobald Sie Ihre Applikation jedoch auf das Produktivsystem deployen, müssen Sie sicherstellen, dass die Kommunikation verschlüsselt erfolgt. Auch hier haben Sie wiederum mehrere Möglichkeiten. Sie können die Kommunikation direkt in Ihrer Applikation verschlüsseln (Listing 4). In diesem Fall benötigt Ihre Nest-Applikation ein SSL-Zertifikat und den dazugehörigen Schlüssel. Das Nest zugrunde liegende Framework, also beispielsweise Express oder Fastify, unterstützt HTTPS.

const options = {
  http2: true,
  https: {
    key: readFileSync(join(__dirname, '..', 'cert', 'fastify.key')),
    cert: readFileSync(join(__dirname, '..', 'cert', 'fastify.cert')),
  },
};

async function bootstrap() {
  const app = await NestFactory.create(
    AppModule,
    new FastifyAdapter(fastify(options)),
  );

  await app.listen(3000);
}
bootstrap();

Alternativ dazu können Sie die Applikation auch hinter einem Gateway betreiben. In diesem Fall kann sich das Gateway um die SSL-Terminierung kümmern und Ihre Applikation spricht unverschlüsseltes HTTP. Die Kommunikation der Clients erfolgt nur mit dem Gateway und dann über eine von der Außenwelt abgeschirmte Verbindung zwischen Gateway und Nest-Applikation. Beide Varianten gelten nicht nur für HTTP, sondern auch für WebSockets. Die Verschlüsselung wird im URL in der Protokollversion durch ein angehängtes „s“ gekennzeichnet, also https:// und wss://.

Zusammenfassung

Datenströme gibt es in der Entwicklung schon seit vielen Jahren. So basieren zahlreiche Kernmodule von Node.js auf diesem Prinzip. Aber auch clientseitig wird häufig mit RxJS gearbeitet. Diese Bibliothek ist ein gutes Beispiel für die Vorteile von Streams. Die Ereignisse, die auf einem solchen Stream auftreten können, sind asynchron. Im Gegensatz zu Promises, die einen begrenzten Vorrat von Zuständen aufweisen, können in einem Stream beliebig viele Ereignisse über den Zeitverlauf auftreten.

Ein weiteres Merkmal von Streams ist, dass sie sehr flexibel sind. Sie können die Ereignisse eines Streams zwischen der Quelle und dem Ziel modifizieren. Auch hierfür ist RxJS ein gutes Beispiel. Die Arbeit mit den Ereignissen erfolgt hier in Form der Operatoren. Hierbei handelt es sich um Funktionen, die einen bestimmten Zweck haben und die Sie flexibel kombinieren können. Dieses Prinzip können Sie jedoch nicht nur im Kleinen im Frontend für das Transformieren von Eingaben oder Daten vom Server einsetzen, sondern auf der gesamten Strecke zwischen Client und Server bis hin zur Datenbank.

In Webapplikationen kommen zwar an einigen Stellen Streams zum Einsatz, diese werden jedoch spätestens an den Systemgrenzen wieder unterbrochen. Das muss jedoch nicht sein. Gerade Protokolle wie WebSockets erlauben es, den Datenstrom ohne Unterbrechung bis zum Server zu leiten. Sie können den Datenstrom sogar bis zur Datenbank weiterführen. Viele Datenbanktreiber bieten Streamingschnittstellen, bei denen Sie über Readable- und Writable-Streams mit der Datenbank interagieren können.

Die Umsetzung von Streams durch die gesamte Applikation erfordert ein Umdenken in der Implementierung und eine Abkehr von den traditionellen Entwurfsmustern. Dies macht sich jedoch bezahlt, wenn es an die Erweiterung Ihrer Applikation geht. Neue Features sind dann in den meisten Fällen nur noch zusätzliche Methoden, die Sie in die Streams Ihrer Applikation integrieren.

PHP Magazin

Entwickler MagazinDieser Artikel ist im PHP Magazin erschienen. Das PHP Magazin deckt ein breites Spektrum an Themen ab, die für die erfolgreiche Webentwicklung unerlässlich sind.

Natürlich können Sie das PHP Magazin über den entwickler.kiosk auch digital im Browser oder auf Ihren Android- und iOS-Devices lesen. In unserem Kiosk ist das PHP Magazin weiterhin im Print-Abonnement erhältlich.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu:
X
- Gib Deinen Standort ein -
- or -