CodeGym /Kurse /Spring Security /In-Memory-Benutzer

In-Memory-Benutzer

Spring Security
Level 7, Lektion 0
Verfügbar

1. Einführung

Wenn man das Projekt als kleine Stadt betrachtet, dann ist SecurityFilterChain die Verkehrsschilder und Kontrollpunkte: „Hier darf jeder rein“, „Hier nur mit Ausweis“, „Hier nur Redakteure“, „und hier – nur Administratoren“. Aber wenn Sie keine „Bürger“ (Benutzerkonten) haben, können Sie nicht richtig überprüfen, ob diese Regeln funktionieren, und das Unangenehmste – Sie können die gleiche Prüfung nicht reproduzierbar in einer Woche wiederholen.

Bisher konnte Spring Boot einen Standardbenutzer (mit zufälligem Passwort) erstellen und damit secure-by-default demonstrieren. Aber sobald Sie eine Zugriffsmatrix für die Secure Content Platform API aufbauen, benötigen Sie Szenario-Konten: normaler Benutzer, Redakteur, Administrator. Und zwar solche, dass Sie deren Logins und Passwörter genau kennen, nicht fragen „warum ist das Passwort heute anders“, und bei jedem Start das gleiche Bild erhalten.

Genau deshalb sollte das erste sinnvolle Benutzermodell im Kurs so einfach und so kontrollierbar wie möglich sein: In-Memory.

2. In-Memory statt Datenbank am Anfang

Es gibt ein sehr verbreitetes Missverständnis: „Wenn wir ein Backend machen, dann sofort eine Datenbank“. In der realen Produktion – ja, Benutzer leben fast immer in einer Datenbank oder in einem externen Identitätsanbieter. Aber im Lernen ist uns etwas anderes wichtiger: die Mechanik von Spring Security zu verstehen, damit sie nicht in den begleitenden Details untergeht.

Eine Datenbank bringt enormes „Rauschen“ mit sich: Migrationen, SQL, JPA-Mappings, Transaktionen, Lazy-Loading, Testcontainer, Seed-Daten, Eindeutigkeitskonflikte, Umgebungen… Und all das überschattet sofort die Hauptfrage des Tages: wie Spring Security einen Benutzer anhand des Benutzernamens erhält und wie wir den Benutzersatz verwalten.

Das In-Memory-Modell ist ein bewusster Lernkompromiss. Wir ‚frieren‘ vorübergehend die Komplexität des externen Speichers ein, um uns auf die reine Security-Mechanik zu konzentrieren. Wir müssen sehen, dass:

1) Spring Security hat eine Stelle, wo es fragt: “Gib mir den Benutzer mit dem Namen”;

2) wir können die Standard-Benutzerquelle durch unsere eigene ersetzen;

3) das Passwort muss dennoch encoded sein, selbst wenn der Benutzer nur im Speicher lebt.

Und zur Datenbank kommen wir später – mit einem richtig aufgebauten mentalen Modell, um nicht den gesamten Kurs in „Warum bekomme ich eine NullPointerException in Hibernate, wenn ich mich anmelde?“ zu verwandeln.

3. Platz von InMemoryUserDetailsManager in der Kette

Es ist nützlich, kurz zur Authentifizierungsarchitektur zurückzukehren, die wir bereits in Modul 1 besprochen haben, aber sehr anwendungsnah. Spring Security im klassischen Username/Password-Modell (das uns interessiert) führt ungefähr die folgende Aktionskette aus: Filter empfangen die Anfrage, dann wird die Authentifizierung gestartet, dann delegiert der AuthenticationManager die Prüfung an den AuthenticationProvider, dieser greift auf den UserDetailsService zu, und erst dann erfolgt die Passwortprüfung über den PasswordEncoder.

InMemoryUserDetailsManager ist genau die Komponente, die in dieser Kette die Rolle des UserDetailsService (und sogar mehr) spielt. Sie kann anhand des Benutzernamens ein UserDetails-Objekt zurückgeben. Und zwar ohne Datenbank, ohne Netzwerkaufrufe und ohne Überraschungen: einfach aus dem Speicher des aktuellen Prozesses.

Damit es nicht wie ein „Ritual“ aussieht, lassen Sie uns ein Diagramm auf der Ebene „was ruft was auf“ zeichnen (ohne tiefgehende Analyse der inneren Klassen):

flowchart TD
    R["HTTP request"] --> F["Spring Security filters"]
    F --> AM["AuthenticationManager"]
    AM --> AP["AuthenticationProvider
(usually DaoAuthenticationProvider)"] AP --> UDS["UserDetailsService
(InMemoryUserDetailsManager)"] AP --> PE["PasswordEncoder"] PE --> AP UDS --> AP AP --> SC["SecurityContext (authenticated user)"]

In diesem Diagramm ist für uns ein Gedanke wichtig: InMemoryUserDetailsManager ist keine „Rolleneinstellung“ und keine „Zugriffsregeln“. Es ist das „Telefonbuch der Benutzer“ (man möchte manchmal sagen – das „Heft mit Logins“), das Spring Security verwendet, wenn es die Kontodaten laden muss.

4. In-Memory-Authentifizierung: Was ist das?

Wenn wir von „In-Memory-Benutzern“ sprechen, bedeutet das nicht „nur so tun“. Es bedeutet, dass die Benutzerquelle innerhalb der Anwendung festgelegt ist und so lange lebt, wie Ihr JVM-Prozess lebt. Sie starten die Anwendung neu – die Benutzerliste wird von Grund auf neu erstellt, genau nach dem Code (oder der Konfiguration), den Sie geschrieben haben. Kein „wo ist mein Datenbank-Dump“, kein „warum wurden die Migrationen nicht angewendet“.

Aus praktischer Sicht bietet das In-Memory-Modell sehr nützliche Eigenschaften:

Erstens ist es vollständig mit den Augen lesbar. Sie öffnen eine Konfigurationsklasse und sehen, welche Benutzer existieren und welche Berechtigungen sie haben. Für Anfänger ist das unbezahlbar: weniger Magie, mehr Kontrolle.

Zweitens eignet es sich hervorragend zur Demonstration der Zugriffsmatrix. Sie können drei Konten anlegen (user/editor/admin) und manuell überprüfen, was genau geöffnet und geschlossen ist. Und wenn etwas nicht funktioniert, wissen Sie, dass das Problem fast sicher in den Zugriffsregeln oder in der Benennung von Rollen/Authorities liegt, nicht in der Datenbank.

Drittens diszipliniert es. Wenn Sie separate Szenario-Benutzer haben, hören Sie auf, im Modus „lasst uns jedem alles geben, damit es nicht stört“ zu leben – und beginnen, die tatsächlichen Zugriffsgrenzen zu sehen.

Ja, das In-Memory-Modell hat offensichtliche Einschränkungen, aber sie sind gerade lehrreich nützlich: Es zeigt, dass Benutzer ein Teil der Anwendungsarchitektur sind, nicht ein zufälliger „Login irgendwo da drüben“.

5. Erster UserDetailsService für In-Memory

Jetzt machen wir den wichtigsten technischen Schritt des Tages: Wir geben Spring Security eine eigene Benutzerquelle. Dazu müssen wir nur einen @Bean vom Typ UserDetailsService (oder konkret InMemoryUserDetailsManager) deklarieren. Sobald ein solcher Bean im Kontext erscheint, hört Spring Boot auf, den Standardbenutzer mit generiertem Passwort zu erstellen und beginnt, unseren zu verwenden.

Der entscheidende Punkt: Das Passwort geben wir im encoded-Format an. Wir schreiben nicht {bcrypt}... manuell. Wir verwenden den PasswordEncoder, der bereits in der Anwendung konfiguriert ist, und rufen encode(...) auf. Dann ist das Format korrekt und die ganze „Geschichte mit den {id}-Präfixen“ hört auf, abstrakte Theorie zu sein.

Unten – ein minimaler Start: ein Benutzer reader. Das ist noch keine Szenario-Matrix des Projekts. Hier brauchen wir nur eine Tatsache: Spring Security nimmt den Benutzer bereits von unserem UserDetailsService, nicht vom generierten Standardbenutzer. Wenn wir Benutzer mit Rollen und Zugriffsbereichen verknüpfen, wird aus einem solchen minimalen Start ein vollständiger Satz von Szenario-Konten entstehen.

import org.springframework.context.annotation.Bean;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.crypto.password.PasswordEncoder;

@Bean
UserDetailsService users(PasswordEncoder passwordEncoder) {
    // Erstellen eines Benutzers im Format, das Spring Security "out of the box" versteht
    UserDetails user = User.builder()
        .username("reader")
        // Passwort immer im encoded-Format speichern, auch wenn der Benutzer nur im Speicher lebt
        .password(passwordEncoder.encode("reader-pass"))
        // Rolle wird benötigt, damit der Benutzer mit den Zugriffsregeln in SecurityFilterChain übereinstimmen kann
        .roles("USER")
        .build();

    // InMemoryUserDetailsManager – unsere Benutzerquelle (UserDetailsService)
    return new InMemoryUserDetailsManager(user);
}

Hier sind zwei Dinge sofort zu beachten.

Erstens: Wir verwenden User.builder() aus Spring Security (org.springframework.security.core.userdetails.User). Das ist nicht Ihr Domänenbenutzer aus dem Projekt, nicht UserAccount und nicht UserProfile. Es ist die Security-Darstellung des Benutzers, die Spring Security „out of the box“ versteht. Es ist ein bisschen ärgerlich, dass die Klasse einfach User heißt, aber so ist das Leben: Sicherheit mag kurze Namen, damit das Refactoring später lustiger wird.

Zweitens: Wir haben .roles("USER") hinzugefügt. Wir diskutieren noch nicht, welche Rollen wir im Projekt haben werden – das wird der nächste Schritt sein. Hier wird die Rolle einfach benötigt, damit der Benutzer Ihren bereits vorhandenen Zugriffsregeln entsprechen kann.

6. Befüllen des InMemoryUserDetailsManager

Wenn es mehr als einen Benutzer gibt, möchte man den Code so schreiben, dass er lesbar bleibt. Der InMemoryUserDetailsManager bietet zwei bequeme Füllstile. Erster: Benutzer dem Konstruktor übergeben. Zweiter: Manager leer erstellen und Benutzer mit der Methode createUser(...) hinzufügen. Die zweite Variante liest sich oft angenehmer, wenn Sie den Satz von Szenario-Konten schrittweise erweitern.

Das folgende Fragment handelt auch nicht von der finalen Matrix, sondern von der Speicherform: Der InMemoryUserDetailsManager kann einen vorab bekannten Satz von Konten halten und sie erweitern, während Sie das Zugriffsmodell verfeinern.

Hier ist ein Beispiel, wo wir explizit einen Manager erstellen und einen Benutzer hinzufügen. Beachten Sie: Hier geben wir genau InMemoryUserDetailsManager zurück, nicht das Interface UserDetailsService. Damit betonen wir: Wir können nicht nur „einen Benutzer laden“, sondern auch „den Satz verwalten“ (Benutzer im Speicher erstellen).

import org.springframework.context.annotation.Bean;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.crypto.password.PasswordEncoder;

@Bean
InMemoryUserDetailsManager users(PasswordEncoder passwordEncoder) {
    // Manager kann leer erstellt und mit Benutzern wie einer "Charakterliste" befüllt werden
    InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();

    manager.createUser(User.builder()
        .username("editor")
        // Der Passwort-Hash wird bei jedem Start unterschiedlich sein (wegen des Salzes), aber das "rohe" Passwort ist gleich
        .password(passwordEncoder.encode("editor-pass"))
        // Rollen müssen mit dem übereinstimmen, was die Autorisierungsregeln erwarten
        .roles("USER", "EDITOR")
        .build());

    return manager;
}

Dieser Stil ist praktisch, weil er an das „Zusammenstellen von Szenarien“ erinnert. Sie sagen sozusagen: „In unserem Lernprojekt gibt es dieses Konto, und diese Eigenschaften hat es“. Später, wenn wir die vollständige Zugriffsmatrix zusammenbauen, wird dieser Code wie eine „Charakterliste“ gelesen, die durch Ihre API läuft und prüft, wo die Türen offen sind.

Ein weiterer nützlicher Hinweis, der Anfängern im Voraus Nerven spart. passwordEncoder.encode("editor-pass") erzeugt jedes Mal einen neuen Hash (insbesondere bei bcrypt). Und das ist normal. Das bedeutet nicht, dass sich das Passwort ändert, sondern die Darstellung des Passworts im encoded-Format ändert sich, weil der Algorithmus ein Salz hinzufügt. Der Benutzer loggt sich dennoch mit dem gleichen rohen Passwort editor-pass ein.

7. Wiederholbarer Start mit In-Memory-Benutzern

Nach dem Hinzufügen eines eigenen UserDetailsService geschieht etwas sehr Wichtiges: Die Anwendung wird deterministisch. Gestern konnten Sie das Projekt starten und jedes Mal ein neues generiertes Passwort in den Logs erhalten. Heute haben Sie stabile Konten: reader, editor (und später kommen weitere hinzu). Das klingt banal, aber in der Praxis bedeutet es, dass jeder Student in der Gruppe die Schritte wiederholen kann und das Ergebnis gleich ist.

Aus Sicht von Spring Boot ist das auch ein „logisch korrektes Signal“: Sie haben dem Framework gesagt „Danke für den Demo-Modus, ab jetzt mache ich es selbst“. Boot hört auf, den default user unterzuschieben, weil Sie bereits deklariert haben, dass die Anwendung ihr eigenes Benutzermodell hat.

Das ist noch nicht die gesamte Arbeits-Baseline des Tages, aber schon ihr obligatorisches Fundament: Jetzt haben wir einen stabilen Benutzerspeicher, auf dessen Basis wir ruhig über Rollen, punktuelle Rechte und Zugriffsregeln verhandeln können.

Und hier passiert eine interessante pädagogische Magie. Wenn Sie feste Benutzer haben, können Sie die SecurityFilterChain als Zugriffskarte betrachten, nicht als rätselhaftes Gebilde aus DSL. Sie beginnen direkt, die Konfiguration etwa so zu lesen: „Diese Pfade sind öffentlich; diese sind nur für Authentifizierte zugänglich; diese nur für Redakteure“ – und das kann mit konkreten Logins überprüft werden.

Wir diskutieren noch nicht die konkreten Anmeldemethoden (welcher Login-Mechanismus und wie das UI/Flow aussieht). Jetzt ist es wichtig, eines zu verstehen: Ohne eigene Benutzer bleiben alle Zugriffsregeln nur „auf dem Papier“.

8. Einschränkungen und Angemessenheit des In-Memory-Modells

Das In-Memory-Modell hat eine Grenze, und sie wird nicht verheimlicht: Es ist kein Modell für ein echtes öffentliches Produkt, bei dem sich Benutzer registrieren, ihr Passwort ändern, vom Administrator gesperrt werden und zwischen Neustarts des Dienstes leben. In der In-Memory-Welt bedeutet ein Neustart der Anwendung buchstäblich „wir haben alles vergessen und leben wieder nach der Liste im Code“.

Aber das macht den InMemoryUserDetailsManager nicht nutzlos. Im echten Leben wird er oft als technisches Werkzeug eingesetzt. Zum Beispiel für interne Admin-Oberflächen in der frühen Phase, für Prototypen, für die lokale Entwicklung, für sehr kleine Dienste, für Demo-Umgebungen. Und natürlich für das Lernen – weil er es ermöglicht, die Sicherheitsmechanik von der Infrastruktur zu isolieren.

Es ist nützlich, einen einfachen Vergleich im Kopf zu behalten (nicht als „das ist schlecht/gut“, sondern als „es geht um unterschiedliche Aufgaben“):

Benutzerquelle Bedeutung Vorteile Nachteile
Standardbenutzer vom Starter Demo-Modus von Boot schnell secure-by-default sehen Passwort bei jedem Start neu, Sie kontrollieren das Modell nicht
InMemoryUserDetailsManager Benutzer sind in der Anwendung festgelegt Wiederholbarkeit, Transparenz, Einfachheit Lebt nicht zwischen Neustarts, nicht geeignet für echten Benutzer-Lebenszyklus
Datenbank / externer Anbieter Echtkonten-Modell Realismus, Steuerbarkeit, Lebenszyklus Hohe Komplexität und viele Begleitdetails

Genau deshalb verläuft der Kurs schrittweise: zuerst default user (Effekt gesehen), dann In-Memory (gelernt zu verwalten), und erst dann – Datenbank (Realität).

9. Typische Fehler bei In-Memory-Benutzern

Fehler Nr. 1: „Da der Benutzer im Speicher ist, kann man das Passwort beliebig speichern“.
Die häufigste Falle sieht so aus: Der Entwickler schreibt .password("reader-pass") und wundert sich aufrichtig, dass der Login nicht funktioniert oder Spring Security sich über das Passwortformat beschwert. Das In-Memory-Modell ändert nur den Speicherort des Benutzers, hebt aber nicht die grundlegende Security-Hygiene auf. Das Passwort muss encoded sein, und der richtige Weg ist passwordEncoder.encode(...).

Fehler Nr. 2: Verwechslung zwischen „Benutzerquelle“ und „Zugriffsregeln“.
Manchmal versuchen Studenten, den Zugriff zu „reparieren“, indem sie dem Benutzer Rollen hinzufügen, während das Problem tatsächlich in der SecurityFilterChain liegt, oder sie schreiben die Chain um, wenn sie vergessen haben, den benötigten Benutzer hinzuzufügen. Ein praktischer Weg, dies zu vermeiden, ist einfach: Halten Sie immer zwei Dateien nebeneinander und vergleichen Sie sie mit den Augen. In einer beschreiben Sie die Benutzer, in der anderen die Regeln. Sie sollten in der „Sprache“ übereinstimmen (z.B. bei Rollen), aber beantworten unterschiedliche Fragen.

Fehler Nr. 3: Einen einzigen „Superuser“ mit allen Rechten zu belassen.
Das ist eine praktische Abkürzung … genau bis zu dem Moment, in dem Sie die Zugriffsmatrix überprüfen. Wenn Sie nur ein Konto mit maximalen Rechten haben, sehen Sie keine Grenzen. Sie können nicht zwischen „alles richtig geschlossen“ und „alles zufällig offen“ unterscheiden. Daher sind Szenario-Benutzer zwingend erforderlich: mindestens ein normaler und mindestens ein privilegierter.

Fehler Nr. 4: Verwendung von User.withDefaultPasswordEncoder() als Hauptstil.
Diese Methode existiert als Lehrdemonstration, aber sie formt eine sehr schlechte Angewohnheit: „Passwörter werden irgendwo im inneren der Magie kodiert“. In unserem Kurs ist der richtige Ansatz ein expliziter PasswordEncoder-Bean und explizites encode(...) beim Erstellen von Benutzern. So gewöhnen Sie sich an ein realistisches Modell, nicht an Tutorial-Magie.

Fehler Nr. 5: Verwechslung von org.springframework.security.core.userdetails.User mit dem Domänenbenutzer der Anwendung.
In den ersten Schritten ist es sehr leicht, eine eigene Klasse User im Projekt zu schreiben und dann plötzlich den „falschen User“ zu importieren. Das Ergebnis: Der Code sieht normal aus, aber der Compiler verhält sich wie ein strenger Lehrer: „Da ist aber ein ganz anderer Typ“. Die beste Lösung ist, Domänenentitäten von vornherein spezifischer zu benennen (z.B. UserAccount, UserProfile) und den Security-Benutzer als technischen Adapter zu betrachten, der in der Security-Schicht lebt.

1
Aufgabe
Spring Security, Level 7, Lektion 0
Gesperrt
Ein In-Memory-Benutzer und das Laden über `UserDetailsService`
Ein In-Memory-Benutzer und das Laden über `UserDetailsService`
1
Aufgabe
Spring Security, Level 7, Lektion 0
Gesperrt
Öffentlicher Endpunkt zum Lesen des Benutzers aus `InMemoryUserDetailsManager`
Öffentlicher Endpunkt zum Lesen des Benutzers aus `InMemoryUserDetailsManager`
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION