Was Teil 4.1 als Fundament gelegt hat — die Bootstrap-Diagnose, die anwendersichtige Vaadin-SetupView und der adapter-neutrale InitialAdminBootstrapService — wird hier konkret. Der Service hatte im Konstruktor sieben Beziehungen aufgelistet, von denen drei strukturelle Bedeutung haben und in den folgenden drei Kapiteln jeweils ihren eigenen Schwerpunkt erhalten.
Sämtliche im Folgenden vorgestellten Quelltexte sind auf GitHub veröffentlicht und
unter https://3g3.eu/vaadin-security abrufbar.
Das erste dieser drei Kapitel behandelt die Token-Mechanik — woher das Bootstrap-Token stammt, in welchen drei Modi es existiert und wie es nach erfolgreicher Anlage wieder verschwindet. Das Zweite behandelt den PasswordHasher mit seiner PBKDF2-HMAC-SHA256-Implementierung und die durch alle Schichten getragene char[]-Disziplin, die in der Vaadin-Welt eine bewusst gesetzte Grenze darstellt. Das Dritte behandelt den AdministratorAccountStore-Vertrag und seine beiden Implementierungen — eine Vaadin-spezifische und eine REST-spezifische —, die gemeinsam die Brücke zwischen Bootstrap und dem konkreten Benutzermodell bilden. Was nach diesem Subteil bleibt, sind die vier konkreten Adapter selbst, die Teil 4.3 behandeln.
Token-Mechanik: drei Modi, ein Lebenszyklus
Das Token, das im createInitialAdmin aus Kapitel 3 als selbstverständliche Voraussetzung auftaucht, lebt nicht in der Methode, die es prüft. Es lebt in einer eigenen Mechanik, die beim Hochfahren des Servers entscheidet, ob ein Token erzeugt werden muss, wo es gespeichert wird und wie es dem Operator mitgeteilt wird. Diese Mechanik umfasst drei Modi, die den drei realistischen Betriebsformen einer Anwendung entsprechen — der lokalen Entwicklung, dem Container-Deployment und der produktiven Übernahme der Bereitstellung durch ein externes System.
Der BootstrapMode aus dem security-core benennt die drei Modi mit drei Aufzählungswerten. TRANSIENT_CONSOLE ist der Demo- und Entwicklungsmodus: Bei jedem Start wird ein frisches Token erzeugt, zur Server-Konsole ausgegeben, und mit dem Prozess-Ende verschwindet es wieder. PERSISTENT_FILE ist der Container-taugliche Modus: Das Token wird in eine Datei geschrieben, deren POSIX-Rechte auf 0600 gesetzt werden, und überlebt einen Neustart, sofern das Bootstrap noch nicht abgeschlossen ist. DISABLED ist die bewusste Abschaltung: Es wird kein Token erzeugt, kein Setup-Endpunkt ist aktiv, und das System geht davon aus, dass der erste Administrator durch andere Mittel — etwa eine Datenbank-Migration oder ein Konfigurationsmanagement — bereits angelegt ist.
Welcher dieser drei Modi für eine Vaadin-Anwendung angemessen ist, hängt von der jeweiligen Betriebsform ab. Eine demo-vaadin-Anwendung in der lokalen Entwicklung läuft mit TRANSIENT_CONSOLE, weil der Entwickler die Konsole vor sich hat und das Token unmittelbar lesen kann. Eine demo-vaadin-rest-client-Anwendung in einem Docker-Container nutzt PERSISTENT_FILE, weil die Konsole im Container zwar prinzipiell sichtbar ist, aber bei einem Neustart die Token verloren ginge — und weil die Token-Datei über ein Volume persistiert werden kann. Eine produktive Anwendung mit eigener Benutzerverwaltung wählt DISABLED, weil sie ihre Administratoren über andere Mechanismen einrichtet und die Bootstrap-Mechanik selbst eine unerwünschte Angriffsfläche darstellt. Diese drei Empfehlungen sind nicht neu — sie folgen der allgemeinen Logik der Trennung zwischen Entwicklung, Staging und Produktion —, aber sie werden in der Bibliothek explizit benannt und mit Konfigurationen versehen.
Das Hochfahren der Mechanik übernimmt eine einzige Methode beim Server-Start: BootstrapStartup.initializeIfRequired. Sie ist der einzige Eintrittspunkt, der die drei Modi koordiniert, das Token bei Bedarf erzeugt oder lädt, und das Operator-Banner ausgibt:
public static void initializeIfRequired(
BootstrapStateService stateService,
BootstrapTokenStore tokenStore,
BootstrapTokenGenerator generator,
BootstrapTokenOutput output,
BootstrapConfiguration configuration) {
if (stateService.mode() == BootstrapMode.DISABLED && !stateService.hasAdministrator()) {
throw new IllegalStateException(
"Bootstrap is DISABLED but no administrator account exists. "
+ "The application would be unusable in this state. ...");
}
if (!stateService.bootstrapRequired()) return;
BootstrapToken token = tokenStore.load()
.filter(existing -> !existing.isExpired(java.time.Instant.now(), configuration.tokenValidity()))
.orElseGet(() -> {
BootstrapToken fresh = generator.generate();
tokenStore.save(fresh);
return fresh;
});
output.emit(token, configuration);
}
Die ersten Zeilen beschreiben einen Hard Guard — eine bewusste Schutzmaßnahme gegen die häufigste Fehlkonfiguration. Wenn der Modus DISABLED ist, aber kein Administrator vorhanden ist, kann die Anwendung von niemandem mehr betreten werden, und niemand kann diesen Zustand korrigieren. Statt einer schweigend unbenutzbaren Anwendung wirft die Bibliothek bereits beim Hochfahren eine IllegalStateException mit ausführlicher Fehlermeldung — der Operator sieht den Fehler im ersten Augenblick und kann entweder den Modus ändern oder einen Administrator vorprovisionieren. Diese Eigenschaft ist im Vaadin-Kontext besonders wertvoll, weil ein Vaadin-Server ohne Administrator dem Anwender nur eine Login-View anzeigen würde, in der jede Anmeldung scheitert; eine schweigende Fehlfunktion wäre unangenehm, eine laute Fehlermeldung beim Start ist die korrekte Reaktion.
Die folgenden Zeilen beschreiben den Token-Lebenszyklus. Wenn ein Bootstrap erforderlich ist, wird zunächst der BootstrapTokenStore befragt. Liefert er ein gespeichertes Token, das noch nicht abgelaufen ist, wird dieses wiederverwendet — das ist die Persistenz-Eigenschaft des PERSISTENT_FILE-Modus, die einen Server-Neustart während eines noch nicht abgeschlossenen Setups übersteht. Liefert er keinen Token oder einen bereits abgelaufenen, wird ein neuer Token erzeugt und gespeichert. Anschließend wird das Token einmal über den konfigurierten BootstrapTokenOutput ausgegeben — bei TRANSIENT_CONSOLE an die Konsole, bei PERSISTENT_FILE zusätzlich in eine Datei. Diese Ausgabe ist der einzige Augenblick im gesamten Lebenszyklus, in dem das Token an einer für Menschen lesbaren Stelle erscheint; danach verschwindet es bis zur Eingabe in der SetupView aus Kapitel 2.

Abbildung 1
Abbildung 1: Lebenszyklus des Bootstrap-Tokens in drei Phasen — Erzeugung beim Server-Start, Übergabe an den Operator über die Konsole oder eine Datei, Verbrauch durch einen der vier Adapter mit anschließender Token-Invalidierung im Service.
Die Token-Form selbst ist bewusst gestaltet. Der BootstrapTokenGenerator erzeugt eine Zeichenkette der Form XXXX-XXXX-XXXX-XXXX-XXXX aus einem SecureRandom mit etwa 100 Bit Entropie. Das Alphabet schließt die optisch verwechselbaren Zeichen O, 0, I und 1 aus, weil das Token in der Regel von einer Konsole aus abgelesen und in ein Browser-Formular eingetippt wird — eine Verwechslung wäre nicht nur lästig, sondern würde nach mehreren Versuchen die Eingabe blockieren. Die Gruppierung in fünf Vierergruppen mit Bindestrichen folgt derselben Linie: Sie macht das Token für das Auge lesbar und für die Hand abtippbar.
Die Speicherung in der FileBootstrapTokenStore-Implementierung ist der zweite Punkt, an dem die Bibliothek besondere Sorgfalt walten lässt. Die Datei wird mit POSIX-Permissions rw——- (0600) angelegt, sodass sie nur vom Eigentümer-Prozess lesbar ist, und atomar mit StandardOpenOption.CREATE_NEW plus den Permissions als Datei-Attribut erzeugt, sodass sie niemals — auch nicht für einen Augenblick — mit den Default-Permissions des umask existiert:
@Override
public void save(BootstrapToken token) {
try {
Path parent = path.toAbsolutePath().getParent();
if (parent != null) Files.createDirectories(parent);
Files.deleteIfExists(path);
FileAttribute<?>[] attrs = path.getFileSystem().supportedFileAttributeViews().contains("posix")
? new FileAttribute<?>[]{PosixFilePermissions.asFileAttribute(OWNER_RW)}
: new FileAttribute<?>[0];
String content =
"token=" + token.value() + System.lineSeparator()
+ "createdAt=" + token.createdAt() + System.lineSeparator();
try (var channel = Files.newByteChannel(path,
EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE),
attrs)) {
channel.write(StandardCharsets.UTF_8.encode(content));
}
} catch (IOException e) {
throw new IllegalStateException("Could not write bootstrap token file", e);
}
}
Der Inhalt der Datei ist absichtlich schlicht — zwei Zeilen, token=… und createdAt=…, ohne JSON-Abhängigkeit, ohne weitere Felder. Diese Schlichtheit hält die Bibliothek frei von Transportbibliotheken und macht den Inhalt für menschliche Inspektion zugänglich. Auf Nicht-POSIX-Dateisystemen — etwa unter Windows ohne POSIX-Schicht — wird die Datei ohne explizite Berechtigungen erzeugt; das Betriebssystem trägt dann die Verantwortung für den Zugriffsschutz. Diese Inkonsequenz ist bewusst, weil eine rein POSIX-basierte Disziplin die Bibliothek auf POSIX-Plattformen einschränken würde.
Die Token-Validität wird über eine konfigurierbare Dauer tokenValidity gesteuert, die aus System Properties oder Environment Variables eingelesen wird. Der Standardwert liegt bei 24 Stunden. Wenn der Operator das Token ausgibt, hat er also einen Werktag Zeit, das Setup abzuschließen; danach wird das Token bei der nächsten initializeIfRequired-Ausführung als abgelaufen behandelt und durch ein neues ersetzt. Eine Anwendung kann diese Frist verkürzen, wenn ein engeres Zeitfenster gewünscht wird, oder verlängern, wenn ein Setup über mehrere Tage geplant ist. Die zugehörigen Konfigurationsschlüssel — security.bootstrap.mode, security.bootstrap.token.file, security.bootstrap.token.ttl — werden zentral vom BootstrapConfigurationLoader aus System Properties und Environment Variables eingelesen, sodass sowohl die Vaadin-Demos als auch die REST-Demo dieselben Schlüssel anbieten und kein Adapter eine eigene Konfigurationsparser-Logik enthält.
Mit dieser Token-Mechanik ist die Frage „Woher kommt das Token?“ beantwortet. Es stammt aus einem SecureRandom beim Hochfahren des Servers, lebt entweder im Speicher oder in einer 0600-geschützten Datei, wird beim Eintreten der Validitätsfrist neu erzeugt und vom InitialAdminBootstrapService aus Kapitel 3 nach erfolgreicher Anlage über tokenStore.invalidate() wurde entfernt. Was bleibt, ist die andere selbstverständliche Voraussetzung aus Kapitel 3 — der PasswordHasher, der das übergebene Kennwort-char[] zum gespeicherten Hash macht und damit die char[]-Disziplin durch die letzte Schicht trägt. Sein Aufbau und die Frage, wie diese Disziplin auch das in Vaadin verbleibende String-Problem beleuchtet, sind Gegenstand des Kapitels 5.
PasswordHasher und die Disziplin der Char-Array-Hygiene
Das Kennwort, das die SetupView aus Kapitel 2 als char[] an den InitialAdminBootstrapService aus Kapitel 3 übergibt, durchläuft drei Schichten, bevor es seinen finalen Zustand erreicht — den Hash, der im AdministratorAccountStore aus Kapitel 6 abgelegt wird. Die erste Schicht ist die clientseitige Validierung in der View. Die zweite ist die PasswordPolicy, die das Kennwort vor dem Hashing auf Mindestanforderungen prüft. Die Dritte ist die PasswordHasher, die aus dem char[] einen unumkehrbaren Speicherwert erzeugt. Alle drei Schichten teilen die Eigenschaft, das Kennwort als char[] zu behandeln — eine Disziplin, die in der Java-Welt nicht selbstverständlich ist und in der Vaadin-Welt eine bewusst gesetzte Grenze darstellt.
Der PasswordHasher-Vertrag im security-core ist wohl sehr schmal. Eine Schnittstelle mit zwei Methoden:
public interface PasswordHasher {
String hash(char[] rawPassword);
boolean verify(char[] rawPassword, String storedHash);
}
Vier Zeilen, in denen drei Designentscheidungen sichtbar werden. Erstens: Das Eingabeformat ist char[], nicht String. Diese Wahl folgt der seit Jahren gültigen Empfehlung der Java-Sicherheitsdokumentation, weil ein char[] aktiv überschrieben werden kann, bevor der Garbage Collector es einsammelt — ein String lebt im String-Intern-Pool oder im Heap, ohne dass der Aufrufer ihn zuverlässig löschen könnte. Zweitens: Das Ausgabeformat ist String, weil ein Hash für die Speicherung und den Transport keine Geheimhaltung mehr erfordert. Drittens: Die Verifikation nimmt das Kennwort erneut als char[] entgegen und vergleicht es intern mit dem gespeicherten Hash; der Aufrufer kann das char[] nach dem Aufruf zwischenspeichern, ohne dass der Hasher es zwischenspeichern muss.
Die Standardimplementierung Pbkdf2PasswordHasher nutzt PBKDF2 mit HMAC-SHA256 als Pseudozufallsfunktion und 120 000 Iterationen als Standardaufwand. Das Format des gespeicherten Hashes ist absichtlich selbstbeschreibend: pbkdf2$<iterations>$<base64-salt>$<base64-hash>. Diese Form trägt drei Informationen — das Schema, den Iterationsaufwand und das Salt — in einer einzigen Zeichenkette und ermöglicht auch nach einem späteren Wechsel der Iterationszahl die Verifikation, weil jeder gespeicherte Hash sein Konfigurationsumfeld mit sich bringt. Ein in zwei Jahren auf 200 000 Iterationen erhöhter Standard würde ältere Hashes nicht beschädigen; sie werden weiterhin mit ihrer ursprünglichen Iterationszahl verifiziert, und ein Re-Hashing kann beim nächsten erfolgreichen Login transparent erfolgen.
Die derive-Methode, die das eigentliche Hashing leistet, zeigt die char[]-Disziplin an ihrem tiefsten Punkt:
private static byte[] derive(char[] password, byte[] salt, int iterations) {
PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, HASH_BITS);
try {
SecretKeyFactory factory = SecretKeyFactory.getInstance(ALGORITHM);
return factory.generateSecret(spec).getEncoded();
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new IllegalStateException("Password hashing failed", e);
} finally {
spec.clearPassword();
}
}
Der PBEKeySpec aus dem JDK ist eine der wenigen JDK-Klassen, die explizit eine clearPassword-Methode anbietet — sie überschreibt die intern gespeicherte Kopie des Kennworts mit dem Null-Zeichen. Der Finally-Block ruft diese Methode unabhängig vom Ergebnis des Hashings auf. In Verbindung mit dem Arrays.fill(command.password(), ‘\0’) aus dem InitialAdminBootstrapService aus Kapitel 3 ergibt sich eine vollständige Kette: Die Vaadin-SetupView gibt das char[] ab, der Service wischt es nach dem Hashing, und die JDK-Klasse wischt ihre eigene Kopie. Drei Stellen, drei Arrays.fill-Aufrufe, ein einziger Pfad ohne Lücke.

Abbildung 2
Abbildung 2: Lebensweg eines Kennworts vom Vaadin-PasswordField bis zur JDK-PBKDF2-Schicht. Die drei Wisch-Marker (grün) sichern den char[]-Pfad an drei Stellen; die zwei String-Lücken (rot) markieren die unvermeidlichen Zwischenstadien, an denen die String-Eigenschaft der Eingabe-APIs die Disziplin durchbricht.
Die verify-Methode verkörpert eine weniger sichtbare, aber ebenso wichtige Disziplin. Sie verwendet MessageDigest.isEqual statt eines naiven Arrays.equals zum Vergleich zwischen berechnetem und gespeicherten Hash. Diese Wahl schützt vor Timing-Angriffen — die Vergleichszeit ist konstant und unabhängig davon, an welcher Position zwei Hashes voneinander abweichen. Ein naiver Vergleich würde frühzeitig abbrechen und damit aus der Antwortzeit schließen lassen, wie viele Bytes des Hashes bereits korrekt geraten wurden. Diese Eigenschaft ist für die Bootstrap-Mechanik nicht unmittelbar relevant — der Bootstrap nutzt nur hash, nicht verify —, aber sie wird die laufende Authentifizierung in Teil 5 stützen.
Die PasswordPolicy ergänzt den Hasher als zweite Schicht. Der Vertrag liefert eine validate-Methode, die ein PasswordPolicyResult mit ok oder violation(reason) zurückgibt. Die Standardimplementierung MinimumLengthPasswordPolicy ist bewusst trivial: Sie prüft nur die Mindestlänge, mit einem im Konstruktor übergebenen Wert. Der Demo-Standard liegt bei acht Zeichen — niedrig genug, um eine Demo nicht mit einer Sechzehn-Zeichen-Forderung vor dem ersten Login abzuwehren, und mit der ausdrücklichen Erwartung, dass eine produktive Anwendung eine eigene strengere Policy einbindet. Die Trennung von Hasher und Policy ist erzählerisch wertvoll, weil sie zeigt, dass Hashing und Validierung zwei strukturell verschiedene Aufgaben sind — die eine ist eine Eigenschaft der Speicherung, die andere eine Eigenschaft der Eingabe.
Die Vaadin-Welt hat in dieser Schichtung eine bewusst gesetzte Grenze. Das Vaadin-PasswordField liefert seinen Wert über getValue() als String, nicht als char[]. Diese API-Eigenschaft ist nicht der Bibliothek zuzuschreiben — sie folgt dem allgemeinen Vaadin-Komponentenmodell, das auf Strings basiert. Sie ist aber die Stelle, an der die ansonsten konsequente char[]-Disziplin durch alle Schichten der Bibliothek mit einer Realität konfrontiert wird, die die Bibliothek nicht ändern kann. Was die Vaadin-SetupView aus Kapitel 2 leisten kann, ist die Disziplin, am letzten Vaadin-Berührungspunkt einzuziehen: Sie wandelt den String so früh wie möglich in ein char[] um, übergibt es an den Service und ruft anschließend passwordField.clear() aufrufen, damit die Vaadin-Komponente den Wert nicht behält. Was die View nicht leisten kann, ist, die String-Instanzen aus dem JVM-Speicher zu entfernen — sie leben bis zum nächsten Garbage-Collector-Lauf, und ein speicherinspizierender Angreifer könnte sie theoretisch lesen.
Diese Inkonsequenz ist in der heutigen Vaadin-Welt nicht vermeidbar. Eine konsequente Lösung würde erfordern, dass das PasswordField selbst eine char[]-API anbietet — eine Erweiterung, die in Vaadin Flow ohne Eingriff in die Komponente selbst nicht möglich wäre. Die Bibliothek wählt deshalb die ehrliche Behandlung: Sie zieht die Disziplin so weit ein, wie sie es kann, und benennt die verbleibende Lücke als bekannte Eigenschaft. Diese Haltung folgt der in Teil 3 etablierten Linie, die Klartext-Authentifizierung der Demo-Benutzer offen anzusprechen statt sie zu kaschieren — sie wird in Teil 5 noch einmal aufgegriffen werden, wenn die laufende Authentifizierung selbst auf den PasswordHasher umgestellt wird.
Damit sind die zwei selbstverständlichen Voraussetzungen aus Kapitel 3 — Token-Mechanik und PasswordHasher — vollständig beschrieben. Was bleibt, ist die dritte Beziehung des InitialAdminBootstrapService aus seinem Konstruktor: der AdministratorAccountStore, in den der neu angelegte Administrator geschrieben wird. Sein Vertrag und seine drei Adapter-spezifischen Implementierungen — eine für die in-JVM-Vaadin-Demo, eine für das REST-Backend, eine implizite dritte für den Vaadin-REST-Client — sind Gegenstand von Kapitel 6.
Verträge im Kern und ihre Implementierungen pro Adapter
Der InitialAdminBootstrapService aus Kapitel 3 hatte sieben Beziehungen, von denen Kapitel 4 die Token-Mechanik und Kapitel 5 den PasswordHasher mit der PasswordPolicy behandelt haben. Es bleibt eine Beziehung, die strukturell die wichtigste ist — der AdministratorAccountStore, in den der neu angelegte Administrator geschrieben wird. Diese Beziehung ist deshalb die wichtigste, weil sie die Nahtstelle zwischen der Bibliothek und der konkreten Anwendung darstellt: Die Bibliothek besitzt das Benutzermodell der Anwendung nicht; die Anwendung muss es selbst bereitstellen.
Der Vertrag ist denkbar schmal:
public interface AdministratorAccountStore {
boolean hasAnyAdministrator();
void createAdministrator(NewAdministrator newAdministrator);
}
Zwei Methoden, eine fragend, eine schreibend. hasAnyAdministrator ist die Frage, die der BootstrapStateService aus Kapitel 3 nutzt, um die Bootstrap-Mechanik abzuschalten, sobald der erste Administrator existiert; sie ist auch die Frage, die der doppelte Check in createInitialAdmin als zweiten Schritt stellt. createAdministrator ist der schreibende Aufruf, der nach erfolgreicher Hash-Erzeugung den fertigen Administrator speichert. Die Eingabe ist ein versiegelter NewAdministrator-Record:
public record NewAdministrator(
String username,
String displayName,
String email,
String passwordHash
) { ... }
Vier Felder, von denen username und passwordHash zwingend leer sein müssen. Das passwordHash-Feld ist bewusst so benannt — und nicht password. Implementierungen müssen den Hash unverändert speichern; ein zweites Hashing wäre eine schwerwiegende Inkonsistenz, die die Verifikation während der laufenden Authentifizierung scheitern lässt. Diese Erwartung ist im Javadoc der Methode ausdrücklich benannt, und sie ist die einzige Stelle in der gesamten Bootstrap-Mechanik, an der ein Adapter eine Eigenschaft zu wahren hat, die nur durch Vertragstreue erfüllt werden kann.
Die erste Implementierung befindet sich im demo-vaadin und heißt VaadinAdministratorAccountStore. Sie ist die Brücke zwischen dem adapter-neutralen Bootstrap und dem Vaadin-spezifischen DemoUserDirectory:
public final class VaadinAdministratorAccountStore implements AdministratorAccountStore {
private final DemoUserDirectory directory;
private final AtomicLong idSequence = new AtomicLong(1000);
public VaadinAdministratorAccountStore(DemoUserDirectory directory) {
this.directory = Objects.requireNonNull(directory, "directory");
}
@Override
public boolean hasAnyAdministrator() {
return directory.hasAnyAdministrator();
}
@Override
public void createAdministrator(NewAdministrator newAdministrator) {
Set<AuthorizationRole> roles = new HashSet<>();
roles.add(AuthorizationRole.ADMIN);
roles.add(AuthorizationRole.USER);
String displayName = newAdministrator.displayName() == null || newAdministrator.displayName().isBlank()
? newAdministrator.username()
: newAdministrator.displayName();
MyUser user = new MyUser(idSequence.getAndIncrement(), displayName, roles);
directory.registerWithHashedPassword(newAdministrator.username(), newAdministrator.passwordHash(), user);
}
}
Drei Beobachtungen zum Vaadin-Adapter. Die erste betrifft die Rollenwahl. Der frisch angelegte Administrator erhält nicht nur die ADMIN-Rolle, sondern zusätzlich auch die USER-Rolle. Diese Doppelung folgt dem Modell des demo-vaadin, in dem ADMIN eine zusätzliche Rolle über USER steht und nicht eine ersetzende Rolle. Ein Administrator soll alle Views betreten können, die ein einfacher Benutzer betreten kann, sowie diejenigen, die nur Administratoren zugänglich sind. Diese Konvention ist nicht Bestandteil des AdministratorAccountStore-Vertrags — sie ist eine Eigenschaft des Vaadin-Demo-Modells und wird hier vom Adapter explizit definiert. Ein anderes Vaadin-Modell mit anderer Rollensemantik würde an dieser Stelle anders aussehen.
Die zweite Beobachtung betrifft die ID-Generierung. Ein AtomicLong mit dem Startwert 1000 erzeugt fortlaufende eindeutige IDs für jeden neu angelegten Administrator. Diese Wahl ist demo-spezifisch — eine produktive Anwendung würde die ID aus einer Datenbanksequenz oder einem ID-Generator beziehen, nicht aus einem In-Memory-AtomicLong. Die Bootstrap-Mechanik selbst kennt diese ID nicht; sie ist eine Eigenschaft des Vaadin-Modells und wird vom Adapter bereitgestellt. Der NewAdministrator-Record hat keine ID, da er im Verantwortungsbereich des Speichers liegt.
Die dritte Beobachtung betrifft den Aufruf von directory.registerWithHashedPassword(…). Diese Methode ist die zweite Hälfte der Vertragstreue: Sie nimmt das Hash-Passwort entgegen und speichert es unverändert. Die DemoUserDirectory-Schnittstelle hat zu diesem Zweck eine eigene Methode neben der addUser(String, plaintextPassword, …)-Methode, die für die vorpopulierten Demo-Benutzer mit Klartext-Kennwörtern existiert. Diese explizite Trennung ist erzählerisch wertvoll, weil sie die Klartext-Lücke aus Teil 3 für die Bootstrap-Anlage tatsächlich schließt — der Bootstrap nutzt den gehashten Pfad, die Demo-Benutzer den Klartext-Pfad, und beide leben nebeneinander, bis Teil 5 die Demo-Benutzer ebenfalls auf gehasht umstellt.
Die zweite Implementierung lebt im demo-rest und heißt DemoAdministratorAccountStore. Sie ist strukturell parallel zur Vaadin-Variante, aber kürzer:
public final class DemoAdministratorAccountStore implements AdministratorAccountStore {
private final DemoUserStore users;
public DemoAdministratorAccountStore(DemoUserStore users) {
this.users = users;
}
@Override
public boolean hasAnyAdministrator() {
return users.hasAnyAdministrator();
}
@Override
public void createAdministrator(NewAdministrator newAdministrator) {
String displayName = newAdministrator.displayName() == null || newAdministrator.displayName().isBlank()
? newAdministrator.username()
: newAdministrator.displayName();
users.register(new DemoUser(
newAdministrator.username(),
displayName,
newAdministrator.passwordHash(),
DemoRole.ROLE_ADMIN));
}
}
Die Struktur ist symmetrisch zur Vaadin-Variante — derselbe Konstruktor mit einem Speicherbezug, dieselbe hasAnyAdministrator-Delegation, derselbe displayName-Fallback. Die Unterschiede liegen im Demo-Modell selbst: Der DemoUserStore des demo-rest arbeitet mit einer einzelnen DemoRole.ROLE_ADMIN statt mit zwei Rollen, und er braucht keine separate ID-Generierung, weil der DemoUser-Record keinen numerischen ID-Wert hat. Beide Eigenschaften sind Konsequenzen des unterschiedlichen Demo-Modells, nicht Eigenschaften des AdministratorAccountStore-Vertrags. Der Vertrag selbst lässt beide Modelle gleichermaßen zu.
Die dritte Implementierung ist die interessanteste, weil sie nicht existiert. Der demo-vaadin-rest-client hat keinen eigenen AdministratorAccountStore — er ruft den REST-Endpunkt des Backends auf, der seinerseits den DemoAdministratorAccountStore aus dem demo-rest nutzt. Die Vaadin-SetupView aus Kapitel 9 kommuniziert über HTTP mit dem Backend, und das Backend übernimmt die Bootstrap-Initialisierung. Aus Sicht des Vaadin-Clients ist der Store transparent: Er sieht nur einen Backend-Client mit einer createInitialAdmin-Methode, die unter der Haube einen HTTP-POST an /api/bootstrap/admin sendet. Aus Sicht des Backends ist der Aufruf identisch mit einem Aufruf aus der CLI oder einem direkten REST-Aufruf — der Store kennt die Quelle nicht.
Diese Beobachtung schließt das Bild der vier Adapter ab. Der AdministratorAccountStore lebt zweifach — einmal im demo-vaadin für den in-JVM-Bootstrap, einmal im demo-rest für den REST-vermittelten Bootstrap, der von drei verschiedenen Adaptern (REST direkt, CLI, Vaadin-REST-Client) genutzt wird. Vier Adapter, drei AdministratorAccountStore-Konsumenten, zwei Implementierungen. Der InitialAdminBootstrapService kennt keine dieser Implementierungen, und er muss sie nicht kennen — er kennt nur den Vertrag, und der Vertrag trägt jede der drei Anbindungen ohne weitere Anpassung.
Damit ist der Kreis um den Bootstrap-Kern geschlossen. Was der Service leistet, was er voraussetzt, woher seine Voraussetzungen stammen und wie er den fertigen Administrator speichert — alles ist beschrieben. Was bleibt, ist die Behandlung der vier konkreten Adapter selbst. Den Anfang macht Teil 4.3 mit dem REST-Endpunkt im demo-rest, der die Bootstrap-Mechanik über HTTP zugänglich macht und damit die Grundlage für die beiden Nicht-Vaadin-Adapter sowie den Vaadin-REST-Client legt.

Vier Aufrufkontexte erwarten den Leser in Teil 4.3: der REST-Endpunkt im demo-rest, das CLI-Subkommando init-admin in derselben Anwendung, die zweite Vaadin-SetupView aus dem demo-vaadin-rest-client als REST-Konsument und die abschließende Bilanz zur Vier-Adapter-Architektur als visueller Klammer. Die zweite Vaadin-Anbindung ist die didaktische Pointe — sie unterscheidet sich von der ersten aus Teil 4.1 in einer einzigen Codezeile, deren Bedeutung das gesamte Bild der Bibliothek trägt.