CodeGym /Kurse /Spring Security /Technische Passwortrichtlinie

Technische Passwortrichtlinie

Spring Security
Level 6, Lektion 4
Verfügbar

1. Passwortrichtlinie: Sinn und Grenzen

Wenn man die Worte „Password Policy“ hört, malt das Gehirn normalerweise das klassische Bild: „8 Zeichen, Großbuchstaben, Kleinbuchstaben, Ziffer, Sonderzeichen und eine Träne des Entwicklers“. Das gibt es tatsächlich, aber heute geht es um etwas anderes. Technische Passwortrichtlinie – das sind die Regeln, wie das Backend mit dem Passwort als Geheimnis umgeht: wo es auftaucht, wo es verschwinden muss, was wir speichern, wie wir überprüfen, was wir loggen und was selbst „für einen Moment“ zu loggen verboten ist.

In unserem Kurs ist die Richtlinie kein 40-seitiges Dokument für die Sicherheitsabteilung und kein Unternehmensstandard um des Standards willen. Es ist eine sehr praktische Disziplin, die den Code vorhersagbar macht: Jeder im Team weiß, dass das Passwort nicht mit equals verglichen werden darf, nicht in ein Log ausgegeben werden darf, nicht in ein Antwort-DTO gesteckt werden darf, nicht „mal eben so verarbeitet“ und gespeichert werden darf. Seltsamerweise sind es genau diese „Kleinigkeiten“, die am häufigsten die Sicherheit brechen: nicht weil jemand ein Bösewicht ist, sondern weil jemand müde war, sich beeilt hat und meinte „das ist doch nur lokal“.

Die Mechanik für diesen Punkt ist bereits zusammengestellt: Das Passwort wird mit einem PasswordEncoder kodiert, über matches(...) verifiziert, und Abkürzungen wie withDefaultPasswordEncoder() und {noop} haben bereits gezeigt, wie leicht dieses Modell zu brechen ist. Jetzt bleibt nur noch, dies von einer Ansammlung von Wissen in Projektregeln umzuwandeln: Wie das Feld benennen, wo nur der Hash gehalten wird, was zu loggen verboten ist und an welchen Stellen das Passwort überhaupt erscheinen darf.

Um den Sinn nicht zu verwässern, lassen Sie uns die Begriffe trennen. Wir diskutieren keine UX-Anforderungen an die Passwortkomplexität und erstellen keine komplexen Eingaberegeln – das ist ein separates Produktthema. Hier brauchen wir eine bodenständigere Frage: Wie gehen wir mit rawPassword und passwordHash im Code um, damit das Projekt nicht anfängt, Geheimnisse „durchsickern“ zu lassen.

2. Einheitlicher PasswordEncoder in der Anwendung

In den ersten Schritten hat ein Anfänger den natürlichen Drang, new BCryptPasswordEncoder() zu schreiben, wo „jetzt gerade ein Passwort benötigt wird“. Das sieht logisch aus, bis das Projekt auf 5–10 Klassen anwächst, und an einer Stelle haben Sie bcrypt mit Standardparametern, an einer anderen einen anderen Encoder und an einer dritten plötzlich {noop}, weil „es so einfacher zu testen war“. Und schon haben Sie keine Sicherheit mehr, sondern ein Quest „rate, in welcher Klasse die Wahrheit gespeichert ist“.

Daher wird in der Richtlinie das Prinzip festgelegt: Die Anwendung sollte einen zentralen PasswordEncoder Baseline haben, der als Spring-Bean deklariert ist. Dann trifft jede Komponente nicht die Entscheidung „welchen Encoder wählen“, sondern erhält ihn einfach per DI. Dies reduziert die Wahrscheinlichkeit von versehentlich unsicherem Code fast auf null, weil die Entscheidung in die Konfiguration ausgelagert und sofort sichtbar ist.

Für eine solche Baseline genügt PasswordEncoderFactories.createDelegatingPasswordEncoder(). Wichtig ist nicht der Aufruf der Factory selbst, sondern die Disziplin: Die Richtlinie stützt sich auf den bereits gewählten Encoder und erlaubt nicht, dass ein Dienst leise auf bcrypt, ein anderer auf {noop} lebt und ein dritter Strings von Hand vergleicht.

Mit anderen Worten: Ein Encoder, ein Speicherformat, und alle anderen Entscheidungen um das Passwort herum werden auf diesem Wahrheitspunkt aufgebaut.

3. Speicherung: rawPassword → passwordHash

Fast alle ernsthaften Probleme mit Passwörtern beginnen damit, dass rawPassword länger lebt als nötig. Es landet im Feld eines Objekts, im Log, in einer Exception, in einem Speicherdump, in irgendeiner Struktur „für später“. Dann passiert ein Leck von irgendetwas (nicht unbedingt der Datenbank), und plötzlich werden Geheimnisse offengelegt. Die technische Richtlinie geht genau darum: Die Lebensdauer des Raw-Passworts sollte so kurz wie möglich sein: Es kommt am Eingang an, wird sofort in einen sicheren Wert zur Speicherung umgewandelt und verschwindet aus dem Geschäftsablauf.

In unserem Projekt Secure Content Platform API passt das perfekt zum Modell UserAccount, das laut Spezifikation ein Feld passwordHash haben soll. Schon der Name ist Teil der Richtlinie: Wir verbieten uns buchstäblich zu denken, dass dies ein „password“ ist. Es ist kein Passwort. Es ist ein Hash (encoded value) zur Überprüfung. Eine solche Kleinigkeit mit der Namensgebung reduziert die Fehleranzahl wirklich: Wenn Sie passwordHash sehen, zögert die Hand eher, equals zu verwenden.

Es ist nützlich, eine einfache Zustandskarte des Passworts im Kopf zu behalten:

Was ist das Beispiel Kann lange gespeichert werden? Kann geloggt werden?
rawPassword "qwerty123" Nein Niemals
passwordHash (encoded) "{bcrypt}$2a$10$..." Ja (das ist nötig) Besser auch nicht

Ja, passwordHash ist kein Klartext, aber das Protokollieren des Hashs bringt selten einen Nutzen und hilft manchmal einem Angreifer beim Offline-Abgleich. In diesem Studienprojekt betrachten wir das Prinzip „weder raw noch hash loggen“ als sicher.

Eine gute Praxis der Richtlinie ist die Einführung einer einfachen Regel für die Benennung von Variablen: Überall wo das Passwort noch ursprünglich ist, nennen wir es rawPassword, und wo es bereits gespeichert ist, nennen wir es passwordHash oder encodedPassword. Dann liest selbst ein müder Mensch am Freitagabend den Code und verwechselt es trotzdem nicht.

Die Kodieroperation selbst sollte hier nicht lang oder „clever“ sein. Wichtig ist etwas anderes: Sie geschieht sofort an der Eingangsgrenze, nicht verstreut über Controller, Repositories und zufällige Util-Klassen. Sobald der Wert diesen Schritt überstanden hat, ist es im Code nicht mehr password, sondern passwordHash.

Um zu sehen, wie das im Domänenmodell leben würde, kann man ein minimales Account-Objekt skizzieren (vorübergehend ohne JPA und Datenbank, einfach um nicht im luftleeren Raum zu denken):

package com.example.securecontent.auth;

public class UserAccount {
    private final String username;
    private final String passwordHash; // Nach Policy speichern wir nur den Hash, nicht das ursprüngliche Passwort.

    public UserAccount(String username, String passwordHash) {
        this.username = username;
        this.passwordHash = passwordHash;
    }
}

Wenn Sie jetzt bei dem Gedanken ertappt haben „und wo ist das Passwort?“, dann gratuliere ich: Die Richtlinie funktioniert. Ein Passwort als Wert gibt es nicht. Es gibt nur das, was gespeichert werden kann.

4. Überprüfung: nur matches(...)

Einer der häufigsten Anfängerfehler ist der Versuch, es „symmetrisch zu machen“: Wenn wir beim Speichern encode gemacht haben, dann machen wir beim Login auch encode(input) und vergleichen die Strings. Das scheint logisch, bis Sie sich erinnern, dass wir über adaptive one-way Funktionen gesprochen haben: Sie verwenden Salt und andere Parameter so, dass dasselbe Passwort nicht bei jedem encode denselben String liefern muss. Daher ist „encode und vergleichen“ nicht nur schlechter Stil, sondern ein logischer Fehler: Selbst korrekte Passwörter werden „nicht übereinstimmen“.

Die Richtlinie legt die Regel fest: Die Passwortüberprüfung erfolgt immer über PasswordEncoder.matches(raw, stored). Dies ist keine Option oder „wie es bequem ist“. Es ist der einzig richtige API-Weg im Rahmen des Spring Security Mental Model.

Auf Policy-Ebene ist die Regel hier streng: kein encode(input) + equals(...). Die Überprüfung erfolgt immer über matches(rawPassword, passwordHash), weil der gespeicherte Wert das Salt und Algorithmusparameter enthalten kann.

Beachten Sie: Bei einer solchen Überprüfung ist es nützlich, sogar die Parameternamen verständlich zu halten. rawPassword zuerst, passwordHash als zweites – dann werden das Lesen des Codes und das Debuggen nicht zum Ratespiel. Sobald Sie equals zwischen zwei „passwortartigen“ Strings sehen, ist die Richtlinie bereits verletzt.

Ein weiterer praktischer Nuance der Richtlinie: Normalisieren Sie das Passwort nicht. Manchmal möchte man trim() oder toLowerCase() machen, um es „dem Benutzer einfacher zu machen“. Aber ein Passwort ist kein Stadtname. Es muss genau wie eine geheime Zeichenkette verglichen werden. Jede „Normalisierung“ erzeugt unerwartete Effekte und reduziert die tatsächliche Entropie des Passworts. Daher ist die Regel einfach: Das Backend behandelt das Passwort als opaque value und „verbessert“ es nicht.

5. Zentralisierung der Passwortarbeit

Selbst mit dem richtigen PasswordEncoder kann man leicht Chaos anrichten, wenn die Hälfte der Anwendung das Passwort auf „seine eigene Weise“ codieren und überprüfen kann. Die wahre Stärke der Richtlinie liegt nicht darin, dass Sie bcrypt kennen, sondern dass Sie die Arbeit mit dem Passwort an einen Ort ausgelagert haben. Dann ist das Audit einfacher, die Fehlerwahrscheinlichkeit geringer und der Code leichter zu lesen.

In unserem Projekt ist es praktisch, einen solchen Ort als PasswordService zu gestalten und ihn als „einzige Tür“ für encode und matches zu betrachten. Das ist keine Magie, sondern Disziplin: Weniger Stellen im Code kennen das Passwort – geringere Chance, versehentlich den rohen Wert zu loggen, ihn zweimal zu codieren oder Strings von Hand zu vergleichen.

Die gleiche Regel gilt für Seed- und Dev-Benutzer. Wenn ein Testkonto benötigt wird, kann der encodierte Wert einmal lokal mit demselben Basis-Encoder generiert werden und in die Konfiguration oder Dev-Einstellung gelegt werden. Ja, der resultierende String wird jedes Mal anders sein – und genau das ist normal.

Der Sinn liegt hier nicht in einem separaten Dienstprogramm als solchem, sondern in der Gewohnheit: Im Projekt lebt nur der gespeicherte Wert, und das rohe Passwort wird nicht zum ständigen Nachbarn des Quellcodes.

6. Logs und Leaks von Geheimnissen

Wenn ein Passwort einen Charakter hätte, würde es davon träumen, in ein Log zu gelangen. Das Log ist ein warmer, gemütlicher Ort: Es wird gelesen, gespeichert, weitergeleitet, archiviert, manchmal sogar an ein Ticket angehängt. Genau deshalb muss die Passwortrichtlinie eine Regel zum Loggen enthalten. Und diese Regel klingt langweilig, rettet aber Projekte: Ein Passwort wird niemals geloggt, selbst im Development-Profil nicht, selbst nicht mit „ich lösche es später genau“, selbst nicht mit „es ist ja nur ein Testbenutzer“.

Es gibt eine knifflige Falle im modernen Java-Code: Records und Lombok. Ein Record hat standardmäßig ein hübsches toString(), das alle Felder ausgibt. Das ist praktisch, bis eine der Komponenten password ist. Dann wird aus einer harmlosen Zeile log.info("request={}", request) ein Leak.

Schauen wir uns ein Beispiel für gefährlichen Code an. Er sieht harmlos aus, wie eine Katze, die nur kommt, um auf der Tastatur zu sitzen:

// Gefährlich: Wenn ein solcher Record vollständig geloggt wird, leaket das Passwort über toString().
public record LoginRequest(String username, String password) { }

Und irgendwo im Code:

// Schlecht: request.toString() gibt alle Felder aus, einschließlich password.
log.info("Login request={}", request);

Das Passwort landet im Log. Ohne Verschlüsselung, ohne Hash, ohne alles. Einfach weil toString beschlossen hat, zu „helfen“.

Die Richtlinie sollte ein solches Loggen direkt verbieten. Stattdessen loggen wir nur den Kontext des Ereignisses: wer (username), was passiert ist (Erfolg/Misserfolg) und, falls nötig, technische Request-Identifikatoren (z.B. requestId aus MDC – das ist ein separates Thema). Beispiel für ein „gutes“ Log:

package com.example.securecontent.security.auth;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
public class AuthLogging {
    private static final Logger log = LoggerFactory.getLogger(AuthLogging.class);

    public void loginFailed(String username) {
        // Nach Policy: loggen wir nur den Kontext (username), aber nicht das Passwort und nicht den passwordHash.
        log.warn("Login failed for username={}", username);
    }
}

Hier gibt es kein Passwort. Und keinen Hash. Und das ist normal: Für die Diagnose reichen normalerweise die Tatsache und der Username.

Genauso betrifft die Richtlinie Fehler und Ausnahmen. In eine Exception-Nachricht darf kein rohes Passwort eingefügt werden, und fast immer auch kein passwordHash. Selbst wenn Sie unbedingt „sehen wollen, was verglichen wurde“ – dieser Wunsch kommt aus der Kategorie „mal kurz auf das Geheimnis schauen“. Geheimnisse schaut man nicht mal kurz an. Wenn Sie matches debuggen müssen, debuggen Sie über einen Test, nicht durch Protokollieren von Geheimnissen.

Ein weiteres Element der Richtlinie ist was wir dem Client zurückgeben. Für jede API gilt die einfache Regel: In Antworten wird niemals ein rohes Passwort oder ein passwordHash zurückgegeben. Auch nicht in Admin-Endpunkten. Auch nicht „um sicherzustellen, dass der Benutzer erstellt wurde“. Dafür gibt es andere Werkzeuge: Tests, Admin-Oberflächen, separate Diagnosemechanismen, aber keine Ausgabe von Passwörtern im JSON.

Um dies nicht nur Worte sein zu lassen, können Sie eine kurze „Merktafel“ von Formaten bereithalten, die leaken können:

Leckkanal Wie der Fehler aussieht Was laut Richtlinie tun
Logging des Request DTO request={LoginRequest(... password=...)} Nur username/Kontext loggen
Exception throw new ...("wrong password=" + rawPassword) Niemals das Passwort in die Nachricht aufnehmen
Debug println System.out.println(rawPassword) Nicht einmal im Dev machen
API-Antwort {"passwordHash":"{bcrypt}..."} Nicht in Antwort-DTO aufnehmen

Ja, die Tabelle ist einfach. Aber Sicherheitsfehler haben genau diese Natur: Sie sehen nicht aus wie „ein Hack“, sie sehen aus wie „eine kleine praktische Zeile für den Entwickler“.

7. Abkürzungen und Seed‑Daten

Abkürzungen wie User.withDefaultPasswordEncoder() und {noop} verführen genau dadurch, dass sie einen „überflüssigen“ Schritt entfernen. Daher muss die Richtlinie nicht einfach „verboten“ sagen, sondern einen Ersatz bieten. Sonst wird das Verbot zum Kampf gegen die Realität: Die Person wird trotzdem {noop} schreiben, weil „es sonst lange dauert“.

Eine gesunde Lernstrategie ist folgende: Abkürzungen kann man als Demonstration zeigen, aber die Richtlinie des Projekts sollte anders sein und durch Code umgesetzt werden. Das heißt, im Hauptcode des Projekts verwenden wir weder withDefaultPasswordEncoder() noch {noop}. Wenn ein Testbenutzer dringend benötigt wird, codieren wir das Passwort entweder über den PasswordEncoder und legen den encodierten String ab, oder wir generieren einmalig einen encodierten Wert mit demselben Basis-Encoder. Ja, das ist einen Schritt länger. Aber genau dieser Schritt lehrt, richtig zu denken.

Eine weitere Kleinigkeit, die oft vergessen wird: DelegatingPasswordEncoder speichert das Präfix {id} direkt im String. Das bedeutet, dass encodierte Strings länger sein können als erwartet. Und die Richtlinie sollte die banale Regel enthalten: nicht kürzen oder „in ein kleines Feld zwängen“. Selbst wenn Sie die Benutzer jetzt „im Speicher“ speichern, könnten Sie versehentlich Code schreiben, der „der Schönheit halber“ nur die ersten 60 Zeichen des Hashs nimmt. Wenn Sie dann zu einer realistischeren Speicherung wechseln, wird das genauso zuverlässig nach hinten losgehen wie equals statt matches.

Und ja: Wenn Sie in einem fremden Tutorial "{noop}password" gesehen haben und dachten „oh, praktisch“ – denken Sie daran, dass {noop} in der Richtlinie unseres Projekts eher eine rote Flagge als ein Feature ist. Es ist nur als Lernmarker zulässig, der zeigt, „wie das Fehlen einer Kodierung aussieht“, aber nicht als Teil des Arbeitscodes.

8. Richtlinie in Secure Content Platform API

In Secure Content Platform API ist es praktisch, dies nahe beieinander zu halten: Konfiguration des PasswordEncoder in security.config, Arbeit mit dem Passwort in security.password, und der Geschäftscode und die Domänenentitäten sollten „nichts“ über Algorithmen und das, was bcrypt ist, „wissen“.

Man kann dies als kleines Flussdiagramm zeichnen, um den Gedanken endgültig „festzunageln“:

flowchart LR
    A["Eingangsgrenze
rawPassword"] --> B["PasswordService"] B --> C["PasswordEncoder
Delegating..."] C --> D["passwordHash
{id}..."] D --> E["Kontospeicherung
UserAccount.passwordHash"]

Dieses Grundgerüst reicht bereits: ein Encoder-Baseline in security.config, ein Punkt für encode und matches in security.password, und passwordHash im Account-Modell. Die Richtlinie selbst liegt nicht in der Anzahl der Klassen, sondern darin, dass das gesamte Projekt dieselbe Regel verwendet.

Und hier ist es wichtig, den letzten scheinbaren Widerspruch aufzulösen. Selbst wenn der Benutzer vorübergehend im Speicher oder in einem anderen einfachen User-Store lebt, bleibt das Passwort dort dennoch ein encoded value. Die Benutzerquelle ändert sich, nicht das, was genau im Feld password oder passwordHash gespeichert ist. Daher überlebt derselbe Password-Baseline problemlos sowohl den Dev-User als auch ein realistischeres Account-Modell.

9. Typische Fehler bei der technischen Passwortrichtlinie

Am Ende des Tages ist es nützlich, die häufigsten Fallen in einem Block zu sammeln. Nicht um zu erschrecken, sondern damit Sie lernen, diese Fehler am Geruch zu erkennen: Sie sehen eine Codezeile und riechen sofort „das riecht nach Leck“ oder „das riecht nach falscher Überprüfung“. Das ist eine Fähigkeit, die einen Entwickler ruhiger macht, weil weniger Zeit für rätselhafte Bugs und unangenehme Überraschungen aufgewendet wird.

Fehler #1: Das Passwort als „normales String-Feld“ speichern oder übergeben.
Ein Anfänger erstellt UserAccount und fügt dort password hinzu, weil „es genauso bequem ist wie username“. Das bricht das Modell von Grund auf: Das Passwort beginnt lange zu leben, an toString() teilzunehmen, in Serialisierung und Debug zu geraten. Der richtige Ansatz: nur passwordHash speichern, das ursprüngliche Passwort ausschließlich an der Eingangsgrenze halten und sofort codieren.

Fehler #2: Das Passwort über encode + equals überprüfen.
Das ist die klassische logische Falle: „Wenn encode einen String erzeugt, dann vergleichen wir die Strings“. Aber bei normalen Algorithmen muss das Passwort nicht bei jedem encode dasselbe Ergebnis liefern, daher wird die Passwortüberprüfung durch den Vergleich zweier codierter Strings zu einer Lotterie, bei der Sie fast immer verlieren. Laut Richtlinie erfolgt die Überprüfung immer nur über matches(raw, stored).

Fehler #3: new BCryptPasswordEncoder() an verschiedenen Stellen in der Anwendung erzeugen.
Zuerst sieht das nach „schnell und verständlich“ aus, aber dann stellen Sie fest, dass ein Teil des Codes einen Encoder verwendet, ein anderer Teil einen anderen und irgendwo zufällig {noop} aufgetaucht ist. Am Ende kann niemand die Frage beantworten „Was ist unsere Passwortstrategie?“. Laut Richtlinie sollte der Encoder ein einziger Bean sein und alle verwenden ihn per DI.

Fehler #4: Request-Objekte vollständig loggen (insbesondere Records) und versehentlich das Passwort in die Logs leaken.
log.info("request={}", request) ist eine bequeme Angewohnheit, die sich im Falle eines Passworts in einen Leak verwandelt. Bei einem Record umfasst toString() alle Komponenten, und das Passwort landet schneller in den Logs, als Sie „oh“ sagen können. Laut Richtlinie loggen wir nur den Kontext: username, Tatsache des Ereignisses, technische Identifikatoren, aber keine Geheimnisse.

Fehler #5: „Naja, das ist doch Dev / Testprojekt, {noop} ist in Ordnung.“
So entstehen schlechte Gewohnheiten. Heute ist {noop} „nur lokal“, morgen landet es für „fünf Minuten“ im main-Branch und übermorgen vergisst jemand, es zu entfernen. In einem Studienprojekt können wir {noop} als Marker zeigen, aber die Richtlinie des Projekts sollte production-denken bleiben: codierte Passwörter, expliziter PasswordEncoder, keine rohen Geheimnisse im Repository.

1
Aufgabe
Spring Security, Level 6, Lektion 4
Gesperrt
Einheitliche Tür für Passwortoperationen
Einheitliche Tür für Passwortoperationen
1
Aufgabe
Spring Security, Level 6, Lektion 4
Gesperrt
Encoded password in `application.yml`
Encoded password in `application.yml`
1
Umfrage/Quiz
Passwortsicherheit, Level 6, Lektion 4
Nicht verfügbar
Passwortsicherheit
Passwörter speichern und schützen
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION