CodeGym /Kurse /Spring Security /DB-backed Users ohne Ausuferung

DB-backed Users ohne Ausuferung

Spring Security
Level 14, Lektion 4
Verfügbar

1. Sinn von DB-backed Users

Die Kontoform, die Grenze zwischen account und profile sowie das Rollenwörterbuch sind bei uns bereits festgelegt. Jetzt müssen wir das Ganze nur noch in das Projekt übertragen, sodass die Konten im persistence layer leben, ohne eine Generalüberholung zu verursachen: Bei den Worten „wir stellen auf DB um“ neigt die Hand eines Neueinsteigers normalerweise dazu, Controller, Services und die halbe Security-Konfiguration umzuschreiben. Genau diesen Impuls gilt es hier zu bremsen.

Der Sinn von DB-backed Users ist hier einfach: Das Konto ist nicht länger Teil der Konfiguration, sondern wird zu Daten. Es überlebt einen Neustart, Rollen und Zustände leben im persistence layer, und die access matrix sowie der SecurityFilterChain bleiben sinngemäß dieselben. Wichtig ist genau diese Stütze: Die Datenbank speichert bereits den Benutzer, und der DaoAuthenticationProvider liest noch nicht direkt daraus, bis wir dieses Modell mit dem UserDetailsService verknüpfen.

Um dies im Kopf zu verankern, hilft eine einfache Skizze (sie geht nicht um „Klassen“, sondern um „Sinn“):

flowchart LR
  A["SecurityFilterChain + access rules"] --> B["Authentication uses users"]
  B --> C["User source"]
  C --> D["InMemory: Benutzer als Konfiguration"]
  C --> E["DB: Benutzer als Daten"]

Falls Ihnen aufgefallen ist, dass sich beim Umstieg auf die DB plötzlich die URL-Regeln geändert haben, einige Endpoints verschwunden sind und die Hälfte der Antworten anders aussieht – dann ist das mit ziemlicher Sicherheit nicht die „unvermeidliche Komplexität“, sondern einfach, weil Sie zu viele Änderungen auf einmal vorgenommen haben.

2. Prepared checkpoint: Fokus behalten

Genau deshalb brauchen wir hier einen prepared checkpoint: Der persistence layer ist bereits so weit in einen arbeitsfähigen Zustand versetzt, wie es für die Security-Betrachtung nötig ist. Wir tauchen nicht tief in JPA ein und bauen keine User-Management-Plattform; wir brauchen nur, dass das Projekt einen echten persistenten User Store erhält.

Prepared checkpoint ist wie eine eingebaute Küche in der Wohnung: Sie kommen, um kochen zu lernen, nicht, um den Herd zu löten und die Leitungen durch die Wände zu ziehen. Der Herd steht schon, und wir geben ehrlich zu: Ja, das Anbinden der Data-Layer ist auch wichtig, aber jetzt brauchen wir eine funktionierende Benutzerdatenbank, keinen separaten Kurs über Persistence.

Es ist nützlich, eine kleine Tabelle der Grenzen im Kopf zu behalten. Sie hilft, nicht auszuufern:

In dieser Lektion tun wir bewusst In dieser Lektion tun wir bewusst nicht
Persistence-Abhängigkeiten und Datasource-Konfiguration einbinden Wir optimieren keine Abfragen und diskutieren keine JPA-Performance
Wir legen fest, wo im Projekt UserAccount, Rollen und Seed-Daten leben Wir tauchen nicht tief in Mappings und Feinheiten von Beziehungen ein
Wir erstellen Startbenutzer in der DB mit einem ordentlichen PasswordEncoder Wir bauen keine „ideale“ User-Management-Plattform
Wir behalten die bisherige Bedeutung von SecurityFilterChain und access matrix bei Wir schreiben die API nicht um und zerstören keine Schichten

Und ja, das ist der Fall, in dem „wir diskutieren jetzt nicht“ keine Schwäche ist, sondern Disziplin. Der Kurs über Sicherheit profitiert davon, dass Sie den Faden nicht verlieren.

3. Abhängigkeiten und application.yml

Bevor wir Klassen schreiben, ist es wichtig, den Boden vorzubereiten: Abhängigkeiten und Konfiguration. Das ist langweilig (wie das Lesen eines Lizenzvertrags), aber es spart stundenlanges „warum startet es nicht“ und „warum ist bei mir alles rot“. Wir machen genau das Minimum, damit das Projekt eine Verbindung zu Postgres herstellen und funktionieren kann.

Gradle-Abhängigkeiten: JPA und PostgreSQL

Der Sinn ist einfach: Security bleibt Security, und Persistence ist einfach nur eine neue Datenquelle. Daher fügen wir spring-boot-starter-data-jpa und den PostgreSQL-Treiber hinzu, ohne den Build in eine Suppe aus zufälligen Bibliotheken zu verwandeln.

dependencies {
    // JPA + Hibernate: Verleihe der Anwendung eine Persistenzschicht
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")

    // Datenbanktreiber wird nur zur Laufzeit benötigt (für JDBC-Verbindung)
    runtimeOnly("org.postgresql:postgresql")

    // Security war bereits vorhanden: Zugriffsregeln bleiben gleich
    implementation("org.springframework.boot:spring-boot-starter-security")
}

Beachten Sie: PostgreSQL ist runtimeOnly. Wir müssen den Code nicht „mit PostgreSQL“ kompilieren, sondern nur zur Laufzeit in der Lage sein, eine Verbindung zur Datenbank aufzubauen.

Datasource in application.yml: Arbeitsminimum

Als Nächstes geben wir die Verbindungsparameter an. In einem Übungsprojekt ist es praktisch, einfach zu beginnen: ein lokaler Postgres, eine verständliche URL und (vorübergehend) eine einfache DDL-Strategie. Ja, in einem echten Projekt sind Migrationen wichtiger, aber wir behalten den Fokus.

spring:
  datasource:
    # JDBC URL für lokalen Postgres
    url: jdbc:postgresql://localhost:5432/secure_content
    username: secure_content
    password: secure_content
  jpa:
    hibernate:
      # Lernmodus: Hibernate aktualisiert das Schema selbst
      # (in der Produktion verwendet man normalerweise Migrationen)
      ddl-auto: update

ddl-auto: update funktioniert im Übungskontext wie „Selbstmontage von IKEA-Möbeln“: stellenweise schief, aber schnell. In der Produktion ist das riskant, aber hier hilft es, uns nicht mit der manuellen Schemaerstellung abzulenken.

4. Persistenzschicht des Benutzers

Wenn eine Datenbank ins Projekt kommt, kann leicht eine neue Art von Chaos entstehen: Pakete werden wahllos benannt, und dasselbe Account-Modell bekommt drei verschiedene Versionen. Hier ist es wichtig, dasselbe Projektbild beizubehalten: UserAccount, UserProfile und Role bleiben dieselben Entitäten, nur das Konto bekommt eine Persistenz-Repräsentation und ein Repository.

Eine saubere Variante (nicht die einzige, aber verständlich):

  • com.example.securecontent.security.account – Modell des Kontos und zugehöriger Security-Code
  • com.example.securecontent.security.account.persistence – JPA-Entity + Repository
  • com.example.securecontent.security.role – Rollenwörterbuch
  • com.example.securecontent.profile – Profil und dessen Persistenz

Und hier gilt eine wichtige Regel: Eine JPA-Entity ist nicht Ihr REST-DTO und auch nicht Ihr „universeller User“. Es ist ein Objekt zur Speicherung. Es sollte minimal und vorhersagbar sein.

Rollen als enum

Die Rolle ist Teil des Modells, keine Zeichenketten-Magie. Daher legen wir das Wörterbuch fest:

package com.example.securecontent.security.role;

// Rollen als enum festlegen, um "magische Zeichenketten" im Code und in der DB zu vermeiden
public enum Role {
    USER,
    EDITOR,
    ADMIN
}

Ja, das sieht fast zu einfach aus. Und genau deshalb ist es gut: Je weniger Stellen, an denen man Fehler machen kann, desto weniger „mystische 403“ haben Sie.

UserAccountEntity: minimale JPA-Entität

Unten sehen Sie nicht eine neue Kontoform, sondern eine kurze JPA-Repräsentation desselben UserAccount-Modells, das wir bereits festgelegt haben. passwordHash bleibt der Hash, Rollen sind ein Set, Konto-Status werden als separate Flags abgebildet; in diesem Fragment wurden roles, enabled und accountNonLocked der Kürze halber weggelassen.

package com.example.securecontent.security.account.persistence;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;

@Entity
public class UserAccountEntity {

    @Id
    @GeneratedValue
    private Long id;

    // Login (normalerweise eindeutig); daran werden wir den Benutzer bei der Authentifizierung suchen
    private String username;

    // E-Mail getrennt vom username: Vermischung von Anmeldekennung und Kontaktdaten vermeiden
    private String email;

    // Hier genau der Passwort-Hash (nicht rohes Passwort)
    private String passwordHash;
}

Das heißt, hier legen wir die Persistenz-Grenze fest, anstatt einen „neuen Benutzer“ zu zeichnen.

Repository: Einstiegspunkt in die Persistenz

Damit Seeding und zukünftige Operationen mit Konten als Daten arbeiten können, brauchen wir ein Repository. Minimale Version:

package com.example.securecontent.security.account.persistence;

import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserAccountRepository extends JpaRepository<UserAccountEntity, Long> {

    // Finde Konto nach username: dies ist der "Einstiegspunkt" für den Login
    Optional<UserAccountEntity> findByUsername(String username);
}

Ein wichtiger Punkt: Die Existenz von findByUsername ist nicht „Schönheit um der Schönheit willen“. Es ist die grundlegende Art auszudrücken, dass username der Login und eine eindeutige Kennung ist. Selbst wenn auf DB-Ebene noch kein Constraint hinzugefügt wurde (oder bereits), wird dies auf Code-Ebene bereits richtig gelesen.

5. Seed-Daten: Startbenutzer

Wenn Studierende zum ersten Mal auf die DB umsteigen, stoßen sie oft auf das Problem: Die Anwendung startet, aber „niemand kann sich anmelden“, die Rollen kommen von unerklärlichen Orten, und die Sicherheitsüberprüfung wird zu einer Quest „erstelle den Benutzer manuell in der Tabelle, ohne Formatfehler“. Für ein Übungsprojekt ist das zu viel Aufmerksamkeitskosten, daher brauchen wir Seed – Startkonten.

Seed-Daten sind kein Cheaten. Es ist wie das „Startpaket an Teilen“ in einem Baukasten: Es wird benötigt, damit Sie sofort das Modell zusammenbauen können, anstatt zum Laden für Schrauben zu rennen.

Seeder + PasswordEncoder

Hauptdisziplin: Wir speichern nirgendwo rohe Passwörter. Auch nicht im Seeder. Auch nicht „nur für einen Moment“. Auch nicht, wenn es sehr verlockend ist.

Hier ist ein kompaktes Beispiel eines Seeders, der drei Benutzer mit dem Passwort "password" (Übungspasswort, nicht so in einem echten Projekt machen) und verschiedenen Rollen erstellt:

import com.example.securecontent.security.account.persistence.UserAccountEntity;
import com.example.securecontent.security.role.Role;
import java.util.Set;
import org.springframework.security.crypto.password.PasswordEncoder;

public class UserAccountSeeder {

    public UserAccountEntity user(PasswordEncoder encoder) {
        // Erstelle die Entity so, wie wir einen "echten" Benutzer anlegen würden:
        // zuerst Daten, dann sichere Umwandlung des Passworts in einen Hash
        UserAccountEntity a = new UserAccountEntity();

        a.setUsername("user");

        // Passwort nicht im Klartext speichern – nur Hash
        a.setPasswordHash(encoder.encode("password"));

        // Rollen explizit setzen: wichtig für vorhersagbare hasRole/hasAuthority-Prüfungen
        a.setRoles(Set.of(Role.USER));

        return a;
    }
}

Ja, setRoles(...) verweist auf die Felder des Account-Modells, die oben nicht ausgeführt wurden. Das ist in Ordnung: Seed sollte demselben Rollen- und Zustandsmodell gehorchen, nicht sein eigenes erfinden.

Der Sinn dieses Beispiels ist nicht, dass man Seeder genau so schreiben „muss“. Der Sinn ist, dass Seeding denselben Sicherheitsregeln gehorcht wie das normale Erstellen eines Benutzers. Wenn der Seed ein rohes Passwort speichert, lehren Sie das Gehirn eine schlechte Angewohnheit. Und das Gehirn, wie bekannt, wiederholt das später in der Produktion „versehentlich“.

Wann Seeding ausführen

Ein sehr häufiger Fehler ist es, bei jedem Start Benutzer zu säen und Duplikate, Konflikte oder seltsame Zustände zu erhalten. Eine normale Regel: Wenn es bereits Konten gibt, schweigt der Seeder.

import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;

@Bean
ApplicationRunner seedUsers(UserAccountRepository repo, PasswordEncoder encoder) {
    return args -> {
        // Nur die leere Datenbank befüllen, um bei jedem Start Duplikate zu vermeiden
        if (repo.count() == 0) {
            // Hier könnte Ihr factory/seeder helper stehen, der die Startbenutzer erstellt
            repo.save(UserAccountFactory.user(encoder));
        }
    };
}

Das ist ein einfacher Mechanismus, der den Projektstart vorhersagbar macht: Der erste Start erstellt Konten, die folgenden zerstören die Daten nicht.

6. SecurityFilterChain unverändert

Nach dem Umstieg auf die DB ändert sich die Benutzerquelle, nicht die Zugriffskarte. Öffentliche, /me, Editor- und Admin-Bereiche bleiben dieselben, daher schreiben wir den SecurityFilterChain sinngemäß nicht um.

SecurityFilterChain bleibt gleich

Hier ist ein Beispiel (das wir bereits früher geschrieben haben): Der öffentliche Bereich ist offen, der Admin-Bereich erfordert eine Rolle, der Rest – authenticated.

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    // Konfiguriere Zugriffsregeln für URLs – die Benutzerquelle (in-memory/DB) wird hier nicht beschrieben
    http.authorizeHttpRequests(auth -> auth
        // Öffentliche Endpunkte sind für alle zugänglich
        .requestMatchers("/api/public/**").permitAll()

        // Admin-Endpunkte erfordern die Rolle ADMIN
        .requestMatchers("/api/admin/**").hasRole("ADMIN")

        // Alles andere nur für authentifizierte Benutzer
        .anyRequest().authenticated()
    );

    return http.build();
}

Und jetzt die entscheidende Verbindung: Hier leben die Rollen in der Domäne und der DB als USER, EDITOR, ADMIN. An der Grenze zu Spring Security werden sie zu Authorities mit dem Präfix ROLE_, und genau dort erhält hasRole("ADMIN") seine erwartete Bedeutung. Daher brauchen wir auf Speicherebene eine Disziplin: ADMIN, nicht admin und kein gemischtes Set aus ADMIN/ROLE_ADMIN durcheinander.

Controller und Endpoints – dieselben

Der ehrlichste Test „wir sind nicht ausgeufert“ klingt so: Öffentliche Endpunkte, /api/me, Editor-/Admin-Endpoints bleiben dieselben wie gestern. Wir ändern die Benutzerquelle, nicht den API-Vertrag.

Zum Beispiel bleibt /api/me der persönliche Bereich:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MeController {

    @GetMapping("/api/me")
    public String me() {
        // Wichtig ist nicht der Inhalt der Antwort, sondern die Tatsache:
        // der Endpunkt bleibt derselbe, und der Zugriff wird durch SecurityFilterChain kontrolliert
        return "authenticated zone";
    }
}

Ja, vorerst ist die Antwort einfach. Aber der Sinn ist wichtiger: Ein Endpunkt muss sich nicht ändern, nur weil der Benutzer in die DB umgezogen ist.

7. Überprüfung des Wechsels

Nach dem Umstieg auf die DB entsteht bei Neueinsteigern der natürliche Wunsch, „alles auf einmal zu überprüfen“ und dann noch „ein bisschen“ Abfragen zu optimieren und „kurz“ Tabellen umzuschreiben. Das ist eine Falle. Wir überprüfen grundlegende, einfache Dinge, die zeigen: Das Projekt stützt sich jetzt auf persistente Konten, und das hat das bisherige Zugriffsbild nicht zerstört.

Eine Überprüfung auf gesundem Menschenverstand besteht normalerweise aus drei Beobachtungen. Erstens: Die Anwendung startet und verbindet sich ohne roten Text auf einem halben Bildschirm mit der Datenbank. Zweitens: Die Seed-Konten sind in der Tabelle erschienen und haben die erwarteten Rollen und den passwordHash (eben Hash, nicht „password“). Drittens: Der SecurityFilterChain verhält sich weiterhin gleich: Der öffentliche Bereich ist offen, der private Bereich erfordert Authentifizierung, der Admin-Bereich erfordert eine Rolle.

Wenn diese drei Punkte zutreffen, ist der Checkpoint gesammelt: Die Datenbank fungiert bereits als Kontospeicher, und die Zugriffskarte ist sinngemäß gleich geblieben.

Wenn Sie eine kleine Diagnose direkt im Code machen möchten (ohne neue Endpoints und ohne Postman-Akrobatik), können Sie vorübergehend die Anzahl der Benutzer beim Start in das Log ausgeben. Das ist ein „Beweis des Lebens“ der Persistenz.

import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;

@Bean
ApplicationRunner logUsers(UserAccountRepository repo) {
    return args -> System.out.println("Accounts in DB = " + repo.count()); // Accounts in DB = 3
    // Für die Übungsüberprüfung ist System.out in Ordnung, aber verwenden Sie in einem echten Projekt einen richtigen Logger
}

Ja, System.out.println ist kein „idealer Logger“, aber für eine Übungsüberprüfung ist das in Ordnung. Hauptsache: Sie sehen die Zahl und verstehen, dass die Datenbank wirklich am Leben der Anwendung teilnimmt.

8. Typische Fehler bei DB-backed Users

Fehler Nr. 1: „Lass uns alles auf einmal ändern, das ist schneller.“
Das führt fast immer zum gegenteiligen Effekt: Sie ändern Storage, Konfiguration, Zugriffsregeln und Endpoints – und verlieren die Fähigkeit zu verstehen, was genau kaputt gegangen ist. Das Ergebnis ist der klassische Entwicklerzustand: „Es funktioniert nicht, aber ich weiß nicht warum.“ Die Behandlung ist langweilig: Ändern Sie jeweils eine Komplexitätsachse und lassen Sie den Rest unverändert, bis der neue Teil vorhersagbar ist.

Fehler Nr. 2: Seed-Daten mit rohen Passwörtern.
Das passiert oft nicht aus böser Absicht, sondern „weil es schneller geht“. Aber es zerstört das mentale Modell des Kurses: Ein Passwort wird niemals als roher Wert gespeichert. Wenn Sie sich einmal erlauben, Seed mit rohem Passwort zu machen, merkt sich das Gehirn, dass das „manchmal okay ist“. Sicherheit aber handelt genau davon, dass „manchmal okay“ zu „warum ist es in der Produktion noch so“ wird.

Fehler Nr. 3: Rollen beginnen in verschiedenen Formaten an verschiedenen Orten zu leben.
Zum Beispiel im Code hasRole("ADMIN"), in der Datenbank wird "admin" gespeichert, und in der README steht "ROLE_ADMIN". Das ist ein garantierter Weg zu einer 403 und anschließender Wut auf Spring Security (obwohl es nichts damit zu tun hat). Hier hilft eine strenge Konvention: Rollen sind ein enum, in der DB speichern wir dieselben Werte und prüfen sie auf dieselbe Weise.

Fehler Nr. 4: UserAccount wird zu einem „kombinierten Alleskönner“.
Wenn eine DB auftaucht, möchte man sofort displayName, bio, avatarPath, UI-Einstellungen, die Lieblingsfarbe des Buttons und, zur Sicherheit, die Liste der Entwürfe hineinlegen. Ergebnis: eine schwere Entität, die man kaum nach außen geben möchte und die ständig „versehentlich“ Security-Daten in Profilantworten mitnimmt. Der richtige Schritt ist, die Grenze einzuhalten: UserAccount – Security-Kern, UserProfile – anwendungsbezogene Daten.

Fehler Nr. 5: „Ich muss eine DB anbinden, also muss ich den SecurityFilterChain umschreiben.“
Nein, das ist nicht nötig. Der SecurityFilterChain beschreibt die Zugriffsregeln für URL-Bereiche, nicht die Benutzerquelle. Wenn Sie die Zugriffsregeln nur umgeschrieben haben, weil Sie auf die DB umgestiegen sind, haben Sie ein Risiko hinzugefügt, die falschen Endpoints zu öffnen oder zu schließen – und das hat mit der Aufgabe des aktuellen Schritts überhaupt nichts zu tun.

1
Aufgabe
Spring Security, Level 14, Lektion 4
Gesperrt
Projektgerüst mit persistenten Benutzern
Projektgerüst mit persistenten Benutzern
1
Aufgabe
Spring Security, Level 14, Lektion 4
Gesperrt
Access Matrix unabhängig von der Benutzerquelle
Access Matrix unabhängig von der Benutzerquelle
1
Umfrage/Quiz
Spring-Benutzer, Level 14, Lektion 4
Nicht verfügbar
Spring-Benutzer
Konten und Rollen speichern
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION