CodeGym /Kurse /Spring Security /Externe Security-Konfiguration

Externe Security-Konfiguration

Spring Security
Level 28, Lektion 0
Verfügbar

1. Hardcodierte Secrets zerstören das Sicherheitsmodell

Konfiguration ist der Ort, an dem Entwickler am häufigsten die Sicherheit brechen – nicht aus böswilliger Absicht, sondern aus Erschöpfung und dem Wunsch, „es soll einfach funktionieren“. Die häufigste Sünde ist es, ein Secret, ein Passwort oder einen Token direkt in den Java-Code zu hardcodieren und sich dann zu wundern, warum die Sicherheit „plötzlich“ zu einem Problem des Teams wurde und nicht einer bestimmten Klasse.

Stellen Sie sich vor, wir hätten einen perfekten JWT-Login und eine Token-Validierung erstellt (wie im Modul über zustandslose Sicherheit), aber die Token-Signatur basiert auf dem String "super-secret-123", der direkt in TokenService liegt. Formal funktioniert die Anwendung. Praktisch ist das bereits ein Vorfall, der nur darauf wartet, einzutreten. Sobald der Code im Repository landet, ist das Geheimnis keines mehr. Selbst wenn das Repository privat ist – „privat“ bedeutet nicht „vor Leck geschützt“: Zugriffe werden vergeben, Kündigungen passieren, CI-Logs landen manchmal dort, wo sie nicht hingehören, und Menschen sind im Allgemeinen kreative Wesen, besonders wenn sie Zugriff auf Git haben.

Dazu gehört auch die „Konfiguration aus Gewohnheit“: wenn Sie aus Bequemlichkeit CORS auf * setzen, die TTL des Tokens auf 10 Jahre, das Avatare-Verzeichnis auf /tmp/uploads, und das dann unbemerkt zu „das war schon immer so“ wird. Daher ist die Hauptthese dieser Vorlesung einfach: Sicherheit besteht nicht nur aus Filtern und Annotationen. Sicherheit betrifft auch die Frage, woher die Werte kommen, die diese Filter verwenden.

Wenn die Zugriffsregeln bereits zusammengestellt und sogar getestet sind, kommt die nächste Fehlerklasse nicht von den Controllern, sondern von den Laufzeitwerten um sie herum. Deshalb schauen wir uns als Nächstes nicht neue Filter an, sondern wie man ein bereits funktionierendes Sicherheitsmodell nicht durch Secrets, TTL und Umgebungskonfigurationen zerstört.

2. Externalisierte Konfiguration in Spring Boot

Externalisierte Konfiguration klingt nach einem Begriff aus der Welt der „Architekten auf hohem Ross“, aber die Idee ist sehr bodenständig: Code sollte stabile Lösungen enthalten, und die Werte sollten von außen kommen. Sie schreiben nicht jedes Mal die SecurityFilterChain um, wenn sich der erlaubte Origin ändert. Sie ändern die Konfiguration. Sie ändern nicht den TokenService, wenn Sie eine andere TTL möchten – Sie ändern die Konfiguration. Und schon gar nicht bauen Sie das Jar neu, nur weil in der Produktion ein anderes Geheimnis verwendet werden soll.

In Spring Boot ist die Konfiguration ein Satz von Quellen, die zu einer einzigen Environment zusammengeführt werden. Die Quellen können unterschiedlich sein: application.yml, application-local.yml, Umgebungsvariablen, Systemeigenschaften, Kommandozeilenargumente. Ein wichtiger Punkt für den beginnenden Entwickler: Boot „errät“ nicht, was Sie meinen. Es wählt deterministisch einen Wert basierend auf Prioritätsregeln aus. Ihre Aufgabe ist es daher, dafür zu sorgen, dass dies vorhersagbar ist und nicht in „bei mir läuft es lokal, aber auf dem Server nicht“ ausartet.

Ein kleines Schema „wie man denkt“ (vereinfacht, ohne alle internen Feinheiten):

flowchart TD
    %% Code – stabil, Werte – kommen aus externen Quellen (application.yml, Profile, Umgebungsvariablen).
    A["Java-Code (stabile Logik)"] --> B["@Value / Environment / Konfigurationsbindung"]
    C["application.yml (Basis)"] --> D["Spring Environment"]
    E["application-local.yml (Überschreibung)"] --> D
    F["UMGEBUNGSVARIABLEN (Secrets/Umgebungsvariablen)"] --> D
    D --> B
    B --> A

Die entscheidende Trennung hier ist: SecurityFilterChain, Handler für 401/403, Owner-Checks und das Rollenmodell – das ist Code und sollte überall gleich sein. Aber jwt.secret, access-token-ttl, allowed-origins, Dateispeicherpfade, Logging-Level – das sind Werte, die sich legitimerweise zwischen Umgebungen unterscheiden. Und wenn sie sich unterscheiden, ist das kein Grund für einen neuen Commit im Code.

3. Was wir im Projekt in die Konfiguration auslagern

Bevor wir YAML anfassen, ist es sinnvoll, sich selbst zu fragen: Welche Werte „ziehen“ vom Code in die Konfiguration um? Hier ist es leicht, ins Extrem zu verfallen und alles zu konfigurieren, einschließlich der Namen von Rollen und Pfad-Mustern, aber das macht das Projekt nicht flexibel, sondern einfach nur unverständlich. Wir brauchen die goldene Mitte: das auslagern, was wirklich von der Umgebung abhängt, und das im Code belassen, was Teil der Architektur ist.

Für unser Projekt sieht ein vernünftiges Minimum so aus: JWT-Secret und TTL des Tokens, CORS-erlaubte Origins, das Avatare-Verzeichnis (weil das auf verschiedenen Maschinen unterschiedliche Pfade sind) sowie einige Flags für den „lokalen Betrieb“ (z. B. Logging-Level). Beachten Sie einen wichtigen Punkt: Wir lagern nicht aus, „wie die Sicherheit aufgebaut ist“, sondern „mit welchen Werten sie arbeitet“.

Beispiel für ein Basis-application.yml (nicht ein riesiges „300-Zeilen-Monster“, sondern genau die Basis):

app:
  security:
    jwt:
      secret: ${APP_SECURITY_JWT_SECRET} # Secret kommt nur von außen: ohne es muss der Start fehlschlagen
      access-token-ttl: 15m              # menschenlesbare Dauer: Spring Boot konvertiert in Duration
    cors:
      allowed-origins: ${APP_SECURITY_CORS_ALLOWED_ORIGINS:http://localhost:3000} # Standard ist sicher für lokal

  storage:
    avatar-dir: ${APP_STORAGE_AVATAR_DIR:./data/avatars} # Pfad hängt von der Umgebung ab (aber es ist kein Secret)

Hier sind drei Ideen erkennbar. Erstens wird das Secret nicht als Wert notiert – stattdessen wird ein Platzhalter ${APP_SECURITY_JWT_SECRET} verwendet. Zweitens wird die TTL als menschenlesbares 15m gelesen, und Boot kann das in Duration umwandeln. Drittens können wir für nicht geheime Werte einen Standardwert angeben, damit der lokale Start nicht zu einer „Konfiguriere erst 12 Umgebungsvariablen, dann bekommst du Kaffee“-Quests wird.

Um bei den Schlüsseln nicht durcheinanderzukommen, ist es hilfreich, sie in einem einheitlichen Stil zu halten. Wir verwenden das einheitliche Präfix app.* und darin app.security.* und app.storage.*. Das hilft sehr, wenn Sie ein Projekt-Audit durchführen und schnell sehen möchten: „Aha, hier ist alles, was die Sicherheit betrifft“.

4. Platzhalter ${...} und Umgebungsvariablen

Platzhalter in YAML sind ein einfaches, aber mächtiges Werkzeug: Sie schreiben im Grunde „der Wert wird soundso sein, aber nimm ihn von außen“. Für Anfänger kann das wie Magie der Stufe „warum kennt YAML plötzlich Umgebungsvariablen“ aussehen, aber technisch gesehen ist es nur eine Substitution durch die Property-Resolution von Spring. Wichtig ist, zwei Formen zu verstehen: ${VAR} und ${VAR:default}.

Die Form ${VAR} bedeutet: „Der Wert muss von außen kommen“. Wenn die Variable nicht existiert, sollte die Anwendung beim Start fehlschlagen. Und das ist hervorragend für Secrets, denn „leise ohne Secret gestartet“ ist fast immer schlechter als „laut sofort fehlgeschlagen“. Wir bevorzugen Fail-Fast: Besser ein Startfehler als ein unbemerkt gestartetes System mit falscher Sicherheit.

Die Form ${VAR:default} bedeutet: „Wenn kein Wert von außen kommt, nimm den Standardwert“. Das ist praktisch für die lokale Entwicklung und nur dort sicher, wo der Standardwert wirklich sicher ist und das System nicht offenlegt. Zum Beispiel ist ein Standard-Allowed-Origin für das lokale Frontend in Ordnung. Ein Standard-JWT-Secret ist eine schlechte Idee, weil es das „Secret“ in eine „hartcodierte Konstante, nur nicht in Java, sondern in YAML geschrieben“ verwandelt.

Hier ist ein guter Ansatz (für ein Studienprojekt) für Secrets und „nur Einstellungen“:

app:
  security:
    jwt:
      secret: ${APP_SECURITY_JWT_SECRET}              # ohne Standard: Secret ist obligatorisch, sonst Fail-Fast
      access-token-ttl: ${APP_SECURITY_JWT_TTL:15m}   # Standard ist möglich, das ist kein "Signaturmaterial"

Warum kann TTL einen Standard haben, Secret aber nicht? Weil TTL ein Verhaltensparameter ist und Secret das Material für die kryptografische Signatur. Wenn Sie versehentlich mit einer falschen TTL starten, ist das unangenehm. Wenn Sie versehentlich mit einem „allgemeinen“ Secret starten, ist das bereits eine Sicherheitsbedrohung: Token können gefälscht werden und Ihre Benutzer werden plötzlich „vertrauensselig“.

Ein weiterer Punkt: Platzhalter sind auch nützlich für Dateipfade. Der Pfad zum Avatare-Verzeichnis hängt von der Maschine ab und manchmal auch davon, ob Sie die Anwendung in einem Container oder auf einem Laptop ausführen. Das ist also definitiv Konfiguration, keine Java-Konstante.

5. Profile: local/test/prod ohne YAML-Kopiererei

Profile sind eine Möglichkeit, der Anwendung zu sagen: „Wir befinden uns jetzt in dieser Umgebung, daher sollten die Werte etwas anders sein“. Der häufigste Anfängerfehler ist es, die gesamte application.yml in application-local.yml, dann in application-dev.yml, dann in application-prod.yml zu kopieren und nach einer Woche festzustellen, dass die Dateien auseinandergelaufen sind und Sie nicht mehr sicher sind, welche Einstellung wo ist. Profile sind nicht zum Kopieren gedacht, sondern für gezielte Überschreibungen.

Das richtige mentale Modell ist: Die Basis-application.yml enthält „vernünftige Standardwerte“ und Platzhalter. Die Profildateien ändern nur das, was sich wirklich unterscheidet. Zum Beispiel wollen wir lokal einen anderen Pfad für Avatare und eine weichere CORS-Richtlinie (weil das Frontend bei uns auf localhost läuft), während wir in Tests vorhersagbare Werte wollen, damit CI keine manuelle Eingabe von Secrets erfordert.

Beispiel für eine application-local.yml, die nicht die Welt kopiert, sondern zwei gezielte Überschreibungen vornimmt:

app:
  storage:
    avatar-dir: ./data/local-avatars # lokal kann man es neben dem Projekt speichern
  security:
    cors:
      allowed-origins: http://localhost:3000 # lokales Frontend auf localhost

So aktivieren Sie das Profil. Die für Anfänger bequemste Möglichkeit ist die Umgebungsvariable SPRING_PROFILES_ACTIVE. Zum Beispiel für Linux/macOS:

export SPRING_PROFILES_ACTIVE=local # Profil aktivieren, damit application-local.yml geladen wird
./gradlew bootRun                   # Anwendung mit dem ausgewählten Profil starten

Für PowerShell unter Windows:

$env:SPRING_PROFILES_ACTIVE="local" # Profil für lokale Entwicklung aktivieren
./gradlew bootRun                   # Anwendung starten

Man kann (und sollte oft) das Profil auch in der IDE über die Run-Konfiguration aktivieren, aber wichtig ist das Prinzip: Ein Profil ist kein „separater Anwendungsmodus“, sondern einfach ein Schalter für Konfigurationsquellen.

Und noch ein praktischer Gedanke: Profile sind nicht nur local/prod. In unserem Kurs ist das Profil test oft nützlich, weil Tests deterministische Einstellungen benötigen. Aber machen Sie daraus nicht „in Tests schalten wir immer alles aus“. In Tests wollen wir eher das Gegenteil: Die Sicherheit soll aktiv sein, aber die Werte sollen kontrollierbar sein.

6. Umgebungsvariablen und Secrets: Aufbewahrung ohne Lecks

Umgebungsvariablen (Env Vars) sind im Wesentlichen ein „Lieferkanal“ für Werte in die Anwendung, ohne den Code zu ändern und ohne sie ins Repository zu committen. Sie werden (sowohl in DevOps als auch im Backend) nicht deshalb geliebt, weil es modern ist, sondern weil sie bequem und sicherer sind, als Secrets in Dateien zu speichern, die leicht versehentlich committet werden können. Dennoch sind Umgebungsvariablen kein magischer Safe: Wenn Sie sie in Logs ausgeben, werden sie zu einem „Geheimnis, das alle gesehen haben“. Die Kultur im Umgang mit Secrets ist daher wichtiger als die spezifische Speichermethode.

Für unser Projekt sieht der minimale Satz an Umgebungsvariablen so aus: APP_SECURITY_JWT_SECRET (Signatur-Secret) und möglicherweise APP_SECURITY_CORS_ALLOWED_ORIGINS und APP_STORAGE_AVATAR_DIR. Davon ist nur das JWT-Secret wirklich sensibel, die anderen sind eher „Umgebungseinstellungen“.

Beispiel, wie man ein Secret setzt und die Anwendung lokal startet (Linux/macOS):

export APP_SECURITY_JWT_SECRET="change-me-in-local-only" # Lokales Secret: NICHT committen, NICHT loggen
export SPRING_PROFILES_ACTIVE=local                      # Lokales Profil aktivieren
./gradlew bootRun                                        # Anwendung starten

Für PowerShell:

$env:APP_SECURITY_JWT_SECRET="change-me-in-local-only" # Lokales Secret: außerhalb des Repositorys halten
$env:SPRING_PROFILES_ACTIVE="local"                    # Lokales Profil aktivieren
./gradlew bootRun                                      # Anwendung starten

Nun zum schmerzhaftesten Punkt: „Wo speichere ich diesen Wert, damit ich ihn nicht jedes Mal manuell eingeben muss?“. Ein typischer Kompromiss für Schulungszwecke ist es, eine Datei sample.env (oder application-local.example.yml) zu erstellen und keine echten Werte zu committen, sondern nur ein Beispiel. Die echten Werte halten Sie entweder in den Umgebungsvariablen Ihres Betriebssystems oder in einer .env-Datei, die in .gitignore eingetragen ist. Wichtig ist nur, Speicherung und Lieferung nicht zu verwechseln: .env ist nur dann nützlich, wenn die Shell, die IDE oder Docker Compose seine Werte tatsächlich in die Prozessumgebung exportiert. Als Standardquelle liest Spring Boot eine solche Datei nicht selbst.

Eine wichtige Gewohnheit für Sicherheitsentwickler: Wenn Sie in einem Pull Request die Zeile secret: "..." sehen, sollte im Gehirn ein Alarm losgehen. Nicht weil wir paranoid sind, sondern weil dies das billigste und häufigste Leck ist.

7. @Value: Konfiguration auslesen und Fail-Fast

@Value("${...}") geht nicht um Sicherheit an sich, sondern um die Anbindung der Konfiguration an den Code. Es sagt Spring einfach: „Spritze hier den Wert aus der Environment ein“. Aber genau hier entscheiden Sie zwei wichtige Dinge: welche Schlüssel die Anwendung wirklich benötigt und wie sich die Anwendung verhalten soll, wenn der Wert fehlt oder falsch ist. Für Sicherheitsparameter ist ein guter Stil Fail-Fast: Besser beim Start fehlschlagen, als irgendwie zu leben.

Beginnen wir mit etwas Einfachem, das unserem Projekt nahe kommt: TokenService, der JWT signiert und die Lebensdauer festlegt. Wir möchten, dass secret und ttl aus der Konfiguration kommen.

import java.time.Duration;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service
public class TokenService {

    private final String secret; // Secret im Speicher des Dienstes, aber niemals loggen
    private final Duration ttl;  // Duration kann bequem aus "15m/30s/1h" direkt bezogen werden

    public TokenService(@Value("${app.security.jwt.secret}") String secret,
                        @Value("${app.security.jwt.access-token-ttl}") Duration ttl) {
        // Hier "hängen" wir die Werte einfach an die Environment an.
        // Die Validierung (Fail-Fast) machen wir besser als separaten Schritt unten.
        this.secret = secret;
        this.ttl = ttl;
    }
}

Beachten Sie eine schöne Sache: Duration kann direkt bezogen werden, wenn in YAML 15m, 30s, 1h steht. Boot führt die Konvertierung selbst durch. Das reduziert die Menge an „manueller Parsing“ und, so seltsam es klingt, verringert die Wahrscheinlichkeit von Sicherheitsfehlern (weil weniger selbstgeschriebener Code, der fehlerhaft sein könnte).

Jetzt fügen wir eine Fail-Fast-Prüfung hinzu. Wenn das Secret leer ist oder nur aus Leerzeichen besteht, brauchen wir keinen „Start ohne Signatur“, sondern einen expliziten Startabbruch. In einem Studienprojekt ist das direkt nützlich: Sie erfahren sofort, dass Sie die Konfiguration vergessen haben.

import java.time.Duration;

import org.springframework.util.Assert;

public TokenService(String secret, Duration ttl) {
    // hasText = nicht null und nicht leer/Leerzeichen: für ein Secret ist das eine grundlegende hygienische Prüfung
    Assert.hasText(secret, "JWT secret must be configured");

    // TTL muss ebenfalls gesetzt sein: sonst wird das Token-Verhalten zufällig/fehlerhaft
    Assert.notNull(ttl, "JWT access token TTL must be configured");

    // Nach den Prüfungen speichern wir die Werte in die Felder des Dienstes
    this.secret = secret;
    this.ttl = ttl;
}

Und noch ein wichtiger Punkt – das Loggen. Es ist sehr verlockend, log.info("JWT secret: {}", secret) zu schreiben, um sicherzustellen, dass es „übernommen wurde“. Das darf man nicht tun. Wenn Sie es unbedingt überprüfen möchten, loggen Sie die Tatsache, nicht den Wert. Zum Beispiel die Länge des Secrets oder einfach true/false (aber auch die Länge ist manchmal zu viel – je weniger Spuren des Secrets, desto besser).

Und ja: @Value ist ein bequemer Einstieg, aber nicht der „einzig richtige“ Weg. In größeren Projekten werden oft strukturiertere Methoden verwendet (z. B. das Gruppieren von Einstellungen in einer separaten Klasse). Aber im Rahmen des Grundlagenkurses ist es uns wichtiger, Disziplin zu lernen: Secret in der Konfiguration, Schlüssel sind stabil, Werte kommen von außen, die Anwendung stürzt ab, wenn das Secret nicht gesetzt ist.

8. Typische Fehler

Konfigurationsfehler sehen selten aus wie „unser Secret ist durchgesickert“. Häufiger sehen sie aus wie „warum sind meine Tests fehlgeschlagen“, „warum kann das Frontend nicht zugreifen“, „warum ist der Token plötzlich unendlich gültig“. Daher ist es nützlich, im Voraus zu wissen, welche Rechenfehler am häufigsten vorkommen, um nicht mit fröhlichem Gesicht hineinzutreten.

Fehler Nr. 1: Ein Secret „mal schnell“ im Java-Code hardcodieren.
Der heimtückischste Punkt hier ist, dass „mal schnell“ sich leicht in „ein halbes Jahr“ verwandelt. Dann ist das Secret bereits in allen Umgebungen, es taucht im Pull Request auf, es lebt in der Git-Historie, und selbst wenn Sie es gelöscht haben – es wurde trotzdem irgendwann veröffentlicht. Die Lösung liegt nicht im „Löschen der Zeile“, sondern in der Disziplin: Secrets nur über externalisierte Konfiguration, und für die lokale Entwicklung über Umgebungsvariablen oder nicht committete Dateien.

Fehler Nr. 2: Einen Standardwert für ein Secret über ${VAR:default} angeben.
Auf den ersten Blick ist das praktisch: Die Anwendung startet immer. Auf den zweiten Blick haben Sie selbst ein „universelles Secret“ geschrieben und es jedem geschenkt, der das Repository liest. Für ein Secret ist es besser, „gar nicht zu starten“, als mit einem Standardwert zu starten. Ein Standardwert ist sinnvoll für erlaubte Origins, Verzeichnispfade und andere nicht sensible Dinge, aber nicht für die Tokensignatur.

Fehler Nr. 3: Die gesamte application.yml in application-local.yml kopieren und in zwei Realitäten leben.
Zuerst scheint es einfacher: „Ich habe einfach eine lokale Version gemacht“. Dann stellt sich heraus, dass Sie die TTL in einer Datei geändert, in der anderen vergessen haben, und Sie haben dasselbe Projekt mit zwei verschiedenen Sicherheitsmodellen. Eine Profildatei sollte klein und gezielt sein: zwei oder drei Werte überschrieben – und gut ist.

Fehler Nr. 4: Echte Secrets in application-local.yml oder .env committen.
Hier greift die „Regel des versehentlichen Klicks“: Sie können sicher sein, dass Sie nicht committen werden, aber eines Tages zeigt die IDE die Datei als geändert an, Sie klicken auf „Commit all“, und schon ist es passiert. Also halten Sie Secrets entweder nur in Umgebungsvariablen, ignorieren Sie Dateien mit Secrets strikt über .gitignore oder committen Sie nur *.example.*-Versionen ohne echte Werte.

Fehler Nr. 5: Secrets „zum Debuggen“ loggen.
Logs sind kein Mülleimer, sie sind eine Kopie Ihres Lebens, die dann in Dateien, Aggregatoren, CI, Tickets und Chats lebt. Wenn Sie einmal ein JWT-Secret in ein Log ausgegeben haben, ist es kein Geheimnis mehr, selbst wenn Sie das Log später „gelöscht“ haben. Zum Debuggen der Konfiguration loggen Sie die Tatsache des Vorhandenseins und das aktive Profil, aber nicht den Wert. Und drucken Sie auf keinen Fall den gesamten Authorization-Header aus oder geben Sie rohe JWT-Token aus (der Token enthält ebenfalls Daten, die nicht immer in Logs verstreut werden sollten).

1
Aufgabe
Spring Security, Level 28, Lektion 0
Gesperrt
JWT-Konfiguration aus externer Konfiguration
JWT-Konfiguration aus externer Konfiguration
1
Aufgabe
Spring Security, Level 28, Lektion 0
Gesperrt
Profil local fuer Laufzeiteinstellungen
Profil local fuer Laufzeiteinstellungen
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION