Was Teil 2 als Trennung zwischen Entscheidung und Vollzug eingerichtet und Teil 3.1 mit den vier Übergangsthemen vorbereitet hat, wird hier konkret. Der REST-Adapter aus dem Modul security-rest steht als zweite Anbindung für den Security-Kern vor uns. Drei kleine Übersetzerklassen, zwei Filter, eine Subject-Brücke. Mehr leistet er nicht, und mehr muss er nicht leisten, weil der Kern den Rest übernimmt.
Drei Kapitel führen durch diese Implementierung: zunächst die Symmetrie zur Vaadin-Trias aus Teil 2, dann die Trennung der beiden Filter, schließlich die Subject-Brücke aus dem Bearer-Token. Was bleibt, ist der Übergang zum Konsumenten dieses Adapters — dem zweiten Vaadin-Demo, das Teil 3.3 behandelt.
Sämtliche im Folgenden vorgestellten Quelltexte sind auf GitHub veröffentlicht und
unter https://3g3.eu/vaadin-security abrufbar.
Der REST-Adapter im Detail: drei Klassen, eine Symmetrie
Wer die drei Übersetzerklassen aus Teil 2 vor Augen hat — VaadinAccessContextFactory, VaadinAccessDecisionMapper, VaadinSessionSubjectStore — kennt das Muster, das Kapitel 5 erzählt, bereits. Der REST-Adapter führt es ohne Überraschung fort. Drei Klassen, drei Aufgaben, jede in einem einzigen Satz beschreibbar. Die RestAccessContextFactory erstellt den AccessContext aus einer HTTP-Anfrage. Der HttpStatusDecisionMapper übersetzt eine AuthorizationDecision in einen HTTP-Status. Der RestSubjectResolver löst das Subject aus der Anfrage auf. Wer Teil 2 verstanden hat, sieht sofort, welcher Vaadin-Klasse welche Klasse entspricht — und welche Stelle die Symmetrie ehrlich nicht trägt, weil REST und Vaadin strukturell an einem Punkt unterschiedlich sind.

Abbildung 1
Abbildung 1: Symmetrie der zwei Adapter-Triaden. Der Vaadin-Adapter aus Teil 2 und der REST-Adapter aus Teil 3 münden auf dieselben drei Kerntypen insecurity-core. Die strukturelle Parallele wird an den drei Pfeilpaaren sichtbar.
Die RestAccessContextFactory ist der direkte Verwandte der VaadinAccessContextFactory aus Teil 2. Dort baute eine Methode create(BeforeEnterEvent) aus dem Vaadin-Event einen AccessContext mit Pfad, Zielklasse und einer offenen Attributes-Map. Hier baut eine Methode create(RestRequest, Optional<SecuritySubject>, String, Map<String, Object>) aus der Anfrage, dem aufgelösten Subject, einem Operation-Namen und zusätzlichen Attributes einen AccessContext der heutigen, fünfteiligen Form:
public AccessContext create(
RestRequest request,
Optional<SecuritySubject> subject,
String operation,
Map<String, Object> attributes) {
Map<String, Object> contextAttributes = new LinkedHashMap<>(
attributes == null ? Map.of() : attributes);
contextAttributes.put("method", request.method());
contextAttributes.put("path", request.path());
contextAttributes.put("queryParameters", Map.copyOf(request.queryParameters()));
return new AccessContext(
subject,
"rest-endpoint",
request.path(),
operation,
contextAttributes);
}
Die Klasse hat genau eine öffentliche Methode und keine eigenen Felder. Sie liest die HTTP-Methode, den Pfad und die Query-Parameter aus der Anfrage, fügt sie der übergebenen Attributes-Map hinzu, setzt den resourceType auf rest-endpoint, übernimmt den Pfad als resourceName und führt die Operation durch. Das Subjekt kommt vorgefertigt von außen herein — der Aufrufer hat es bereits aufgelöst. Diese letzte Eigenschaft ist der einzige nennenswerte Unterschied zur Vaadin-Variante: Im Vaadin-Adapter aus Teil 2 spielte das Subject im Kontext keine Rolle, weil der Evaluator es bei Bedarf über den SubjectStore selbst beschaffte; hier ist es ein Pflichtparameter, weil die HTTP-Welt keinen statischen Session-Accessor kennt und der Aufrufer das Subject ohnehin schon aus dem Token aufgelöst hat.
Der zweite Bestandteil der Trias ist der HttpStatusDecisionMapper. Sein Pendant aus Teil 2, der VaadinAccessDecisionMapper, hatte fünf Switch-Zweige zu bedienen, weil AccessDecision fünf Varianten kennt. Der HTTP-Mapper hat drei Zweige, weil AuthorizationDecision drei Varianten kennt:
public boolean apply(AuthorizationDecision decision, RestResponse response) {
return switch (decision) {
case AuthorizationDecision.Granted() -> true;
case AuthorizationDecision.Unauthenticated(String ignored) -> {
response.status(401);
response.body("Unauthorized");
yield false;
}
case AuthorizationDecision.Forbidden(String ignored) -> {
response.status(403);
response.body("Forbidden");
yield false;
}
};
}
Granted gibt true zurück und überlässt den Aufruf der Handler-Methode dem Filter. Unauthenticated gibt 401 mit dem generischen Body „Unauthorized“ und false zurück. Forbidden setzt den Statuscode 403, verwendet den generischen Body „Forbidden“ und gibt ebenfalls false zurück. Der reason-String aus den beiden Decision-Varianten wird mit dem Variablennamen ignored quittiert — er wird bewusst nicht weitergereicht, weil eine HTTP-Antwort an einen anonymen Anrufer keine Diagnoseinformation enthalten soll. Die Demo-Tests im Modul demo-rest belegen diese Disziplin sogar explizit, indem sie nicht nur den Statuscode prüfen, sondern auch sicherstellen, dass der Body weder das Wort „Exception“ noch interne Paketnamen enthält.
Die dritte Stelle der Trias ist der RestSubjectResolver, und sie ist die Stelle, an der die Symmetrie zur Vaadin-Welt aus Teil 2 ehrlich nicht durchgezogen wird. Im Vaadin-Adapter ist der Subject-Begriff durch eine konkrete Implementierung im Modul security-vaadin vertreten — den VaadinSessionSubjectStore, der die VaadinSession als Speicher verwendet und als SubjectStore-Implementierung in META-INF/services registriert ist. Der REST-Adapter kann diese Form nicht einfach übernehmen, weil HTTP zustandslos ist und keine Session-Analogie kennt, die die Bibliothek vorgeben dürfte. Manche Anwendungen lesen Bearer-Tokens, andere Cookies, andere validieren JWT-Strukturen mithilfe von Public-Key-Kryptographie. Die Bibliothek darf keine dieser Strategien erzwingen.
Aus dieser Beobachtung ergibt sich die Form des RestSubjectResolvers. Er ist kein Speicher und auch kein vollständig im Adapter lebender Übersetzer; er ist ein Vertrag. Die einzige Methode resolveSubject(RestRequest) gibt ein Optional<SecuritySubject> zurück. Die Implementierung liegt in der Anwendung. Die Bibliothek bietet einen kleinen Helfer für den häufigsten Fall — den BearerTokenExtractor, der den Authorization-Header parst und den Token-Wert niemals protokolliert —, aber sie zwingt niemanden, ihn zu verwenden. Das demo-rest-Modul implementiert mit DemoSubjectResolver eine Token-basierte Variante, die genau diesen Extractor nutzt; eine andere Anwendung würde einen anderen Resolver schreiben, ohne dass eine Zeile im Adapter selbst geändert werden müsste. Die Asymmetrie zwischen Vaadin und REST ist hier kein Mangel, sondern die korrekte Abbildung der fachlichen Asymmetrie zwischen den beiden Welten.
Damit ist der REST-Adapter in seinem Inneren beschrieben. Drei Klassen, drei Aufgaben, eine Symmetrie mit einer ehrlichen Ausnahme. Mehr leistet er nicht, und mehr muss er nicht leisten, weil der Kern den Rest übernimmt. Was bleibt, ist die Frage, wie diese drei Klassen in einem Filter zusammengeführt werden — und warum security-rest zwei Filter statt eines anbietet. Diese Frage greift Kapitel 6 auf.
Zwei Filter, zwei Antworten
Ein einzelner Filter könnte alle vorhandenen Endpunkte der Demo bedienen — er würde das Subject auflösen, eine vorhandene Annotation suchen, eine Decision treffen und in einen HTTP-Status umsetzen. Wenn keine Annotation gefunden wird, würde er den Handler einfach durchlassen. Das wäre konsequent, einfach und ausreichend. security-rest bietet trotzdem zwei Filter an. Diese Doppelung ist keine Verschwendung; sie ist die konsequente Übertragung eines Musters, das in Teil 1 bereits sichtbar war — die Trennung der beiden Vaadin-Listener MyLoginListener und AuthorizationListener, die zwei strukturell verschiedene Fragen beantworten und deshalb auch in zwei eigenständigen Klassen leben.
Die erste Frage lautet: Ist überhaupt ein authentifiziertes Subject vorhanden? Sie ist binär. Sie braucht keine Annotation, keine Permission und keinen Evaluator. Wer sie stellt, will einen Endpunkt schützen, der jedem authentifizierten Benutzer offensteht — Stamm-Endpunkte wie /api/me, /api/logout, /api/operations, die für jeden eingeloggten Benutzer dieselbe Antwort liefern, aber für einen anonymen Aufruf ein 401 zurückgeben müssen. Der RestAuthenticationFilter ist die präzise Antwort auf diese Frage. Seine Methode requireAuthenticated(RestRequest, RestResponse, RestHandler) ruft den RestSubjectResolver auf, prüft das Ergebnis und leitet entweder den Handler weiter oder setzt einen 401 mit dem generischen Body Unauthorized. Mehr leistet er nicht, und mehr leistet er bewusst nicht — weil der Aufrufer, der diesen Filter verwendet, ohnehin keine Permission prüfen will.
Die zweite Frage lautet: Trägt der Aufrufer die geforderte Permission oder Rolle? Sie ist nicht binär; sie ist annotation- und subject-abhängig. Sie braucht den Scanner aus Kapitel 4, einen Evaluator aus dem Kern und den Mapper aus Kapitel 5. Der RestAuthorizationFilter ist die Antwort auf diese Frage, und seine zentrale Methode bringt die in den vorigen Kapiteln einzeln behandelten Bausteine in eine kompakte Sequenz:
public void authorizeAndHandle(
RestRequest request,
RestResponse response,
RestHandler handler,
AnnotatedElement securedElement,
String operation,
Map<String, Object> attributes) {
var pair = scanner.scan(securedElement);
if (pair.isEmpty()) {
handler.handle(request, response);
return;
}
Optional<SecuritySubject> subject = subjectResolver.resolveSubject(request);
AccessContext context = contextFactory.create(request, subject, operation, attributes);
AuthorizationDecision decision = evaluate(pair.get().evaluatorClass(), context, pair.get().annotation());
if (decisionMapper.apply(decision, response)) {
handler.handle(request, response);
}
}
Drei Phasen, jeweils einer kleinen Klasse aus Kapitel 5 zugeordnet. Zuerst die Auflösung — der SecurityAnnotationScanner findet das Annotation-Evaluator-Paar; ist keines vorhanden, läuft der Handler ohne weitere Prüfung. Dann die Entscheidung — der RestSubjectResolver löst das Subject auf, die RestAccessContextFactory baut den AccessContext, und der instanziierte AuthorizationEvaluator liefert eine AuthorizationDecision. Schließlich der Vollzug — der HttpStatusDecisionMapper entscheidet, ob der Handler laufen darf, und schreibt im Negativfall den Statuscode bereits in die Response. Wer die Sequenz mit dem geschrumpften AuthorizationListener.beforeEnter aus Teil 2 vergleicht, findet dieselbe Dreiphasigkeit — Auflösung, Entscheidung, Vollzug — in einer leicht anderen, aber strukturell identischen Form.
Eine Eigenschaft des Filters verdient besondere Erwähnung. RestAuthorizationFilter bietet zwei Überladungen von authorizeAndHandle an. Die kürzere, ohne Operation und Attributes, reicht intern an die ausführliche weiter und setzt dort die HTTP-Methode in Kleinbuchstaben als Operation und eine leere Map als Attributes. Damit funktioniert ein Aufruf wie der von filter.authorizeAndHandle(request, response, handlers::deleteDocument, deleteDocumentMethod) ohne weitere Angaben, und der Evaluator erkennt im Kontext eine Operation mit dem Wert delete — was für die meisten Anwendungen die richtige Vorgabe ist. Wer eine andere Granularität benötigt — etwa eine logische Operation, die mehrere HTTP-Methoden zusammenfasst —, nutzt die ausführliche Variante.
Wo beide Filter zusammenwirken, wird die Trennung lesbar. Der DemoHttpRouter.dispatch zeigt sie nebeneinander in einem einzigen Switch-Block:
if (DemoEndpoints.ME.equals(path) && "GET".equals(method)) {
authenticationFilter.requireAuthenticated(request, response, handlers::me);
return;
}
if (DemoEndpoints.OPERATIONS.equals(path) && "GET".equals(method)) {
authenticationFilter.requireAuthenticated(request, response, handlers::operations);
return;
}
if (DemoEndpoints.LOGOUT.equals(path) && "POST".equals(method)) {
authenticationFilter.requireAuthenticated(request, response, handlers::logout);
return;
}
if (DemoEndpoints.DOCUMENTS.equals(path)) {
switch (method) {
case "GET" -> filter.authorizeAndHandle(request, response, handlers::listDocuments, listDocumentsMethod);
case "POST" -> filter.authorizeAndHandle(request, response, handlers::createDocument, createDocumentMethod);
default -> notAllowed(response);
}
return;
}
if (path.startsWith(DemoEndpoints.DOCUMENT_BY_ID) && "DELETE".equals(method)) {
filter.authorizeAndHandle(request, response, handlers::deleteDocument, deleteDocumentMethod);
return;
}
Die obere Hälfte des Ausschnitts ruft den authenticationFilter auf, die untere die Variable filter — eine Instanz des RestAuthorizationFilters. Die Wahl ist auf einen Blick erkennbar: Wer keine Permission benötigt, geht durch den schmalen Filter; wer eine Permission benötigt, geht durch den vollen. Der Login-Endpunkt steht in derselben Klasse wie diese Auswahl und ruft seinen Handler ohne jeden Filter auf — er muss anonym erreichbar sein, weil hier das Subject erst entsteht. Diese drei Stufen — kein Filter, schmaler Filter, voller Filter — werden in der Endpoint-Tabelle aus docs/demo-rest-anzeige.md zusammengefasst und dort in derselben Reihenfolge wie im Code aufgeführt.
Damit ist die Frage geklärt, warum es zwei Filter statt eines gibt. Ein einzelner Filter müsste an jedem Endpunkt entscheiden, ob er nur authentifizieren oder auch autorisieren soll. Diese Entscheidung wäre entweder in den Filter selbst eingebaut — was ihn unnötig komplex machen würde — oder an jedem Aufruf zu wiederholen — was die Lesbarkeit zerstören würde. Zwei Filter sind die sauberere Trennung, und sie spiegeln zudem das Listener-Muster aus Teil 1 wider, das dieselbe Trennung im Vaadin-Adapter bereits durchgezogen hat. Was bleibt, ist die Frage, wie das Subject — das sowohl der Authentication- als auch der Authorization-Filter benötigt — überhaupt ins System gelangt ist und wer es zwischen UI und Endpunkt trägt. Diese Frage greift auf Kapitel 7 mit dem Bearer-Token und der Subject-Brücke zurück.
Subject-Brücke: der Bearer-Token zwischen UI und Endpunkt
Wer den RestSubjectResolver aus Kapitel 5 als Vertrag im Kopf hat, weiß, dass die Bibliothek die Form der Subject-Auflösung bewusst offen lässt. Eine Anwendung mag das Subject aus einem Cookie lesen, aus einem JSON Web Token mit Public-Key-Validierung, aus einer Session-Tabelle in der Datenbank oder aus einem extern geprüften Identity-Provider beziehen — der Vertrag schreibt keine Strategie vor. Die im Repository ausgelieferte Demo trifft eine konkrete Wahl, weil sie eine konkrete Wahl treffen muss: einen schmalen Bearer-Token im Authorization-Header. Diese Wahl ist die heute übliche Form für serverseitige REST-Schnittstellen, und sie hat den Vorteil, dass sie sich mit zwei kleinen Klassen vollständig beschreiben lässt — einer im Kern, einer in der Demo.
Die kernseitige Klasse heißt BearerTokenExtractor und lebt im Modul security-rest. Sie bietet eine einzige öffentliche Methode an, die das Subject nicht auflöst, sondern nur den Token-Wert aus dem Header zieht. Diese strikte Trennung — Extraktion hier, Auflösung dort — ist die eigentliche Pointe der Klasse. Die innere Logik passt in wenige Zeilen:
private static Optional<String> parse(String header) {
String trimmed = header.trim();
int firstSpace = trimmed.indexOf(' ');
if (firstSpace < 0) return Optional.empty();
String scheme = trimmed.substring(0, firstSpace);
if (!BEARER.equals(scheme.toLowerCase(Locale.ROOT))) return Optional.empty();
String token = trimmed.substring(firstSpace + 1).trim();
return token.isEmpty() ? Optional.empty() : Optional.of(token);
}
Drei Eigenschaften sind hervorzuheben. Erstens prüft die Methode den Scheme-Namen Bearer case-insensitiv, weil verschiedene Clients ihn unterschiedlich schreiben — manche Bearer, manche bearer, manche BEARER. Zweitens trimmt sie den Token-Wert beidseitig und verwirft einen leeren Token; ein Header mit Whitespace nach dem Scheme ist daher semantisch dasselbe wie ein fehlender Header. Drittens — und das ist die Eigenschaft, die im Class-JavaDoc explizit festgehalten ist — protokolliert die Klasse den Token-Wert niemals. Im gesamten Modul security-rest gibt es weder eine Logger.debug-Anweisung noch eine toString-Methode, die einen Token-Wert ausgibt. Wer den Code übernimmt, erbt diese Eigenschaft ohne weiteres Zutun.
Die anwendungsseitige Klasse heißt in der Demo DemoSubjectResolver und ist eine konkrete Implementierung des Vertrags aus Kapitel 5. Ihre zentrale Methode ist eine kompakte Methodenkette aus drei Schritten:
public Optional<SecuritySubject> resolveSubject(RestRequest request) {
return extractToken(request).flatMap(tokens::resolve).map(this::toSubject);
}
private SecuritySubject toSubject(DemoUser user) {
RoleName roleName = user.role().roleName();
Set<PermissionName> permissions = mapping.permissionsFor(roleName);
return new SecuritySubject(user.username(), user.displayName(),
Set.of(roleName), permissions);
}
Schritt eins ist die Tokenextraktion mit dem BearerTokenExtractor. Schritt zwei ist die Auflösung des Tokens in einen DemoUser über den DemoTokenStore — eine schmale, in-memory-gehaltene Map, die jedem ausgegebenen Token einen Benutzer zuordnet und sich bei Bedarf invalidieren lässt. Schritt drei baut aus dem Benutzer einen SecuritySubject in der in security-core definierten Form, indem die Rolle des Benutzers und die für diese Rolle zuständigen Permissions zusammengeführt werden — das DemoRolePermissionMapping aus Kapitel 4 übernimmt hier die Übersetzung. Jeder einzelne Schritt ist austauschbar; eine Anwendung mit JWT-Validierung würde den Token-Store durch eine signaturprüfende Variante ersetzen, eine Anwendung mit externer Identitätsverwaltung würde das toSubject durch einen Aufruf an einen Identity-Service ersetzen.
Wo der Token entsteht, ist die zweite Hälfte der Brücke, und sie lebt im Login-Endpunkt. Der DemoHandlers.login prüft den Benutzernamen und das Kennwort gegen einen In-Memory-Store, ruft im Erfolgsfall tokenStore.issue(u) auf und gibt den erzeugten Token in der JSON-Antwort zurück. Auffällig ist eine kleine Disziplin im Code: Nach der Token-Ausgabe ruft der Handler den DemoSubjectResolver selbst gegen einen synthetischen Request mit dem frisch ausgegebenen Token auf, um den SecuritySubject zu erzeugen, dessen Rollen- und Permission-Liste in derselben Antwort mitgeliefert wird. Damit erhält der Client das Token und das Subject-Profil in einer einzigen Antwort und muss nicht zwei separate Aufrufe absetzen.
Auf der anderen Seite des Lebens steht der Logout. Der DemoHandlers.logout ruft DemoSubjectResolver.extractToken(request).ifPresent(tokenStore::revoke) und gibt eine kurze Bestätigung zurück. Der Aufruf ist zweimal interessant. Erstens wird die statische extractToken-Hilfsmethode im DemoSubjectResolver für genau diesen Zweck öffentlich verfügbar gemacht, statt den BearerTokenExtractor an dieser Stelle erneut zu instanziieren. Zweitens ruft der Handler tokenStore.revoke(…), wodurch alle weiteren Aufrufe mit demselben Token in der Auflösung scheitern und der Authentication-Filter ein 401-Response setzt. Der Logout ist damit kein UI-Vorgang, sondern eine serverseitige Token-Invalidierung — eine Eigenschaft, die das zweite Vaadin-Demo aus dem nächsten Kapitel respektiert.

Abbildung 2
Abbildung 2: Lebenszyklus eines Bearer-Tokens vom Login über die Nutzung bis zum Logout. Vier Beteiligte — Vaadin-UI, REST-Endpunkt,DemoTokenStoreundClientSecurityContext— wirken in drei semantisch klar getrennten Phasen zusammen.
Wer in dieser Brücke aus Token und Auflösung das Pendant zum VaadinSessionSubjectStore aus Teil 2 sucht, findet es an einer überraschenden Stelle. Im demo-vaadin-rest-client, dem zweiten Vaadin-Demo des Repositorys, lebt der Token nicht in einer Cookie- oder Header-Brücke, sondern im ClientSecurityContext — einer schmalen Klasse, die den SubjectStore-Vertrag aus Teil 2 implementiert und intern einen Token und einen RemoteUser-Snapshot speichert. Die Vaadin-UI nutzt also weiterhin den SubjectStore aus dem Vaadin-Adapter, nur dass dessen Inhalt nicht mehr aus einem lokalen Login stammt, sondern aus dem Login gegen das REST-Backend. Der Subject-Begriff bleibt dadurch in der Vaadin-Welt unverändert; was sich ändert, ist allein die Quelle. Diese Konstruktion ist eine der eleganten Eigenschaften des zweiten Demos und wird in Kapitel 8 ausführlich behandelt.
Damit ist die Subject-Brücke zwischen UI und Endpunkt vollständig beschrieben. Der Bearer-Token wird im Login erzeugt, im Authorization-Header transportiert, von einem schmalen Extractor aus dem Header gelesen und von einem applikations-spezifischen Resolver in einen SecuritySubject übersetzt — derselben Form, die der Vaadin-Adapter aus Teil 2 in der VaadinSession führt. Der Kern weiß weiterhin nichts von Tokens, Headern oder Sessions; er sieht nur Subjects. Was bleibt, ist der Übergang vom REST-Adapter selbst zur zweiten Vaadin-Demo, die diese Mechanik nutzt. Diese Demo ist Kapitel 8 gewidmet.

Damit ist der REST-Adapter in seinem Inneren beschrieben. Drei Klassen, drei Aufgaben, eine Symmetrie mit einer ehrlichen Ausnahme; zwei Filter, die zwei strukturell verschiedene Fragen beantworten; und eine schmale Subject-Brücke aus dem Bearer-Token. Was bleibt, ist der Übergang vom REST-Adapter selbst zur zweiten Vaadin-Demo, die diese Mechanik nutzt. Diese Demo und die abschließende Bilanz sind Teil 3.3 gewidmet.