Erhältlich ab: März 2017
In einer vereinfachten Sicht umfasst eine IoT-Architektur drei Komponenten: Sensoren, die Daten erfassen, ein Gateway, das die Daten empfängt und für die Weiterleitung vorbereitet sowie eine zentrale Instanz, in der Daten eingehend analysiert und für Steuerungsaufgaben aufbereitet werden. Während sich der erste Teil dieser Miniserie schwerpunktmäßig mit den Sensoren befasste, geht es im zweiten Teil um die Abläufe und die inhaltliche Logik im Gateway.
Dem Bausteinprinzip folgend, besteht ein Smart Gateway in Form eines Raspberry Pi aus den folgenden drei Elementen: Raspbian [1] als Betriebssystem, Docker-Containern, um die verschiedenen, auf dem Gateway ablaufenden Funktionalitäten voneinander zu isolieren, und Red Hat JBoss Fuse als Komponente des Gateways, die den MQTT-Broker für die Sensoren bereitstellt. Sie wandelt darüber hinaus die Nachrichten von einem nativen in ein generisches XML-Format um und bietet ein Content-based Routing, das die Nachrichten an die vorgesehenen zentralen Instanzen weiterleitet – seien es ein On-Premise-Rechenzentrum oder ein Cloud-Anbieter. Während es im ersten Teil der Serie um die Konfiguration des Smart Gateways ging, stehen diesmal die Implementierungsdetails im Vordergrund. Dazu gehören der Empfang von CSV-basierten Nachrichten via MQTT, die Umwandlung der Daten vom CSV- ins XML-Format und deren Erweiterung um Metadaten aus dem MQTT Topic. Ein MQTT Topic bündelt Nachrichten zu einem bestimmten Thema. Das können beispielsweise die durch einen Sensor gemessenen Temperaturen oder andere thermodynamische und mechanische Größen sein. Zur weiteren detaillierten Analyse werden die aufbereiteten Nachrichten via JMS an ein Rechenzentrum vor Ort oder in der Cloud weitergeleitet (Abb. 1).
Die inhaltliche Logik des Smart Gatways lässt sich mit einer Apache Camel Route [2] in Spring XML erstellen. Apache Camel ist ein leichtgewichtiges Open-Source-Integrationsframework, das es ermöglicht, Systeme mit verschiedenen Schnittstellentechnologien und -formaten miteinander zu verknüpfen. Apache Camel übernimmt die Konvertierung der Nachrichten und sorgt für deren Routing zwischen den Systemen. Um die Nachrichten zu transformieren, anzureichern oder neu anzuordnen, stehen unterschiedliche Werkzeuge bereit. Apache Camel ermöglicht eine regelbasierte Verteilung und Zustellung der Nachrichten an die Zielsysteme.
Im Fall unseres Smart Gateways gibt es mehrere Möglichkeiten: Entweder man lässt die Camel Route als eigenständige Java-Applikation laufen oder man implementiert sie im Rahmen von Red Hat JBoss Fuse. Dabei handelt es sich um die von Red Hat unterstützte Enterprise-Version von Apache Camel. Außerdem kann die Camel Route auf der Red Hat JBoss Enterprise Application Platform implementiert werden. Der komplette Sourcecode steht in meinem GitHub Repository [3] zur Verfügung.
Die Camel Route setzt die Anforderungen des Gateways um und ist recht einfach gehalten:
<route>
<from uri="mqtt:mqtt.temp.receiver?host=tcp://localhost:1883& amp;subscribeTopicNames=iotdemo/#/#&amp;userName=admin&amp;password=change12_me"/>
<bean ref="myHelper" method="enhanceMessage" bean-Type="com.redhat.demo.smart_gateway.MyHelper"/>
<unmarshal ref="bindyDataFormat"/>
<convertBodyTo type="java.lang.String"/>
<to uri="activemqDatacener:queue:message.to.rules_cep"/>
</route>
Eine kurze Analyse der Codefragmente veranschaulicht die Arbeitsweise. Zunächst zu dieser Zeile:
<from uri="mqtt:mqtt.temp.receiver?host=tcp://localhost:1883&amp;subscribeTopicNa-mes=iotdemo/#/#&amp;userName=admin&amp;password=change12_me"/>
Der Code verwendet die MQTT-Komponente [4] von Apache Camel und wartet darauf, ob neue Nachrichten zum Topic iotdemo/#/# vorliegen. Die beiden #-Zeichen sind Wildcards, mit denen das Gateway Nachrichten zu allen Topics empfangen kann, die dieser Namenskonvention entsprechen. Das erste # steht für den Sensortyp, z. B. Temperatur oder Luftfeuchtigkeit, das zweite für die Unique ID des Sensors oder Geräts.
<bean ref="myHelper" method="enhanceMessage" bean-Type="com.redhat.demo.smart_gateway.MyHelper"/>
Die empfangene Nachricht und all ihre Metadaten werden zur Verarbeitung an eine benutzerdefinierte JavaBean weitergeleitet. Sie nimmt den CSV-Wert aus der MQTT-Nachricht entgegen und erweitert ihn um Details aus der Definition des MQTT-Topics, über den die Daten gesammelt wurden. Die JavaBean selbst wird später noch einmal Gegenstand einer weiteren Betrachtung sein.
<unmarshal ref="bindyDataFormat"/>
<convertBodyTo ty-pe="java.lang.String"/>
Hat die JavaBean den CSV-Wert um alle relevanten Informationen angereichert, kommt die Bindy-Komponente von Apache Camel zum Zug, um den CSV-Wert in das XML-Format zu transformieren [5].
<convertBodyTo type="java.lang.String"/>
Um von Anfang an möglichst flexibel zu sein, bietet es sich an, die XML-Nachricht in einen String umzuwandeln:
<to uri="activemqDatacener:queue:message.to.rules_cep"/>
Im letzten Schritt der Camel Route wird die Nachricht an den Message Broker weitergeleitet, der sich auf einem Remotesystem im Rechenzentrum oder in der Cloud befindet.
Wer Programmcode erstellt, weiß, dass es immer viele Möglichkeiten gibt, um ein bestimmtes Ziel zu erreichen. Um den Payload einer Apache-Camel-Nachricht um Metadaten anzureichern, kommt in der Camel Route eine JavaBean zum Einsatz. Der wichtigste Teil davon ist folgender:
public class MyHelper {
@Handler
public String enhanceMessage( String body, Exchange exchange ) {String res = null;
res = addDeviceID(body, exchange);
res = addDeviceType(res, exchange);
res = appendTimestamp(res, exchange);
return res;
}
Alle Member-Funktionen, die mit @Handler annotiert sind, können in einer Camel Route verwendet werden. Apache Camel ruft diese Handler auf, übergibt Body und Exchange der Nachricht und hält diese Informationen während des gesamten Message Routings vorrätig. Diese benutzerdefinierte Funktion lässt sich flexibel einsetzen, Grenzen werden nur durch Java gesetzt.
Zusammenfassend ist das Smart Gateway in der Lage, Nachrichten zu empfangen und weiterzuleiten (Abb. 2). Das Einzige, was dann noch zu tun bleibt, ist die Sensoren so zu platzieren, dass sie auch tatsächlich etwas messen können. Auch wenn es sich insgesamt um ein einfaches Set-up handelt, verdeutlicht das vorgestellte Beispiel anschaulich die Idee und das Konzept, wie sich ein Smart Gateway implementieren lässt.
Patrick Steiner ist seit mehr als sechzehn Jahren in der IT-Branche tätig. Den größten Teil seiner professionellen Laufbahn hat er für IBM Deutschland gearbeitet, davon die letzten zehn Jahre als Senior-IT-Architekt im Industrial Sector. Seit Mai 2013 ist er als Solution Architect für Red Hat Deutschland tätig.
[1] Raspbian: https://www.raspbian.org
[2] Apache Camel: http://camel.apache.org
[3] IoT-Demo: https://github.com/PatrickSteiner/IoT_Demo_Gateway
[4] MQTT-Komponente: http://camel.apache.org/mqtt.html
[5] Bindy-Komponente: http://camel.apache.org/bindy.html
Mit den Standards OAuth 2.0 und OpenID Connect lassen sich flexible Authentifizierungs- und Autorisierungslösungen auf der Basis von Security-Tokens entwickeln. Sie erlauben die Integration bestehender Identity-Lösungen wie Active Directory sowie die Nutzung zentraler Benutzerkonten für unterschiedliche Anwendungen.
Die wenigsten Geschäftsanwendungen kommen ohne Authentifizierung aus. Häufig müssen bestehende Identity-Lösungen wie Active Directory oder LDAP-Systeme integriert werden, um Single Sign-on zu ermöglichen. In modernen Webanwendungen muss der Client auch das Recht erhalten, im Namen des angemeldeten Benutzers auf Services zuzugreifen. All diese Anforderungen lassen sich elegant mit Security-Tokens lösen. Dieser Artikel zeigt anhand eines Beispiels, wie tokenbasierte Sicherheit in einer Angular-Anwendung genutzt werden kann. Dazu kommen neben einer Angular-Anwendung ein auf Spring Boot basierendes Web-API und die zertifizierte Identity-Lösung Keycloak [1] aus der Feder von Red Hat zum Einsatz. Der gesamte Quellcode findet sich unter [2] und [3].
Wer sich heutzutage mit tokenbasierter Sicherheit beschäftigt, kommt wohl kaum an den beiden populären Standards OAuth 2.0 [4] und OpenID Connect [5] vorbei. Sie beschreiben unter anderem, wie sich ein Benutzer bei einem verteilten System anmelden kann und wie ein Client das Recht erhält, im Namen des Benutzers Services zu konsumieren. Dazu kommt, dass diese Standards direkt auf HTTPS aufsetzen und sich somit wunderbar für leichtgewichtige Web-APIs eignen. Abbildung 1 verdeutlicht die Funktionsweise von OAuth 2.0 aus der Vogelperspektive. Der Client leitet den Benutzer zur Anmeldung zu einem so genannten Authorization-Server weiter. Diese Instanz hat Zugriff auf zentrale Benutzerkonten. Hat sich der Benutzer dort angemeldet, erhält der Client ein so genanntes Access-Token, das ihm im Namen des Benutzers Zugriff auf Services im Backend gibt, so genannte Resource-Server.
Ein Access-Token informiert den Resource-Server unter anderem über den entsprechenden Benutzer sowie über die Rechte, die der Client im Namen des Benutzers wahrnehmen darf. Zusätzlich finden sich im Token meist auch Metadaten, wie der Aussteller, das Ausstellungsdatum oder die Gültigkeitsdauer. Diese vom Prinzip her einfache Vorgehensweise hat mehrere Vorteile. Jeder Benutzer kann ein zentrales Benutzerkonto für verschiedene Clients und Services nutzen. Da die Anmeldung beim Authorization-Server erfolgt, erhält der Client das Passwort nicht. Die Authentifizierung ist vom Client entkoppelt und lässt sich somit in bestehende Identity-Lösungen integrieren. Tokens erhöhen außerdem die Flexibilität. Beispielsweise könnte ein Service das Token an einen weiteren Service weiterreichen, um zu beweisen, dass er im Namen des Benutzers agiert. Zum Zugriff auf andere Sicherheitsdomänen kann der Service das Token auch gegen eines für diese Domäne tauschen. Die Lösung kommt ohne Cookies aus. Somit kann der Client auch auf Services zugreifen, die auf anderen Servern laufen oder eine andere Origin haben. Zusätzlich schränkt der Verzicht auf Cookies bestimmte Angriffe ein.
Das Format des Access-Tokens sowie die Maßnahmen, die der Resource-Server zum Validieren des Tokens unternimmt, sind von OAuth 2.0 nicht näher beschriebene Implementierungsdetails. Häufig kommen digitale Signaturen zum Einsatz, damit der Resource-Server einfach prüfen kann, ob das Token von einem vertrauenswürdigen Authorization-Server stammt. Alternativ dazu könnte das Token auch nur aus einer nicht vorhersehbaren ID bestehen, mit welcher der Resource-Server sich erneut an den Authorization-Server wendet.
Als Ergänzung zu OAuth 2.0 definiert OpenID Connect (OIDC) unter anderem, wie der Client Informationen über den Benutzer bekommen kann. Diesen Aspekt deckt OAuth 2.0 nicht ab. Selbst das ausgestellte Token muss für den Client nicht lesbar sein. Dazu spezifiziert OIDC unter anderem ein so genanntes ID-Token, das der Client zusätzlich zum Access-Token erhalten kann. Während das Access-Token zum Zugriff auf das Backend bestimmt ist, kann der Client aus dem ID-Token direkt Informationen über den Benutzer entnehmen (Abb. 2).
Im Gegensatz zu Access-Tokens bei OAuth 2.0 ist der Aufbau von ID-Tokens vorgegeben. Es handelt sich dabei immer um ein JSON Web Token (JWT), das signiert und verschlüsselt sein kann. Zusätzlich definiert OIDC einen User-Info-Endpunkt. Dabei handelt es sich um einen Service, der dem Client weitere Informationen zum aktuellen Benutzer verrät, sofern er das erhaltene Access-Token vorweisen kann.
Bei den von OpenID Connect verwendeten JWTs handelt es sich um einen offenen Standard der IETF [6], der ein kompaktes und in sich geschlossenes Containerformat zur sicheren Übertragung von Daten beschreibt. JSON-Objekte können dabei durch kryptografische Verfahren und gängige Algorithmen digital signiert [7] und bei Bedarf auch verschlüsselt werden [8]. Während JWTs vielfältige Einsatzzwecke abdecken können, werden sie häufig im Kontext von Authentifizerung und Authorisierung zum Informationsaustausch entitätsbezogener Daten verwendet. Ein signiertes und unverschlüsseltes JWT besteht aus den folgenden drei Teilen:
Header: ein JSON-Objekt, das zumindest den Typ des Tokens und den verwendeten Algorithmus zum Signieren beinhaltet sollte.
Payload: ein JSON-Objekt mit so genannten Claims, die Fakten über eine Entität (zumeist Benutzer) sowie zusätzliche Metadaten enthalten. Unterschieden werden dabei reservierte, öffentliche und private Claims.
Signature: eine digitale Signatur, die mit dem im Header angeführten Algorithmus über die Vereinigung von Header und Payload berechnet wird, basierend auf einem zuvor geteilten Secret oder privaten Schlüssen.
Header, Payload und Signature werden jeweils Base64-Safe-URL-codiert und mit einem Punkt getrennt konkateniert. Zur Illustration des beschriebenen Aufbaus dient ein beispielhaftes JWT in Listing 1.
Listing 1: Beispielhaftes JSON Web Token
HEADER
{"alg": "HS256","typ": "JWT"}
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
PAYLOAD
{"sub": "ABCDEF-123456","aud": "JavaMagazinDemo","iss": "https://..."}
eyJzdWIiOiJBQkNERUYtMTIzNDU2IiwiYXVkIjoiSmF2YU1hZ2F6aW5EZW1vIiwiaXNzIjoiaHR0cHM6Ly8uLi4ifQ
SIGNATURE (Secret = secret)
6VNJFCG-n8CCCgy6fm4iwuo-pYY3944kRAnKz5b1bzo
JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJBQkNERUYtMTIzNDU2IiwiYXVkIjoiSmF2YU1hZ2F6aW5EZW1vIiwiaXNzIjoiaHR0cHM6Ly8uLi4ifQ.6VNJFCG-n8CCCgy6fm4iwuo-pYY3944kRAnKz5b1bzo
Unter [9] können JWTs für Testzwecke anschaulich decodiert, verifiziert und auch generiert werden. Weiter wird eine Browserintegration in Form eines Chrome Plug-ins zur komfortablen Handhabung von JWTs im Zuge des Debuggings angeboten.
Für verschiedene Anwendungsfälle definieren OAuth 2.0 und OIDC so genannte Flows, die festlegen, welche Nachrichten auszutauschen sind, damit der Client die erwähnten Tokens erhält. Für Single Page Applications kommen zwei Flows in Frage: der Implicit Flow und der Resource Owner Password Credentials Flow. Der Implicit Code Flow (Abb. 3) ist für Clients geschaffen, die nicht in der Lage sind, ein geteiltes Secret sicher zu verwahren, wie alle JavaScript-Browseranwendungen. Wie mit der eingangs präsentierten Übersicht gezeigt, sendet der Client eine Autorisierungsanfrage an den Authorization-Server, der auf einer eigenen, von ihm kontrollierten Seite den Benutzer zur Authentifizierung auffordert. Nach Eingabe der Zugangsdaten kann ein optionaler Zwischenschritt erfolgen, bei dem der Benutzer seine explizite Zustimmung erteilt, die vom Client angeforderten Rechte an ihn zu übertragen. Danach erfolgt eine Umleitung zurück zum Client, im Zuge derer auch ein ID-Token (Benutzerinformation) und ein optionales Access-Token (Zugangsrechte) übermittelt werden. Der Client prüft das ID-Token (JWT) auf Plausibilität und Gültigkeit. Mit dem Access-Token können schließlich abgesicherte Bereiche und Ressourcen eines Web-API konsumiert werden.
Der Resource Owner Password Credentials Flow ist Teil der in OAuth 2.0 definierten Flows und wird in der OIDC-Spezifikation nicht explizit erwähnt. Nachdem dieser Flow strenggenommen die Grundidee von OAuth 2.0 untergräbt – Clients keinerlei Zugangsinformationen von Benutzern offenzulegen – sollte er ausschließlich zum Einsatz kommen, sofern ein starkes Vertrauensverhältnis zwischen Benutzern und Clients vorherrscht. Im Unterschied zum Implicit Flow geben Benutzer in diesem Fall ihre Zugangsinformationen direkt dem Client preis, der an den Authorization-Server delegiert und im Zuge dessen die Authentifizierung und Autorisierung von Benutzern erfolgen. Während der Implicit Flow öffentlichen Clients zur Verfügung steht, muss der Client in diesem Fall beim Authorization Server registriert sein. Er muss sich selbst über ein im Vorfeld geteiltes Secret authentifizieren können, damit Tokens erfolgreich ausgestellt werden können (Abb. 4).
Wenn es um die Absicherung von Web-APIs geht, bieten sich viele Alternativen. Der Fokus dieses Artikels liegt auf Keycloak [1], einer im Unternehmensumfeld flexibel einsetzbaren und gut dokumentierten Lösung fürs Identity- und Access-Management. Keycloak ist ein Open-Source-Projekt der Red-Hat-Community, das von der OpenID Foundation im Herbst letzten Jahres zertifiziert wurde und viele Funktionalitäten bietet, um es z. B. als zentrale Single-Sign-on/out-Lösung für Webanwendungen im Unternehmenskontext zu etablieren. Neben einer eigenständigen, vollwertigen Benutzerverwaltung kann Identity Brokering mittels Social Log-ins in Google oder Twitter ebenso einfach realisiert werden wie ein User-Federation-Ansatz zur Authentifizierung von Benutzern mit Active Directory oder LDAP – alles quasi out of the box. Keycloak setzt dabei auf Standards wie OpenID Connect [5], OAuth 2 [4] und SAML [10]. Um die Clientintegration noch ein Stück weit zu erleichtern, liefert das Open-Source-Projekt eine Reihe an Clientadaptern [11] für verschiedene Plattformen und Sprachen mit. Eine detaillierte Betrachtung rund um Aufbau und Funktionsumfang von Keycloak findet sich in einer früheren Ausgabe des Java Magazins [12]. Im vorliegenden Artikel werden hingegen die grundlegenden Aspekte betrachtet, die zur tokenbasierten Absicherung eines einfachen Web-API nötig sind. Nach dem Download und Entpacken der Keycloak-Stand-alone-Server-Distribution [13] lässt sich Keycloak aus dem Unterverzeichnis bin/ mit standalone(.bat|.sh)-Skript lokal auf Defaultport 8080 starten. Sofern Änderungen an der Startkonfiguration benötigt werden, können sie unter standalone/configuration/standalone.xml vorgenommen werden. Öffnet man nach dem erstmaligen Start im Browser die Keycloak-Einstiegsseite (http://localhost:8080/auth), erfolgt die Aufforderung, zunächst einen Administratoraccount anzulegen. Sobald er existiert, ist eine Anmeldung über die Administration Console (http://localhost:8080/auth/admin/) möglich.
Zur Absicherung der Flights Web-APIs wird im ersten Schritt ein eigener so genannter Realm im Administrationsbereich erstellt. Dazu im Menü links oben auf Master | Add Realm klicken und den Namen definieren – für dieses Beispiel angular-spring. Realms stellen innerhalb von Keycloak unterschiedliche, voneinander isolierte Sicherheitsbereiche dar und bieten die Möglichkeit, Benutzer, Rollen, Gruppen sowie Applikationen oder Clients darin zu verwalten.
Für das Beispiel dieses Artikels werden letztlich drei Clients im neu definierten angular-spring-Realm konfiguriert, um sowohl die beiden Flows für das Angular Frontend als auch das Spring Backend zu adressieren. Für das Spring Backend wird ein Client mit der Client-ID spring-webapi erstellt (Abb. 5). Dazu links im Menü auf Clients wechseln, die ID eintragen und mit Create Button bestätigen. Im Tab Settings für diesen neuen Client sollte man unter Access Type „Bearer only“ auswählen. Diese Einstellung bedeutet, dass deratige Clients als Web Service agieren und nur Requests zulassen, die sich mit einem gültigen Bearer-Access-Token im HTTP-Authorization-Header ausweisen können. Die Konfiguration sollte mit Save Button gespeichert werden.
Nun wird ein zweiter Client mit der Client-ID angular-app-1 für den Implicit Flow der Frontend-Applikation registriert (Abb. 6). Im Tab Settings für diesen Client sind ein paar zusätzliche Konfigurationen vorzunehmen, je nachdem, welche OIDC/OAuth 2.0 Flows benötigt werden. Als Protokoll wird openid-connect definiert. Der Access Type ist public, nachdem es sich beim Client um eine Single Page Application im Browser handeln wird, die ein Client Secret nicht sicher verwahren kann. Für diesen Client wird Implicit Flow aktiviert. Wichtig sind noch die Einstellungen für die beiden Optionen Valid Redirect URIs und Web Origins, womit im Endeffekt bestimmt werden kann, von welchen domainfremden Hosts Anfragen erlaubt sein sollen. Für Demozwecke wird hier jeweils * als Wildcard verwendet, um Anfragen von jeglichen Hosts zu akzeptieren. In realen Projekten sollte an dieser Stelle unbedingt restriktiv auf die betreffenden URIs eingeschränkt werden. Durch die Aktivierung der Option „Consent Required“ wird der jeweilige Benutzer im Rahmen der Authentifizierung explizit um seine Zustimmung gefragt, ob die angeforderten Rechte auch tatsächlich an die Clientapplikation übertragen werden sollen. Auch diese Konfiguration ist mit Save Button zu speichern.
Der dritte Client mit der Client-ID angular-app-2 bedient den Password Credentials Flow der Frontend-Applikation. Als Access Type und Protocol werden dieselben Einstellungen wie vorhin gewählt. Zur Unterstützung dieses Flows ist die Option „Direct Access Grants Enabled“ zu aktivieren. Die Option „Consent Required“ bleibt in diesem Fall deaktiviert, weil der Password Credentials Flow keine direkte Möglichkeit für die explizite Zustimmung der Rechtedelegation seitens des Benutzers bietet (Abb. 7).
Für Testzwecke werden dem angular-spring Realm noch eine Rolle und ein Benutzer hinzugefügt. Unter Roles | Add Role wird eine Rolle namens flightapi_user erstellt, die die Berechtigung zur Verwendung des Flight Web API repräsentiert. Die Option „Scope Param Required“ soll aktiviert werden, womit sichergestellt ist, dass diese Berechtigung nur dann erteilt wird, wenn der Client den Rollennamen als Scope-Parameter in seiner Anfrage anführt. Abschließend wird unter Users | Add User ein Benutzer erstellt. Neben dem Pflichtfeld „Username“ sollten auch die Felder „E-Mail“ sowie „First & Last Name“ ausgefüllt werden. Nach dem Speichern kann im Credentials Tab ein Passwort gesetzt werden. Dabei sollte die Option „Temporary“ deaktiviert und mit „Reset Password Button“ bestätigt werden. Ebenso wird dem neuen Benutzer im Role Mappings Tab die zuvor angelegte Rolle flightapi_user zugeordnet, die die Berechtigung – in Form eines Claims im Access-Token – beim Zugriff für die spätere Verwendung des abzusichernden Web-API symbolisiert. Die gesamte Realm-Konfiguration wird als JSON-Datei im GitHub Repository [14] bereitgestellt, um sämtliche hier beschriebenen Einstellungen einfach importieren zu können.
Ausgangspunkt für die Absicherung des Backends mittels OIDC/OAuth 2.0 ist eine simpel gestricktes, auf Spring Boot basierendes Web-API [15], das es konsumierenden Clients ermöglichen soll, nach Flügen zu suchen. Eine detaillierte Beschreibung findet sich im Readme des GitHub Repositorys [2]. Daher soll an dieser Stelle nur ein kurzer Überblick über die ungesicherte Anwendung gegeben werden. Das Domänenmodell des Web-API besteht lediglich aus einer einzigen Entitätsklasse namens Flight, die Attribute von Flügen kapselt. Als Repository für die Flight-Entitäten fungiert ein Spring Data CrudRepository. Wir deklarieren dafür lediglich ein paar verschiedene find*-Methoden, um Flüge basierend auf unterschiedlichen Suchangaben finden zu können. Nachdem sich in den Maven-Abhängigkeiten des Projekts bereits Spring Boot Starter Data JPA sowie H2 als Datenbank befinden, ist die einfache In-memory-Persistenz für Flight-Entitäten damit bereits verwendbar. Clientanwendungen benötigen entsprechende HTTP-Endpunkte, um das Flights-Web-API zu konsumieren. Diese werden über einen Spring Controller, genauer gesagt @RestController, bereitgestellt, der das Flight Repository verwendet, um Suchanfragen zu Flügen beantworten zu können. Die Spring-Boot-Applikation bietet zunächst noch keinerlei Absicherungsmaßnahmen. Daher ist das Backend von beliebigen Clients verwendbar.
Für die Absicherung des Flights-Web-API mit Keycloak kann entweder der bereitgestellte Spring-Security-Adapter oder der Spring-Boot-Adapter verwendet werden. Letzterer ist aufgrund bestimmter Spring-Boot-Konventionen und automatischer Konfigurationen komfortabler und erfordert weniger manuellen Aufwand bei der Integration. Um den Spring-Boot-Adapter ins Projekt zu integrieren, werden zunächst die benötigten Maven-Abhängigkeiten eingefügt:
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-adapter</artifactId>
<version>2.4.0.Final</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-tomcat8-adapter</artifactId>
<version>2.4.0.Final</version>
</dependency>
Der Keycloak-Adapter selbst kann einfach und wie gewohnt in der application.properties-Datei (alternativ auch im YAML-Format application.yml) wie in Listing 2 konfiguriert werden.
Listing 2: Den Keycloak-Adapter konfigurieren
keycloak.cors = true
keycloak.realm = angular-spring
keycloak.auth-server-url = http://localhost:8080/auth
keycloak.bearer-only = true
keycloak.resource = spring-webapi
keycloak.securityConstraints[0].securityCollections[0].name = secured controller
keycloak.securityConstraints[0].securityCollections[0].authRoles[0] = flightapi_user
keycloak.securityConstraints[0].securityCollections[0].patterns[0] = /api/flight/*
Im ersten Konfigurationsblock werden die Basiseinstellungen definiert. Dazu gehören der Realm-Name (angular-spring), der Auth-Server-URL, die Client-ID (spring-webapi) des Backends und der Access Type (bearer-only). Danach erfolgt die eigentliche Absicherung von bestimmten URL-Mustern (patterns) mit den dafür benötigten Berechtigungen (authRoles).
Wichtig ist an dieser Stelle der Hinweis, dass sämtliche URL-Muster, die nicht auf die konfigurierten passen, standardmäßig ungesichert sind. In diesem Beispiel sind sämtliche Endpunkte des Flight Controllers (/api/flight/*) vor unautorisierten Clientzugriffen geschützt. Nur für Anfragen mit der entsprechenden Berechtigung (flightapi_user) werden Daten ausgeliefert. Keycloak prüft dabei, ob sich ein gültiges Bearer-Access-Token in Form eines JSON Web Tokens im Authorization-Header des HTTP Requests befindet. Das Access-Token muss außerdem den Realm_Access.Roles Claim flightapi_user aufweisen, damit der Zugriff auf das Flight-Web-API genehmigt wird. Im Fehlerfall wird zwischen zwei HTTP-Statuscodes unterschieden: 401 (Unauthorized) bei fehlendem oder ungültigem JWT Bearer Token im Authorization Header oder 403 (Forbidden) bei nicht vorhandenen Claims und damit fehlenden Berechtigungen.
Um den Einsatz der beiden vorgestellten Flows zu demonstrieren, bietet die hier genutzte Angular-Anwendung auch zwei Arten der Anmeldung. Der Benutzer kann sich zur Anmeldung somit entweder zum Authorization-Server umleiten lassen oder dem Client sein Passwort direkt anvertrauen.
Für die clientseitige Umsetzung der beiden Flows nutzt der Client die Bibliothek angular-oauth2-oidc [16], die der Autor über npm zur Verfügung gestellt hat: npm install angular-oauth2-oidc –save. Um die Bibliothek nach dem Herunterladen der Angular-Anwendung bekannt zu machen, ist das OAuthModule-Root-Modul zu importieren (Listing 3).
Listing 3: „OAuthModule“-Root-Modul importieren
import { OAuthModule } from 'angular-oauth2-oidc';
@NgModule({
imports: [
[...]
OAuthModule.forRoot()
],
[...]
})
export class AppModule {
}
Danach ist die Bibliothek mit Eckdaten zum registrierten Client sowie zum Authorization-Server zu registrieren. Die hier betrachtete Implementierung übernimmt diese Aufgabe im Konstruktor der AppComponent. Dazu lässt sie sich den OAuthService injizieren und hinterlegt die benötigten Informationen in ihren Eigenschaften (Listing 4).
Listing 4: „OAuthService“ injizieren
import { Component } from '@angular/core';
import { OAuthService } from 'angular-oauth2-oidc';
@Component({
selector: 'flight-app',
templateUrl: './app.component.html'
})
export class AppComponent {
constructor(private oauthService: OAuthService) {
// Für den Client registrierte Id
this.oauthService.clientId = "angular-app-1";
// Url des Angular-Clients, an die das Token zu senden ist
this.oauthService.redirectUri = window.location.origin + "/index.html";
// Rechte, die der Client wahrnehmen möchte
this.oauthService.scope = "openid profile email flightapi_user";
// Definieren, dass auch ein Id-Token abgerufen werden soll
this.oauthService.oidc = true;
// Festlegen, ob Tokens im localStorage oder im sessionStorage zu speichern sind
this.oauthService.setStorage(sessionStorage);
let url = 'https://hpg-keycloak.northeurope.cloudapp.azure.com/auth/realms/angular-spring/.well-known/openid-configuration';
this.oauthService.loadDiscoveryDocument(url).then((doc) => {
// Eventuelle Tokens aus Url entnehmen
this.oauthService.tryLogin({});
});
}
}
Zu diesen Informationen gehören die ID des Clients sowie dessen URL, an den die Tokens beim Implicit Flow zu senden sind. Aus Sicherheitsgründen müssen diese beiden Informationen im Vorfeld beim Authorization-Server registriert werden. Auf diese Weise stellt er sicher, dass tatsächlich der Client mit der angegebenen ID und somit jener Client, für den sich der Benutzer anmeldet, die Tokens erhält. Der Scope repräsentiert die einzelnen Berechtigungen, die der Client im Namen des Benutzers durchführen möchte. Die ersten drei hier definierten Werte stammen aus der Welt von OpenID Connect. Sie erlauben Zugriff auf die Benutzer-ID (openid), Profilinformationen wie Vorname und Nachname (profile) und die E-Mail-Adresse (email) des Benutzers. Der vierte Scope (flightapi_user), der im Authorization-Server zu definieren ist, ist Use-Case-spezifisch und ermöglicht Zugriff auf das Spring-Boot-basierte Web-API.
Weitere Eckdaten bezieht der Client über das Discovery-Dokument, das der Authorization-Server bereitstellt. Dabei handelt es sich um ein durch OpenID Connect definiertes JSON-Dokument, das unter anderem die einzelnen Endpunkte zum Anfordern von Tokens oder Benutzerinformationen widerspiegelt. Per Definition findet es sich unter jenem URL, der sich ergibt, wenn man an den URL des Authorization-Servers die Segmente .well-known/openid-configuration anhängt. In Fällen, bei denen der Authorization-Server der Wahl kein solches Dokument anbietet, nimmt die Bibliothek die einzelnen Einstellungen auch über Eigenschaften entgegen. Informationen dazu finden sich in der Dokumentation [16].
Der Aufruf von tryLogin prüft gleich zu Programmstart, ob sich in dem URL Tokens befinden. Das ist der Fall, wenn der Authorization-Server den Benutzer am Ende des Implicit Flows wieder zur Anwendung umleitet. Sind Tokens vorhanden, entnimmt tryLogin diese aus dem URL und validiert sie. Anschließend verstaut der OAuthService die extrahierten Tokens im konfigurierten Storage. Entsprechend der betrachteten Konfiguration kommt hierzu der sessionStorage zum Einsatz, der Daten für die Dauer einer Benutzersitzung vorhält.
Ist die Bibliothek konfiguriert, gestaltet sich die weitere Vorgehensweise einfach. Um den Implicit Flow anzustoßen, ist lediglich die Methode initImplicitFlow beim OAuth-Service aufzurufen (Listing 5).
Listing 5: „initImplicitFlow“ über OAuth-Service aufrufen
export class HomeComponent {
[...]
constructor(private oauthService: OAuthService) {
}
login() {
this.oauthService.initImplicitFlow();
}
logout() {
this.oauthService.logOut();
}
get givenName() {
var claims = this.oauthService.getIdentityClaims();
if (!claims) return null;
return claims.given_name;
}
[…]
}
Für das Abmelden steht die Methode logOut zur Verfügung. Sie löscht die im Storage hinterlegten Tokens und leitet den Benutzer zu einem Logout-URL des Authorization-Servers um. Auf diese Weise erfährt auch dieser, dass sich der Benutzer abgemeldet hat. Um Informationen über den Benutzer in Erfahrung zu bringen, ruft der Client die Methode getIdentityClaims auf. Diese enthält die Claims aus den ID-Tokens, die durch OpenID Connect definiert sind. Bei Bedarf lassen sich zusätzliche Claims beim Authorization-Server registrieren.
Über den Resource Owner Password Credentials Flow lässt sich ähnlich einfach ein Access-Token beziehen. Die Bibliothek angular-oauth2-oidc stellt hierzu die Methode fetchTokenUsingPasswordFlowAndLoadUserProfile zur Verfügung (Listing 6).
Listing 6: Die Methode „fetchTokenUsingPasswordFlowAndLoadUserProfile“
userName: string;
password: string;
loginFailed: boolean = false;
loginWithPassword() {
this.oauthService.clientId = "angular-app-2";
this
.oauthService
.fetchTokenUsingPasswordFlowAndLoadUserProfile(this.userName, this.password)
.then(() => {
console.debug('successfully logged in');
this.loginFailed = false;
})
.catch((err) => {
console.error('error logging in', err);
this.loginFailed = true;
})
.then(() => {
this.oauthService.clientId = "angular-app-1";
});
}
Wie der Name vermuten lässt, kümmert sie sich um zwei Dinge: Zum einen tauscht sie Benutzername und Passwort gegen ein Access-Token ein. Zum anderen ruft sie Daten über den Benutzer ab. Für Letzteres kommt der durch OpenID Connect definierte User-Info-Endpunkt zum Einsatz. Das explizite Abfragen dieser Daten ist notwendig, da die Spezifikationen beim Einsatz des Resource Owner Password Credentials Flows kein ID-Token vorsehen. Das erhaltene Access-Token kommt zur Authentifizierung beim User-Info-Endpunkt zum Einsatz. Die auf diese Weise erhaltenen Informationen behandelt die Bibliothek wie jene aus dem ID-Token und stellt sie auch über die oben besprochene Methode getIdentityClaims zur Verfügung.
Auch der Zugriff auf das Web-API gestaltet sich einfach. Hierzu ist lediglich das Access-Token über den Authorization-Header in der Form Authorization: Bearer …Token… an den Server zu übersenden wie in Listing 7.
Listing 7: Access-Token an den Server senden
@Injectable()
export class FlightService {
constructor(
private oauthService: OAuthService,
private http: Http,
@Inject(BASE_URL) private baseUrl: string
) {
}
public flights: Array<Flight> = [];
find(from: string, to: string): void {
let url = this.baseUrl + "/api/flight";
let headers = new Headers();
headers.set('Accept', 'application/json');
headers.set('Authorization', 'Bearer ' + this.oauthService.getAccessToken());
let search = new URLSearchParams();
search.set('from', from);
search.set('to', to);
this
.http
.get(url, {headers, search})
.map(resp => resp.json())
.subscribe(
(flights) => {
this.flights = flights;
},
(err) => {
console.warn('status', err.status);
}
);
}
}
Wichtig ist es hier auch, auf einen eventuellen Fehler zu reagieren, den das Web-API zurückmeldet. Das betrachtete Listing deutet das an, indem es den Fehlercode ausgibt. Besonders die Fehler 401 (Unauthorized) und 403 (Forbidden) sind im Fall der Zugriffskontrolle zu beachten. Ersterer gibt darüber Auskunft, dass der aktuelle Benutzer nicht bekannt ist, und Letzterer informiert darüber, dass der Benutzer nicht die nötigen Berechtigungen aufweist. War der Benutzer zuvor bereits angemeldet, könnten diese Fehler darauf hinweisen, dass das Token nicht mehr gültig und die Benutzersitzung abgelaufen ist. In beiden Fällen könnte man den Benutzer – z. B. mit dem Router von Angular – auf die Log-in-Seite umleiten und dort eine entsprechende Information ausgeben. Damit die Anwendung prüfen kann, ob gültige Tokens vorliegen, stellt der OAuth-Service die Methoden hasValidIdToken und hasValidAccessToken zur Verfügung. Im Zug des Routings könnte die Anwendung mit einem so genannten Guard damit prüfen, ob der Benutzer geschützte Bereiche der Anwendung nutzen darf. Informationen dazu finden sich unter [17].
Dank bestehender Implementierungen lassen sich die populären Standards OAuth 2.0 und OpenID Connect relativ einfach zur Schaffung moderner Authentifizieurngs- und Autorisierungslösungen nutzen: Mit wenigen Maßnahmen lassen sich damit sowohl Web-APIs als auch Clients absichern, wie Single Page Applications. Diese vorhandenen Lösungen sollten aber nicht darüber hinwegtäuschen, dass eine nähere Betrachtung der jeweiligen Standards unumgänglich ist. Gerade wenn es um Detailfragen geht, müssen Architekten wissen, was möglich und was gefährlich oder gar aus Sicherheitsgründen explizit verboten ist. Auch bei der Bewertung unterschiedlicher Optionen und Flows sowie bei der Fehlersuche kommt man zumeist nicht ohne genauere Kenntnis der Standards aus.
Manfred Steyer betreut als Trainer und Berater Unternehmen im gesamten deutschen Sprachraum. Dabei fokussiert er sich auf Angular. Er hat Bücher bei O‘Reilly, Microsoft Press und Hanser veröffentlicht sowie zahlreiche Fachartikel verfasst. Für seine Aktivitäten wurde er von Google als Developer Expert (GDE) und von Microsoft mit dem MVP-Award ausgezeichnet. Sein Wissen gibt er regelmäßig auf Konferenzen weiter.
Hans-Peter Grahsl arbeitet im Java-Bereich als Technical Trainer und ist für das interne Education Department bei Netconomy Software & Consulting GmbH in Graz verantwortlich. Außerdem unterstützt er Kunden als selbstständiger Trainer und Berater bei der Konzeption und Umsetzung von on-premise- oder Cloud-basierten Datenarchitekturen im NoSQL-Umfeld. Nebenberuflich unterrichtet er an der FH CAMPUS 02.
[1] Keycloak: http://www.keycloak.org/
[2] Flights Web-API: https://github.com/hpgrahsl/FlightsWebAPI
[3] OAuth OIDC Sample auf GitHub: https://github.com/manfredsteyer/angular-oauth2-oidc-sample
[4] OAuth: https://oauth.net/2/
[5] OpenID: http://openid.net/connect/
[6] JWT: https://tools.ietf.org/html/rfc7519
[7] JWS: https://tools.ietf.org/html/rfc7515
[8] JWE: https://tools.ietf.org/html/rfc7516
[9] JWT-Projektseite: http://jwt.io
[10] SAML-Wiki: https://wiki.oasis-open.org/security/FrontPage
[11] Java-Adapter: https://keycloak.gitbooks.io/securing-client-applications-guide/content/topics/oidc/java/java-adapters.html
[12] Köbler, Niko: „Einmal und nie wieder“, in: Java Magazin 2.2017
[13] Keycloak-Download: https://downloads.jboss.org/keycloak/2.4.0.Final/keycloak-2.4.0.Final.zip
[14] Flights Web-API GitHub: https://github.com/hpgrahsl/FlightsWebAPI/blob/master/src/main/resources/keycloak-angular-spring-realm-config.json
[15] Spring Boot: https://projects.spring.io/spring-boot/
[16] Support für OAuth 2.0 and OpenID Connect (OIDC) in Angular: https://www.npmjs.com/package/angular-oauth2-oidc
[17] Steyer, Manfred: „Authentifizierung mit OAuth und OIDC“: https://www.softwarearchitekt.at/post/2016/07/01/authentifizierung-in-angular-2-mit-oauth2-oidc-dem-neuen-router-und-guards.aspx
Kühner Gedanke: Wir könnten Anwendungsteile großer Softwaresysteme voneinander entkoppeln und das Internet für den Datentransfer nutzen. Damit bei der losen Koppelung von Services kein Chaos entsteht, sollten wir das Ganze richtig verwalten: Services müssen sich zentral registrieren und eine zentrale Instanz steuert Routing, Berechtigungen, Datenkonvertierung usw. Nennen wir diese Instanz den Enterprise Service Bus (ESB) und die Registrierungsstelle UDDI (Universal Description, Discovery and Integration). Für Protokolle und Servicebeschreibung wählen wir ein sehr mitteilsames Format, weil Komplexität ja besonders ausschweifende Erläuterungen erfordert: XML. Nennen wir das Ganze schließlich SOA: Service-oriented Architecture!
Dieser Gedanke ist aus heutiger Sicht natürlich keineswegs kühn. Aber er repräsentierte damals – vor 1,5 Jahrzehnten, als er in Mode war – den ersten validen Ansatz, das Internet als intrinsischen Bestandteil der Systemarchitektur mitzudenken. Warum wurde SOA dann aber durch Konzepte wie API-Management und Microservices abgelöst? Weil SOA zwar das Internet als Potenzial erkannt, das klassische OO-Denken dabei aber noch nicht überwunden hat: Alle Maßnahmen zur Konsistenzsicherung eines Systems basierten auf der Annahme, dass sich letztlich auch im Netz der gleiche Zustand herstellen lässt wie innerhalb eines geschlossenen Systems.
Kleiner Exkurs: Als der Ottomotor erfunden war und sich rasch verbreitete, verstanden die großen Hersteller von Pferdekutschen schnell, welche Bedeutung dieser ab sofort für die Mobilität der Menschen haben würde. Sie entfernten die Pferdedeichseln und bauten Verbrennungsmotoren in ihre Kutschen ein. Aber wer hat letztlich die automobile Revolution angeführt? Die Newcomer, die erstens Autos und nicht motorgetriebene Kutschen bauten, und die zweitens lernten, in neuen Maßstäben zu skalieren und auch das erfanden. Die Kutschenhersteller hatten aus ihrer Sicht indes nichts falsch gemacht und dennoch die Zeichen der Zeit nicht erkannt.
SOA war im Kern ebenso richtig gedacht, berücksichtigte aber die vielen Details zu wenig, die in offenen, internetbasierten Umgebungen einfach anders sind. Heutige Ansätze, bei denen stets das Web-API im Zentrum steht, sind daher nicht alter Wein in neuen Schläuchen, sondern eine wichtige Neuinterpretation des Themas im Sinne einer SOA 2.0.
Thilo Frotscher gibt ab Seite 46 einen guten Überblick über die Herausforderungen beim Einsatz API-basierter Systeme, Niko Köbler analysiert die Aufgaben eines API Gateway (Seite 54) und Michael Jacoby und Dr. Hylke van der Schaaf zeigen anhand eines spannenden Beispiels, wie ein generisches API im Internet der Dinge beschaffen sein sollte (Seite 60).
Viel Spaß bei der Lektüre wünscht
Kühner Gedanke: Wir könnten Anwendungsteile großer Softwaresysteme voneinander entkoppeln und das Internet für den Datentransfer nutzen. Damit bei der losen Koppelung von Services kein Chaos entsteht, sollten wir das Ganze richtig verwalten: Services müssen sich zentral registrieren und eine zentrale Instanz steuert Routing, Berechtigungen, Datenkonvertierung usw. Nennen wir diese Instanz den Enterprise Service Bus (ESB) und die Registrierungsstelle UDDI (Universal Description, Discovery and Integration). Für Protokolle und Servicebeschreibung wählen wir ein sehr mitteilsames Format, weil Komplexität ja besonders ausschweifende Erläuterungen erfordert: XML. Nennen wir das Ganze schließlich SOA: Service-oriented Architecture!
Dieser Gedanke ist aus heutiger Sicht natürlich keineswegs kühn. Aber er repräsentierte damals – vor 1,5 Jahrzehnten, als er in Mode war – den ersten validen Ansatz, das Internet als intrinsischen Bestandteil der Systemarchitektur mitzudenken. Warum wurde SOA dann aber durch Konzepte wie API-Management und Microservices abgelöst? Weil SOA zwar das Internet als Potenzial erkannt, das klassische OO-Denken dabei aber noch nicht überwunden hat: Alle Maßnahmen zur Konsistenzsicherung eines Systems basierten auf der Annahme, dass sich letztlich auch im Netz der gleiche Zustand herstellen lässt wie innerhalb eines geschlossenen Systems.
Kleiner Exkurs: Als der Ottomotor erfunden war und sich rasch verbreitete, verstanden die großen Hersteller von Pferdekutschen schnell, welche Bedeutung dieser ab sofort für die Mobilität der Menschen haben würde. Sie entfernten die Pferdedeichseln und bauten Verbrennungsmotoren in ihre Kutschen ein. Aber wer hat letztlich die automobile Revolution angeführt? Die Newcomer, die erstens Autos und nicht motorgetriebene Kutschen bauten, und die zweitens lernten, in neuen Maßstäben zu skalieren und auch das erfanden. Die Kutschenhersteller hatten aus ihrer Sicht indes nichts falsch gemacht und dennoch die Zeichen der Zeit nicht erkannt.
SOA war im Kern ebenso richtig gedacht, berücksichtigte aber die vielen Details zu wenig, die in offenen, internetbasierten Umgebungen einfach anders sind. Heutige Ansätze, bei denen stets das Web-API im Zentrum steht, sind daher nicht alter Wein in neuen Schläuchen, sondern eine wichtige Neuinterpretation des Themas im Sinne einer SOA 2.0.
Thilo Frotscher gibt ab Seite 46 einen guten Überblick über die Herausforderungen beim Einsatz API-basierter Systeme, Niko Köbler analysiert die Aufgaben eines API Gateway (Seite 54) und Michael Jacoby und Dr. Hylke van der Schaaf zeigen anhand eines spannenden Beispiels, wie ein generisches API im Internet der Dinge beschaffen sein sollte (Seite 60).
Viel Spaß bei der Lektüre wünscht
Unserer Einschätzung nach werden Schnittstellen oft als Dinge dritter Klasse behandelt. Technologie auswählen, Features bauen und Bugs fixen gehen vor. Sogar Dokumentation schreiben scheint uns in manchen Projekten wichtiger als effektive Schnittstellen zu entwerfen. Wir wünschen Ihnen und uns die API-tektin, die gemeinsam mit Softwarearchitekten den Schnittstellen auf die Sprünge hilft.
Aber was ist eine Schnittstelle? Der einfachste Fall: Sie haben einen Consumer und einen Provider. Consumer benötigt irgendetwas vom Provider, seien es Rechen- oder Abfrageergebnisse, Daten oder Dateien, Messwerte von Sensoren oder Statusinformationen; also eine digitale Antwort auf eine ebenso digitale Frage. Sie können diesen einfachen Fall durch Fragen von Zeitverhalten (sync, async) oder Antwortverhalten (request-response, fire-and-forget, single-consumer, multiple-consumer etc.) noch verkomplizieren. Aber schauen wir mal auf den einfachsten Fall: Ein Consumer muss die Anfrage irgendwie an den Provider übermitteln. Oft geschieht das über einen Funktions- oder Methodenaufruf mit Parametern und Rückgabewert. Viele der üblichen Interaktionen zwischen Consumern und Providern können wir auf solch einfache Aufrufe zurückführen. Diese Art der Zusammenarbeit zwischen Bausteinen ist die Grundlage von Modularisierung und Komponentenbildung und kommt in Systemen aller Art an beliebig vielen Stellen vor. Damit ist Schnittstellenentwurf ein alltägliches Allerweltsproblem. Und wir sollten überlegen, welche der jeweiligen Personen (Stakeholder) eigentlich an solchen Entscheidungen beteiligt sind.
Abbildung 1 zeigt diese Situation – völlig verallgemeinert – mit möglichen Beteiligten: Das Consumer-Team C, Providerteam P, eine Architektin A, Management M sowie sonstige Stakeholder S. In einer solchen Situation müssen für die Schnittstellen jede Menge Detailentscheidungen getroffen werden. Und dabei können sich die Stakeholder A, C, P sowie M und S wie in Tabelle 1 gezeigt einbringen.
Wer? |
Begründung |
---|---|
C + P |
Consumer und Provider stimmen sich über Details ab. |
A + C + P |
API-tektin sorgt bei der Schnittstelle für das Einhalten querschnittlicher Konzepte, die C oder P möglicherweise nicht kennen oder beachten. |
A |
API-tektin bestimmt allein; das reduziert den Abstimmungsaufwand für C und P. |
C |
Consumer definiert, weil C am besten entscheiden kann, was wirklich benötigt wird. |
P |
Provider entscheidet, weil P am besten entscheiden kann, was möglich ist. |
A + M |
API-tektin beteiligt Management, weil die Schnittstelle möglicherweise mehr Geld oder Aufwand benötigt. |
A + S |
Sonstige Stakeholder beteiligen sich, weil gegebenenfalls betriebliche, rechtliche, finanzielle oder sonstige Aspekte zu berücksichtigen sind. Insbesondere können das auch besondere Qualitätsanforderungen sein. |
Tabelle 1: Bei Detailentscheidungen ist nicht immer das Wissen aller nötig
Entscheidet nur eine Partei, minimiert das den Abstimmungsaufwand. Entscheiden viele Parteien, steigert das den Aufwand, führt aber möglicherweise zu einer höheren Qualität der Entscheidungen, weil mehrere unterschiedliche Gesichtspunkte berücksichtigt werden können. Bedenken Sie: In jedem System gibt es Dutzende von Schnittstellenentscheidungen zu treffen, und jede davon lässt sich auf eine der genannten Arten treffen.
Unser dringender Rat: Als API-tektin suchen Sie zuerst die besonders kritischen oder wichtigen Schnittstellen, die auf wesentliche Qualitätsmerkmale des Systems Einfluss nehmen könnten. Dann überlegen Sie für diese Prio-1-Schnittstellen, welche Personen an den jeweiligen Entscheidungen mitwirken sollten. Sie müssen dabei neben den rein architektonischen Anforderungen und Gegebenheiten noch Faktoren wie Zeit, Aufwand, Homogenität der Architektur, Umsetzungsgeschwindigkeit oder andere Qualitätsmerkmale berücksichtigen. Das ist eine schwere Aufgabe. Für diese Metaentscheidung benötigen Sie neben Detailwissen über die jeweiligen Consumer und Provider noch einen guten Überblick über das Gesamtsystem. Daher sind Sie als Architektin – respektive API-tektin – für dieses Thema prädestiniert.
Neben den in der Realität gar nicht so banalen Fragen nach den Namen von Schnittstellen und Parametern taucht in der Praxis sofort die Frage der Fehler- und Ausnahmebehandlung auf. In der Praxis macht die Behandlung von Fehler- und Sonderfällen oft 80 Prozent des gesamten Aufwands aus. In Remotesituationen muss der Consumer einen passenden Provider erst einmal finden – da helfen beispielsweise Registries oder Broker – und sie müssen sich ausweisen (authenticate) sowie Berechtigung nachweisen (authorize). Dann wäre da noch die Frage nach dem technischen Kommunikations- oder Übertragungsprotokoll (Plain Sockets, FTP, HTTP etc.) oder möglichen Handshake-Schritten vor der eigentlichen Kommunikation. Bis hierhin ist es meist noch überschaubar. Aber dann kommen in schlimmen Fällen noch eines oder mehrere der folgenden Themen ins Spiel. Und dann es wird es richtig schwierig:
Vertraulichkeit: Wenn an der Schnittstelle übertragene Daten oder sogar die Benutzung der Schnittstelle an sich vertraulich sind, müssen Sie gegebenenfalls in die Werkzeugkiste der Kryptografie greifen und dazu noch organisatorische Sicherheitsaspekte berücksichtigen. Schwierigkeitsgrad: extrem hoch
Verfügbarkeit: Wenn die Schnittstelle niemals ausfallen darf, könnten Sie Redundanz in Software und Hardware einführen, einen Cluster und Load Balancer einsetzen oder weitere teure Maßnahmen starten. Schwierigkeitsgrad: hoch, Kosten: hoch bis sehr hoch
Durchsatz, Antwortzeit: Die Implementierung des Providers optimieren, Redundanz (z. B. Caching) einführen, extrem schnelle Hardware oder Netze kaufen. Schwierigkeitsgrad: mittel, Kosten: beliebig
Flexibilität: Sie möchten Datenformate flexibel halten, die Provider oder Zugriffskanäle austauschbar gestalten. Theoretisch ist alles möglich, praktisch für Entwurf und Implementierung beliebig aufwendig. Testen wird aber zum Albtraum. Schwierigkeitsgrad: Umsetzung mittel, Test: extrem
Versionierung: Abwärtskompatibel? Source- oder binärkompatibel? Jede Änderung eine neue Version vom Provider? Versionskennung in den Namen der Schnittstelle oder als Metainformation in die Parameter? Wie OSGi? Schwierigkeitsgrad: hoch, Aufwand: klein bis beliebig.
Einige Themen liegen uns nach langjähriger Beschäftigung mit Schnittstellen noch am Herzen. Diese möchten wir Ihnen ungeordnet mitgeben:
Externe Schnittstellen sind viel schlimmer als interne Schnittstellen, weil wir auf die Außenwelt normalerweise kaum Einfluss haben, auf unsere inneren Komponenten aber schon.
Consumer von Schnittstellen könnten ausführbare Tests (Consumer-driven Contracts [1]) schreiben, die entsprechende Provider auf Einhaltung der jeweiligen Schnittstellenvereinbarungen überprüfen. Für solche Art Tests gibt es mit Pact [2] einen schicken Open-Source-Ansatz.
Das Thema API-Management ist zum Managementhype geworden, bei dem viele namhafte Softwarehersteller mitspielen. Wir geben erst gar keine Links an: IBM, SAP, Microsoft, CA, Software AG, HP, Intel und so weiter.
Bis vor kurzer Zeit gab es praktisch keine Literatur zum Thema systematischer Entwurf von Schnittstellen. Vom uralten „Practical API Design“ von Jaroslav Tulach [3] einmal abgesehen, das völlig unbrauchbar für Normalbürger war, weil unverständlich und konfus. Obwohl Jaroslav als einer der zentralen NetBeans-Entwickler nachweislich Ahnung von Schnittstellen hat. Es gab also keine Möglichkeit, dieses wichtige Thema zu vertiefen.
Anfang 2017 hat Kai Spichale endlich für Abhilfe gesorgt [4] und den lang erhofften Durchbruch geschafft: Sein Buch „API-Design“ möchten wir Ihnen wärmstens empfehlen. Sie finden darin Infos zu Grundlagen, zu Java-APIs, wie Sie Ihre eigenen Fluent-APIs entwickeln können, zum schwierigen Thema Kompatibilität, zu REST und Web-APIs, SOAP, Web Services und Messaging. Schließlich bietet Kai Ihnen noch Infos zu diversen übergreifenden Themen an, beispielsweise Dokumentation, Caching oder Skalierbarkeit von Schnittstellen. Also, liebe API-tektinnen und API-tekten, jetzt gibt es keine Ausrede mehr.
Schnittstellen Ihres Systems, interne wie auch externe, können über Erfolg und Misserfolg kräftig mitentscheiden. Widmen Sie ihnen angemessene Aufmerksamkeit und erheben Sie API-Design zu einem der wesentlichen Themen in Architektur- und Entwicklungsdiskussionen. Wie so oft müssen Sie priorisieren: Kümmern Sie sich als API-tektin um die wesentlichen Schnittstellen. Manche können Sie durchaus an die Consumer- oder Providerteams delegieren. Aber bei einigen werden Sie selbst mitbestimmen müssen. API-tektin mag wie ein schwieriger Job klingen, aber Sie können damit für viel bessere Systeme sorgen.
In diesem Sinne: Möge die Macht der Schnittstellen mit Ihnen sein!
Peter Hruschka (System Guild) und Dr. Gernot Starke (innoQ-Fellow) haben vor einigen Jahren www.arc42.de gegründet, das freie Portal für Softwarearchitekten. Sie sind Gründungsmitglieder des International Software Architecture Qualification Board (www.iSAQB.org) sowie Autoren mehrerer Bücher rund um Softwarearchitektur und Entwicklungsprozesse.
[1] Vitz, Michael: „Consumer-Driven Contract (CDC): Testen von Schnittstellen in einer Microservice-Architektur“: https://www.innoq.com/de/articles/2016/09/consumer-driven-contracts/
[2] Pact: https://docs.pact.io/, schöne Einführung unter http://dius.com.au/2016/02/03/microservices-pact/
[3] Tulach, Jaroslav: „Practical API Design“, Apress, 2012
[4] Spichale, Kai: „API-Design“, dpunkt, 2017
Web-APIs erfreuen sich großer Beliebtheit, bieten sie doch eine scheinbar perfekte Lösung zur Systemintegration. Jedoch gibt es auch einige Fallstricke, die unbedingt beachtet werden sollten. So führen Web-APIs zu einer recht engen Kopplung. Deshalb wird unter anderem eine Strategie für den Umgang mit vorübergehend nicht verfügbaren Kommunikationspartnern benötigt. Weitere Aspekte sind die fehlende Zuverlässigkeit des Netzwerks oder auch die Tatsache, dass SSL in manchen Produktivumgebungen keine ausreichende Sicherheit bietet.
Web-APIs sind aus der Softwareentwicklung aktuell nicht mehr wegzudenken. Zahllose Unternehmen bieten auf diese Weise den Zugriff auf ihre Daten und Dienste an, und Behörden stellen zunehmend große Informationsmengen zur Verfügung. Aber auch zur Systemintegration innerhalb eines Unternehmens haben sich Web-APIs inzwischen ihren festen Platz gesichert. Und schließlich haben Web-APIs auch durch den Trend zu Microservices-Architekturen einen weiteren Schub erfahren. Denn auch hier gilt es, eine Kommunikation zwischen den einzelnen Services zu implementieren.
Dabei ist der Begriff Web-API an sich gar nicht so leicht zu greifen. Wie so oft, gibt es durchaus unterschiedliche Definitionen und Ansichten, was genau ein Web-API ist, wodurch es sich auszeichnet oder welche Regeln es einhalten sollte. Diese Diskussion soll hier jedoch nicht geführt werden. Im Kontext des Artikels soll der Begriff Web-API eher allgemein definiert sein als eine Schnittstelle auf Basis von Webtechnologien, insbesondere HTTP.
Solche Schnittstellen lassen sich mit aktuellen Frameworks glücklicherweise recht schnell implementieren. Ganz gleich, ob man Spring oder Java EE einsetzt oder vielleicht eines der vielen anderen verfügbaren Frameworks. In aller Regel können Entwickler schon nach recht kurzer Zeit einen ersten Prototyp des neuen Web-API auf einem Testserver bereitstellen. Diese Mächtigkeit heutiger Frameworks ist nicht hoch genug zu bewerten. Im Vergleich zu früheren Zeiten müssen Entwickler heute deutlich weniger Code schreiben, um zum gleichen Ergebnis zu kommen. Dies ermöglicht nicht nur ein agileres Vorgehen, sondern bedeutet auch, dass entsprechend mehr Zeit bleibt, um sich auf die Fachlichkeit zu konzentrieren.
Es besteht jedoch gleichzeitig die Gefahr, dass die Entwicklung der Schnittstelle dadurch unterschätzt wird. Denn es scheint nur auf den ersten Blick eine leichte Aufgabe zu sein, ein gutes Web-API zu erstellen. Doch je länger ein Projekt andauert, je mehr Anforderungen hinzukommen, je besser die Anwendung getestet und je länger sie betrieben wird, desto mehr zeigt sich, dass viele Aspekte bedacht werden müssen. Und dabei lauern einige Fallen. Denn auch wenn Frameworks viele Dinge erleichtern und verbergen, mindestens eine Komplexität bleibt auf jeden Fall erhalten: die Herausforderungen, die verteilte Systeme mit sich bringen. Hierzu zählen unter anderem Kopplung, Fehlerbehandlung, Zuverlässigkeit, Wiederholbarkeit und Sicherheit.
Zunächst ist festzustellen, dass eine auf HTTP basierende Kommunikation zu einer recht engen Kopplung zwischen den Kommunikationspartnern führt. Diese resultiert aus der direkten Punkt-zu-Punkt-Verbindung zwischen Clients und dem Anwendungsendpunkt, der das Web-API bereitstellt. So ist zwingend erforderlich, dass beide Kommunikationspartner gleichzeitig gestartet und verfügbar sind. Sie kommunizieren synchron und müssen sich gegenseitig kennen. Vor diesem Hintergrund ist die häufig geäußerte Einschätzung, REST-Schnittstellen führten zu einer losen Kopplung, zumindest kritisch zu hinterfragen. Denn Kopplung kann in verschiedenen Varianten auftreten. Und während sich für Web-APIs in manchen Kriterien sicherlich eine eher lose Kopplung feststellen lässt, sind zumindest die genannten Charakteristiken eindeutige Anzeichen für eine enge Kopplung.
Gleichzeitig ist festzuhalten, dass eine enge Kopplung zwischen Kommunikationspartnern nicht automatisch etwas Schlechtes ist. Im Gegenteil, sie hat durchaus auch positive Aspekte. Allerdings bringt sie eben auch verschiedene Nachteile mit sich, die bei der Entwicklung von Web-APIs bedacht werden sollten. Beispielsweise ist eine Strategie für solche Fälle notwendig, in denen der Kommunikationspartner vorübergehend nicht verfügbar ist, oder wenn asynchrone Aufgaben mit synchroner Kommunikation angestoßen und verfolgt werden sollen.
Eine besonders problematische Situation kann entstehen, wenn eine größere Anzahl von Systemen eng miteinander gekoppelt wird. Wenn also beispielsweise alle diese Systeme über Punkt-zu-Punkt-Verbindungen miteinander kommunizieren. In einem Systemverbund resultieren Requests an ein System oft in Folge-Requests an weitere Systeme; und diese möglicherweise wiederum in weiteren Requests innerhalb des Verbunds. Mit der Zeit entsteht so ein kaum noch zu überblickender und beherrschender Abhängigkeitsgraph. Gerade bei enger Kopplung können sich lokale Probleme eines Systems recht schnell auf zahlreiche andere Systeme auswirken. Der Systemverbund wird zu einem fragilen Gebilde. Eine solche Entwicklung geschieht meist schleichend und über einen längeren Zeitraum. Es ist daher wichtig, sich dieser Problematik bewusst zu sein, um von Beginn an Gegenmaßnahmen ergreifen zu können. Beispielsweise können zumindest punktuell alternative Kommunikationslösungen zum Einsatz kommen, beispielsweise Messaging. In jedem Fall aber sollte der Resilienz der einzelnen Systeme hohe Beachtung zukommen.
Die meisten Web-APIs ermöglichen nicht nur den Zugang zu bestehenden Daten, sondern auch deren Modifizierung und das Anlegen neuer Daten oder Ressourcen. Oft erlauben es Web-APIs darüber hinaus, länger laufende Prozesse entweder direkt anzustoßen oder deren spätere, asynchrone Ausführung zu beauftragen. In solchen Fällen muss man sich mit der Frage beschäftigen, was geschehen soll, wenn eine solche HTTP-Anfrage einmal scheitert. Was passiert also beispielsweise, wenn ein Client auf den HTTP Request, eine neue Ressource zu erstellen oder einen fachlichen Prozess auszuführen, keine Antwort erhält? Während lesende Requests frei von Seiteneffekten sein sollten und im Fehlerfall daher einfach wiederholt werden können, ist das bei modifizierenden Requests nicht so einfach möglich. Denn aus Sicht des Clients lässt sich nicht zweifelsfrei feststellen, ob der initiale Request bei dem API angekommen ist und verarbeitet wurde. Ob also lediglich die Antwort verloren ging oder der Request erst gar nicht bei dem API ankam und somit noch keinerlei Aktivitäten erfolgt sind. Ein einfaches Wiederholen des Requests kann daher zu unerwünschten Nebeneffekten führen, wie zum Erzeugen von Duplikaten durch mehrmaliges Anlegen der gleichen Daten oder zum mehrmaligen Ausführen fachlicher Prozesse. Ein möglicher Lösungsansatz könnte es sein, jedem Request eine eindeutige ID hinzuzufügen. Diese ID müsste das Web-API protokollieren und speichern, sodass duplizierte Requests erkannt werden können und sichergestellt ist, dass der gleiche Request nicht mehrmals ausgeführt wird.
Ist das Erkennen von Duplikaten gelöst, schließt sich die nächste, nicht minder wichtige Frage an. Sie beschäftigt sich mit der Wiederholbarkeit fehlgeschlagener Requests. Resultiert ein modifizierender Request in einem Fehler, der auch erfolgreich an den Client übermittelt wurde, kann dieser Fehler entweder grundsätzlicher Natur sein oder nur vorübergehend auftreten. Während grundsätzliche Fehler, etwa unsinnige oder unvollständige Requests, auch im Wiederholungsfall immer abgelehnt würden, lohnt bei vorübergehenden Fehlerursachen (z. B. bei technischen Problemen) gegebenenfalls ein erneuter Versuch des identischen Requests. Zunächst sollte das Erkennen von Duplikaten zum einen so gestaltet sein, dass duplizierte Requests zugelassen sind, wenn vorherige Versuche des gleichen Requests fehlgeschlagen sind. Zum anderen sollte sichergestellt sein, dass im Fehlerfall sämtliche möglicherweise bereits erfolgten Teilschritte der Request-Verarbeitung zurückgenommen werden, damit ein wiederholter Empfang des gleichen Requests nach einem Fehlerfall wieder vom gleichen Ausgangszustand ausgehen kann. Diese komplette Rücknahme sämtlicher Teilschritte ist insbesondere in stark verteilten Architekturen, wie Microservices, oft nicht trivial, da in der Regel keine Transaktion existiert, die alle Aktivitäten umschließt und die einfach zurückgerollt werden könnte.
Schließlich ist eine Strategie festzulegen, wie fehlgeschlagene Requests zu wiederholen sind. In welchen Fehlerfällen sollen Wiederholungen überhaupt durchgeführt werden und in welchen nicht? Bei fehlgeschlagener Authentifizierung können Wiederholungen beispielsweise sogar zusätzliche Probleme schaffen: etwa dadurch, dass ein Account aufgrund mehrmaliger gescheiterter Authentifizierungsversuche gesperrt wird. Wie häufig sollten Wiederholungen maximal erfolgen oder über welchen Zeitraum, bevor der Versand eines Requests für endgültig gescheitert erklärt wird? Und wie lange sollte man nach einem gescheiterten Request pausieren, bevor man es ein weiteres Mal versucht? Ein System, das vorübergehend überlastet ist, könnten zu häufige Wiederholungen endgültig in die Knie zwingen. All diese Fragen sind nicht pauschal zu beantworten, sondern stark von der konkreten Anwendung und ihrem Umfeld abhängig. In jedem Fall sollten sich die jeweiligen Einstellungen aber konfigurieren lassen. Bei der Umsetzung solcher Strategien helfen gegebenenfalls bewährte Patterns wie Circuit Breaker [1], Bulkhead [2] und frei verfügbare Bibliotheken, wie etwa das häufig angeführte Hystrix [3] aus dem Softwarestack von Netflix.
Ein weiterer wichtiger Aspekt von Web-APIs ist das Thema Sicherheit. Wiederum sind hier nicht nur solche Schnittstellen zu betrachten, die externe Systeme anbinden. Auch die Kommunikation zwischen Systemen innerhalb eines Unternehmens oder die Kommunikation von Microservices untereinander sollte tunlichst abgesichert werden. Wer über die Sicherheit HTTP-basierter Kommunikation nachdenkt, landet unweigerlich bei SSL/TLS. SSL sollte aufgrund bekannt gewordener Schwachstellen heute nicht mehr eingesetzt werden, stattdessen kommt in der Regel TLS zum Einsatz. Dennoch ist SSL der weiter verbreitete Begriff, und meist ist TLS gemeint, wenn von SSL gesprochen wird. HTTPS (Hypertext Transfer Protocol Secure) bezeichnet die Verwendung von HTTP über SSL oder TLS.
Die Absicherung der Kommunikation umfasst unterschiedliche Aspekte. Als wichtigste Eigenschaft wird meist die Vertraulichkeit der Verbindung angesehen, die durch Verschlüsselung erreicht wird. HTTPS bietet hier auf den ersten Blick eine verhältnismäßig leicht umzusetzende Lösung an, da die Verbindung zwischen Client und Endpunkt mit SSL verschlüsselt wird. Betrachtet man jedoch gängige IT-Architekturen für produktive Systeme, sieht die Lage plötzlich etwas anders aus. Denn tatsächlich kommunizieren Clientsysteme in aller Regel nicht direkt mit dem Endpunkt eines Web-API. Stattdessen sind meist Reverse Proxies zwischengeschaltet. Diese können allerlei Aufgaben übernehmen, wie Sicherheitsfunktionen, Adressumsetzung, Caching oder Load Balancing zwischen mehreren Instanzen der Anwendung. Beim Einsatz von Reverse Proxies kommt es nun darauf an, wie diese konfiguriert sind und betrieben werden. Eine verbreitete Variante ist, dass der Reverse Proxy die Kommunikationsverbindung mit dem Client terminiert (SSL Termination). Die verschlüsselte Verbindung endet also an dieser Stelle. Der Reverse Proxy leitet die Anfrage dann mit einer neuen, separaten Verbindung zum Anwendungsserver weiter. Doch selbst wenn auch diese zweite Verbindung zwischen Reverse Proxy und Anwendungsserver wieder verschlüsselt erfolgt, sind die vom Client gesendeten Daten doch mindestens auf dem Reverse Proxy im Klartext vorhanden. Dies ist ein lohnenswerter Angriffspunkt. Selbstverständlich ist es stark von der Art der Anwendung und der Natur der versendeten Daten abhängig, ob dies ein kritisches Sicherheitsproblem ist oder eher ein zu vernachlässigendes. Tatsache ist jedoch, dass ein solches Szenario in einigen Branchen und Anwendungsfällen keine ausreichende Sicherheit bietet. Hier gilt es also abzuwägen und gegebenenfalls über Alternativen nachzudenken.
Eine Alternative könnte sein, viele Reverse Proxies, einen so genannten SSL-Pass-through-Modus zu konfigurieren. In diesem Modus wird die Verbindung mit dem Client nicht terminiert, und eingehende Requests werden folglich auch nicht entschlüsselt. Stattdessen leitet der Reverse Proxy die TCP-Pakete basierend auf seiner Konfiguration einfach an andere Server weiter. Während dieser Betriebsmodus also die Sicherheit erhöht, geht dabei andererseits die Möglichkeit verloren, die Inhalte eingehender Requests vom Reverse Proxy inspizieren zu lassen und die darauf aufbauenden inhaltsbasierten Funktionen auszuführen.
Eine andere Alternative besteht darin, nicht die Verbindung zwischen Client und Endpunkt zu verschlüsseln, sondern die Daten selbst. Erfolgt die Kommunikation beispielsweise im JSON-Format, müssten Clients in diesem Fall ein zu versendendes JSON-Konstrukt vor dem Versand verschlüsseln, und die empfangende Anwendung muss diese Daten wieder entschlüsseln. Das sorgt für Zusatzaufwände während der Entwicklung, löst aber dafür das Problem möglicher Angriffe auf dem Transportweg. Gegebenenfalls könnte man die verschlüsselten Daten auch über eine ungesicherte Verbindung versenden, also ohne den Einsatz von SSL.
Neben der Vertraulichkeit der Kommunikation ist auch die Authentifizierung ein wichtiger Sicherheitsaspekt. Darunter versteht man, dass die Identität des Kommunikationspartners überprüfbar ist. Was den Server betrifft, sind Sicherheitszertifikate ein verbreitetes und sinnvolles Mittel. Zur Identifizierung des Clients sind dagegen mehrere Varianten denkbar. Häufig werden Anmeldedaten als Bestandteil von Requests verschickt. Diese Variante ist wieder leicht umzusetzen und daher weit verbreitet. Jedoch hält sie nicht jedem Securityaudit stand. Denn bei diesem Ansatz wird ein Geheimnis, nämlich typischerweise das Passwort des Clients, über das Netzwerk verschickt. Selbst wenn dieser Versand über einen verschlüsselten Transportkanal erfolgt, gibt es Branchen und Anwendungen, in denen diese Art der Authentifizierung als nicht ausreichend sicher angesehen wird. Denn es existieren diverse Angriffsmöglichkeiten, wie Session Hijacking oder Replay-Attacken. Eine mögliche Alternative sind Tokens, bei denen Anmeldedaten und gegebenenfalls weitere charakteristische Merkmale zu versendender Requests in codierter Form eingebettet werden. Eine gängige Variante sind etwa JSON Web Tokens [4].
Eine andere Möglichkeit besteht darin, Clientzertifikate einzusetzen. Doch auch sie haben ihre eigenen Herausforderungen. So ist die Frage zu klären, wie Clientzertifikate verteilt werden, insbesondere, falls einzelne oder sogar alle Clients nicht im Vorhinein bekannt sind. Darüber hinaus wird eine Strategie benötigt, um abgelaufene Clientzertifikate zu ersetzen; und zwar idealerweise schon vor Ende ihres Gültigkeitszeitraums und nicht erst dann, wenn die Kommunikation nicht mehr funktioniert. Eine mögliche Lösung könnte sein, dass Clients die verbleibende Gültigkeitsdauer ihrer Zertifikate selbstständig in regelmäßigen Abständen prüfen und bei einer gewissen Restgültigkeitsdauer eine Mitteilung an einen geeigneten Empfänger senden.
Ein ganz anderer Aspekt, der ebenfalls bedacht werden sollte, ist die zu erwartende Kommunikation zwischen Clients und Web-API, die daraus resultierende Last auf Netzwerk und Anwendungsserver und wie sich diese gegebenenfalls reduzieren lässt, um die Gesamtperformanz zu verbessern. Nicht zuletzt lassen sich durch eine reduzierte Last meist die Betriebskosten senken. Und oft profitieren vor allem mobile Clients von effizienterer Kommunikation, da sie über schwankende Verbindungsqualität und möglicherweise über begrenztes Datenvolumen verfügen.
Wichtigster Ansatzpunkt ist hierbei der Abruf von Daten und Informationen. So sollten Clients in ihren Anfragen möglichst feingranular angeben können, an welcher Teilmenge der Gesamtdaten sie tatsächlich interessiert sind. Das kann etwa mit einer Kombination mehrerer Filterkriterien geschehen. Gegebenenfalls kann auch eine Abfragesprache zum Einsatz kommen, die es Clients erlaubt, komplexe Filter für ihre Anfragen zu definieren. Zwar könnten Clients die empfangenen Daten auch selbst filtern oder reduzieren, effizienter ist es jedoch, nur die wirklich benötigten Daten an den Client zu schicken.
Ebenso lohnt ein Blick auf Polling-Szenarien, bei denen Clients in regelmäßigen Abständen den aktuellen Stand der Daten abfragen. Beispiele hierfür sind etwa sich regelmäßig verändernde Werte wie Finanzdaten, Zwischenergebnisse von Sportereignissen, die Auslastung technischer Systeme oder der Status lang laufender Prozesse, die durch einen entsprechenden vorherigen Request angestoßen wurden. Je nachdem, wie viele Clients gleichzeitig ausgeführt werden und in welchen Abständen sie pollen, kann durch solche Requests erhebliche Last entstehen.
Um diese Last zu reduzieren, kann ein erster Schritt in der Nutzung von Caching-Mechanismen liegen, beispielsweise HTTP ETag [5]. Dabei sendet der Anwendungsserver in der Antwort zur ersten Anfrage eines Clients neben den eigentlichen Daten ein zusätzliches Header-Feld, das einen für den aktuellen Datenstand spezifischen ETag-Wert enthält. Bei einer erneuten Anfrage der gleichen Daten sendet der Client diesen ETag-Wert an den Server zurück. Nur wenn sich in der Zwischenzeit die Daten, und somit auch deren ETag-Wert, geändert haben, sendet der Server erneut den aktuellsten Stand der Daten an den Client. Haben sich die Daten jedoch nicht verändert, antwortet er mit einer leeren Antwort und dem HTTP Status Code 304 (not modified).
Mithilfe solcher Mechanismen lässt sich zumindest verhindern, dass unveränderte Daten wiederkehrend an den gleichen Client gesendet werden. Die Datenmenge wird also reduziert, die große Anzahl von Requests bleibt dagegen unverändert. Doch auch diese binden wertvolle Ressourcen, um verarbeitet und beantwortet zu werden. Um hier eine deutliche Verbesserung zu erzielen, könnte beispielsweise das Kommunikationsmuster derart geändert werden, dass Clients nur einmalig oder zumindest deutlich seltener Daten anfordern und anschließend vom Server immer dann informiert werden, wenn Datenänderungen oder bestimmte Ereignisse vorliegen. Mit einer solchen Strategie kommt deutlich weniger Kommunikation zustande. Im Wesentlichen immer nur dann, wenn es tatsächlich etwas mitzuteilen gibt.
Für webbasierte Clients oder Apps bieten sich zur Umsetzung beispielsweise Long Polling, Server-sent Events [6] oder ein Protokollupgrade auf WebSocket an. Zwar vertreten manche Entwickler die Ansicht, man könne beim Einsatz solcher Kommunikationsansätze eigentlich nicht mehr von einem reinen Web-API sprechen, doch wie eingangs erwähnt, soll der Begriff im Kontext dieses Artikels weiter gefasst sein.
Im Fall einer Server-zu-Server-Kommunikation bieten sich alternativ Web Hooks an. Dabei handelt es sich um ein nicht standardisiertes Verfahren, das jedoch unter anderem von einigen populären Diensten wie GitHub oder Slack angeboten wird. Hierbei stellt die anfragende Anwendung einen URL bereit, den die datenliefernde Anwendung kontaktieren kann, wenn Datenänderungen oder Ereignisse vorliegen. Das bedeutet, dass in diesem Fall das Web-API den Kommunikationskanal zur Übermittlung geänderter Daten initiiert.
Ein weiterer wichtiger Aspekt bei der Entwicklung von Web-APIs betrifft die Frage, wie sichergestellt wird, dass die Schnittstelle im Fall neuer Anforderungen möglichst einfach weiterentwickelt werden kann. Im Grunde stellen sich hier die gleichen grundsätzlichen Herausforderungen wie bei allen Schnittstellen. Sobald eine Schnittstelle veröffentlicht ist und von Clients verwendet wird, ist sie nicht mehr so leicht zu ändern. Denn zumindest solche Änderungen, die nicht rückwärtskompatibel sind, erfordern gleichzeitig eine Änderung oder den Austausch der Clients. Das lässt sich noch einigermaßen bewältigen, solange das API und seine Clients vom gleichen Team oder wenigstens im gleichen Unternehmen entwickelt und betrieben werden. Stehen die Clients jedoch nicht unter eigener Kontrolle, sondern werden beispielsweise von Geschäftspartnern entwickelt, sind nicht kompatible Änderungen des API in aller Regel recht schmerzhaft für alle Beteiligten. Denn es müssen gleichzeitige Deployments unterschiedlicher Systeme koordiniert werden, und das über Unternehmensgrenzen hinweg. Praktisch unmöglich wird dieses Unterfangen, wenn nicht einmal sämtliche Clients oder deren Entwickler bekannt sind. Das kann geschehen, wenn ein Web-API keinen geschlossenen Benutzerkreis hat, sondern der Öffentlichkeit zur Verfügung steht.
Ein verbreiteter Lösungsansatz besteht in der Einführung einer Versionierungsstrategie für die Schnittstelle. Neue Versionen werden eindeutig gekennzeichnet, angekündigt und dokumentiert. Clients müssen in ihren Anfragen anzeigen, welche Version des Web-API sie ansprechen möchten. Es gibt unterschiedliche Ansätze, versionierte APIs in der Praxis umzusetzen. Eine Möglichkeit besteht darin, unterschiedliche Versionen des API jeweils auf einem separaten Endpunkt oder einem separaten URL bereitzustellen. Das bedeutet allerdings einen entsprechenden Mehraufwand im Betrieb und wirft die Frage auf, wie viele ältere Versionen man eigentlich anbieten möchte und wie lange. Darüber hinaus widerspricht die Strategie mit separaten URLs dem REST-Prinzip, dass jede Ressource einen eindeutigen URL haben soll. Sofern man also das Ziel verfolgt, ein RESTful Web-API zu implementieren, scheidet dieser Ansatz aus. Alternativ dazu kann man mehrere Versionen auch auf dem gleichen Endpunkt und unter dem gleichen URL betreiben. In diesem Fall könnten Clients beispielsweise durch einen speziellen Header oder einen Parameter in ihren Requests anzeigen, welche Version des API sie ansprechen möchten. Nach Empfang eines Requests müsste das Web-API dann entsprechend verzweigen.
Eine alternative Strategie besteht darin, gar keine Versionierung einzuführen. So gibt es zahlreiche Stimmen, die darin weniger eine Lösung als vielmehr das Schaffen neuer Probleme erkennen. Anstelle also größere Anstrengungen in eine Versionierungsstrategie zu investieren, könnte man stattdessen überlegen, wie das Web-API zu entwerfen ist, damit notwenige Änderungen möglichst rückwärtskompatibel sind. Erweiterungen eines existierenden API, also zusätzliche Daten und Operationen, lassen sich in aller Regel rückwärtskompatibel umsetzen. Gegebenenfalls fehlende Daten müssten lediglich durch sinnvolle Standardwerte ersetzt werden. Probleme schaffen dagegen in der Regel nur Breaking Changes, wie Änderungen an existierenden Operationen und Daten, also Umbenennungen, Entfernungen oder abweichende Funktionalität. Die Kunst besteht also darin, das Web-API von Beginn an so zu entwerfen, dass es leicht erweiterbar ist, und dass neue Anforderungen möglichst keine Änderungen an bestehender Funktionalität erfordern. Doch ganz gleich, ob die Wartbarkeit der Schnittstelle nun mit oder ohne Versionierungsstrategie erreicht werden soll, in jedem Fall ist es wichtig, sich über diese Fragestellung Gedanken zu machen, bevor das Web-API veröffentlicht wird. Denn nachträglich lässt sich weder eine Versionierung leicht einführen noch ein API so umgestalten, dass es leichter erweiterbar ist. Beides sollte von Beginn an gegeben sein.
Schließlich sollte ausreichend Zeit eingeplant und verwendet werden, um sicherzustellen, dass das eigene Web-API leicht zu benutzen ist. Es sollte einfach verständlich sein, also sinnvolle Namen und Bezeichnungen verwenden, und nach Möglichkeit ganz natürlich zur Nutzung unterschiedlicher Operationen in einer sinnvollen Reihenfolge hinführen. Eine aktuelle, ausreichend detaillierte und leicht zugängliche Dokumentation sollte selbstverständlich sein. Die Praxis zeigt, dass Entwicklerteams, die ein Web-API bereitgestellt haben, häufig zahlreiche Supportanfragen von Entwicklern der Clientsysteme zu bewältigen haben. Je besser die vorstehenden Aufgaben erfüllt sind, je leichter sich das Web-API also nutzen lässt, desto weniger solcher Anfragen sind zu erwarten. Eine gute Benutzbarkeit und Dokumentation liegen daher vor allem auch im Interesse des Web-API-Betreibers.
Web-APIs eigenen sich hervorragend, um Integrationslösungen umzusetzen, sowohl für die Integration unterschiedlicher Systeme als auch für Kommunikation innerhalb ein und desselben Systems, etwa im Fall einer Microservices-Architektur. Zu den Vorteilen des Ansatzes zählen der Einsatz bewährter Technologien wie HTTP und SSL, die schnelle Umsetzbarkeit erster Prototypen, vergleichsweise leichtes Debugging und die breite Verfügbarkeit von Erfahrungen, Leitfäden und Tools. Während die Erstellung eines Web-API mit aktuellen Frameworks recht schnell von der Hand geht, wird jedoch leicht übersehen, dass die Entwicklung eines robusten, sicheren und wartbaren Web-API das Entwicklerteam durchaus vor einige Herausforderungen stellt. Wichtige Fragen sind zu klären, weitreichende Entscheidungen zu treffen. Diese betreffen die resultierende enge Kopplung von Systemen, Fehlerbehandlung und Wiederholbarkeit von Requests, das Sicherheitskonzept, die Begrenzung der Last oder auch die Frage, wie ein Web-API wartbar bleibt, sodass eine Weiterentwicklung nicht notwendigerweise zum Austausch aller existierenden Clients führen muss. Das Erstellen von Web-APIs birgt also durchaus einige Fallstricke, zu deren Umschiffung dieser Artikel hoffentlich ein wenig beitragen konnte.
Thilo Frotscher arbeitet als freiberuflicher Softwarearchitekt und Trainer mit den technischen Schwerpunkten Enterprise Java und Systemintegration. Er unterstützt Entwicklerteams durch die praktische Mitarbeit in Projekten und mit der Durchführung von maßgeschneiderten Schulungen.
[1] Fowler, Martin: „Circuit Breaker“: https://martinfowler.com/bliki/CircuitBreaker.html
[2] Nygard, Michael T.: „Release It!: Design and Deploy Production-Ready Software“, O’Reilly
[3] Hystrix: https://github.com/Netflix/Hystrix
[4] JSON Web Token: https://jwt.io/
[5] HTTP ETag: https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19
[6] Server-sent Events: https://www.w3.org/TR/eventsource/