optimierungen auth-Modul und cache-Modul

This commit is contained in:
stefan
2025-09-03 15:19:11 +02:00
parent abd2543caf
commit 63a1b97db7
16 changed files with 578 additions and 797 deletions
+88 -8
View File
@@ -1,4 +1,4 @@
# Infrastructure/Auth Module - Comprehensive Documentation # Infrastructure/Auth Modul Aktuelle Dokumentation (Stand: September 2025)
## Überblick ## Überblick
@@ -6,6 +6,21 @@ Das **Auth-Modul** ist die zentrale Komponente für die gesamte Authentifizierun
Als Identity Provider wird **Keycloak** verwendet. Dieses Modul kapselt die gesamte Interaktion mit Keycloak und stellt dem Rest des Systems eine einheitliche und vereinfachte Sicherheitsschicht zur Verfügung. Als Identity Provider wird **Keycloak** verwendet. Dieses Modul kapselt die gesamte Interaktion mit Keycloak und stellt dem Rest des Systems eine einheitliche und vereinfachte Sicherheitsschicht zur Verfügung.
## Aufgabe des Moduls
- Zentrale Bereitstellung von Authentifizierungs- und Autorisierungsfunktionen für alle Services
- Minimierung der Kopplung an Keycloak durch eine API/Client-Abstraktion (`auth-client`)
- Einheitliche, typsichere Repräsentation von Rollen und Berechtigungen als Enums
- Sichere Erzeugung, Validierung und Auswertung von JWTs (Issuer, Audience, Ablauf, Signatur)
- Bereitstellung eines dedizierten Auth-Servers für Benutzer-Workflows (Login, optional Passwortänderung, Token-Ausstellung)
## Umsetzung (High-Level)
- Authentifizierung findet gegen Keycloak statt; der `auth-server` kapselt dessen Aufrufe.
- Nach erfolgreicher Authentifizierung wird ein signiertes JWT erzeugt, das Rollen/Berechtigungen enthält.
- Downstream-Services validieren das JWT über den `auth-client` und führen autorisierte Domänenaktionen aus.
- Das API-Gateway kann JWTs vorvalidieren und Metadaten-Header weitergeben; vollständige Validierung sollte via `auth-client` erfolgen.
## Architektur ## Architektur
Das Auth-Modul ist in zwei spezialisierte Komponenten aufgeteilt, um eine klare Trennung der Verantwortlichkeiten zu gewährleisten: Das Auth-Modul ist in zwei spezialisierte Komponenten aufgeteilt, um eine klare Trennung der Verantwortlichkeiten zu gewährleisten:
@@ -20,12 +35,17 @@ infrastructure/auth/
Dieses Modul ist eine **wiederverwendbare Bibliothek** und kein eigenständiger Service. Es enthält die gesamte Logik, die andere Microservices (wie `masterdata-service`, `members-service` etc.) benötigen, um ihre Endpunkte abzusichern. Dieses Modul ist eine **wiederverwendbare Bibliothek** und kein eigenständiger Service. Es enthält die gesamte Logik, die andere Microservices (wie `masterdata-service`, `members-service` etc.) benötigen, um ihre Endpunkte abzusichern.
**Hauptaufgaben:** Aktueller Stand (09/2025):
* **JWT-Management:** Stellt einen `JwtService` zur Erstellung und Validierung von JSON Web Tokens bereit. - Enthält ein typensicheres Rollen- und Berechtigungsmodell: `RolleE`, `BerechtigungE` (kotlinx.serialization-annotiert für konsistente JSON-Serialisierung).
* **Modell-Definition:** Definiert die **Quelle der Wahrheit** für sicherheitsrelevante Konzepte wie `RolleE` und `BerechtigungE` als typsichere Kotlin-Enums. Dies stellt sicher, dass alle Services dieselbe "Sprache" für Berechtigungen sprechen. - Definiert die Schnittstelle `AuthenticationService` mit suspend-Funktionen und Result-Typen zur Authentifizierung und Passwortänderung. Rückgabewerte sind versiegelt (sealed) und decken Success/Failure/Locked ab. Dadurch klare, explizite Fehlerfälle ohne Exceptions in Kontrollflüssen.
* **Schnittstellen:** Bietet saubere Schnittstellen wie `AuthenticationService` an, die von der konkreten Implementierung (z.B. Keycloak) abstrahieren. - Stellt den `JwtService` bereit, der via Spring konfiguriert werden kann und in Services zur Token-Erzeugung/-Validierung genutzt wird.
Jeder Microservice, der geschützte Endpunkte anbietet, bindet dieses Modul als Abhängigkeit ein. **Hauptaufgaben:**
* **JWT-Management:** Stellt einen `JwtService` zur Erstellung und Validierung von JSON Web Tokens bereit (Signatur, Claims, Ablaufzeiten). Neue, result-basierte APIs erleichtern das Fehler-Handling.
* **Modell-Definition:** Definiert die **Quelle der Wahrheit** für sicherheitsrelevante Konzepte wie `RolleE` und `BerechtigungE` als typsichere Kotlin-Enums. Dies stellt sicher, dass alle Services dieselbe "Sprache" für Berechtigungen sprechen.
* **Schnittstellen:** Bietet saubere Schnittstellen wie `AuthenticationService` an, die von der konkreten Implementierung (z.B. Keycloak) abstrahieren. Dadurch können Implementierungen im `auth-server` oder in Tests (Mocks/Fakes) ausgetauscht werden.
Einbindung: Jeder Microservice, der geschützte Endpunkte anbietet, bindet dieses Modul als Abhängigkeit ein.
### `auth-server` ### `auth-server`
@@ -36,6 +56,42 @@ Dies ist ein **eigenständiger Spring Boot Microservice**, der als Brücke zwisc
* **Token-Endpunkte:** Ist verantwortlich für das Ausstellen von Tokens nach einer erfolgreichen Authentifizierung. * **Token-Endpunkte:** Ist verantwortlich für das Ausstellen von Tokens nach einer erfolgreichen Authentifizierung.
* **Implementierung der `AuthenticationService`-Schnittstelle:** Enthält die konkrete Logik, die gegen Keycloak prüft, ob ein Benutzername und ein Passwort korrekt sind. * **Implementierung der `AuthenticationService`-Schnittstelle:** Enthält die konkrete Logik, die gegen Keycloak prüft, ob ein Benutzername und ein Passwort korrekt sind.
**Konfiguration (AuthServerConfiguration):**
Der Service stellt einen konfigurierbaren `JwtService` per Spring-Bean bereit. Die dazugehörigen Properties werden über `auth.jwt.*` gesetzt:
```yaml
auth:
jwt:
secret: <32+ Zeichen starkes Secret>
issuer: meldestelle-auth-server
audience: meldestelle-services
expiration: 60 # Minuten
```
Kotlin-Konfiguration (vereinfacht):
```kotlin
@Configuration
@EnableConfigurationProperties(JwtProperties::class)
class AuthServerConfiguration {
@Bean
fun jwtService(props: JwtProperties) = JwtService(
secret = props.secret,
issuer = props.issuer,
audience = props.audience,
expiration = props.expiration.minutes
)
@ConfigurationProperties(prefix = "auth.jwt")
data class JwtProperties(
val secret: String,
val issuer: String,
val audience: String,
val expiration: Long
)
}
```
Hinweis: Standardwerte sind nur für lokale Entwicklung gedacht und müssen in Produktion überschrieben werden. Zusätzlich validiert der Auth-Server die JWT-Properties: Secret min. 32 Zeichen, issuer/audience nicht leer; bei Verwendung des Default-Secrets wird eine Laufzeit-Warnung ausgegeben.
## Zusammenspiel im System ## Zusammenspiel im System
1. Ein **Benutzer** meldet sich über eine Client-Anwendung am **`auth-server`** an. 1. Ein **Benutzer** meldet sich über eine Client-Anwendung am **`auth-server`** an.
@@ -43,10 +99,11 @@ Dies ist ein **eigenständiger Spring Boot Microservice**, der als Brücke zwisc
3. Bei Erfolg erstellt der `auth-server` mit dem `JwtService` aus dem `auth-client` ein JWT, das die Berechtigungen des Benutzers enthält, und sendet es an den Client zurück. 3. Bei Erfolg erstellt der `auth-server` mit dem `JwtService` aus dem `auth-client` ein JWT, das die Berechtigungen des Benutzers enthält, und sendet es an den Client zurück.
4. Der **Client** sendet eine Anfrage an einen anderen Microservice (z.B. `members-service`) und fügt das JWT als Bearer-Token in den Header ein. 4. Der **Client** sendet eine Anfrage an einen anderen Microservice (z.B. `members-service`) und fügt das JWT als Bearer-Token in den Header ein.
5. Der **`members-service`**, der ebenfalls den `auth-client` als Abhängigkeit hat, nutzt den `JwtService`, um das Token zu validieren und die Berechtigungen typsicher auszulesen. 5. Der **`members-service`**, der ebenfalls den `auth-client` als Abhängigkeit hat, nutzt den `JwtService`, um das Token zu validieren und die Berechtigungen typsicher auszulesen.
6. Das **Gateway** kann vorgelagert JWT-basierte Authentifizierung durchführen. Aktuell existiert ein `JwtAuthenticationFilter`, der über `gateway.security.jwt.enabled=true` aktiviert wird. In der vorliegenden Codebasis nutzt dieser noch eine vereinfachte Validierung; die geplante Integration ist die Nutzung des `auth-client` zur vollständigen Validierung und Claim-Extraktion.
Diese Architektur entkoppelt die Fach-Services von der Komplexität der Identitätsverwaltung und schafft eine robuste, zentrale Sicherheitsinfrastruktur. Diese Architektur entkoppelt die Fach-Services von der Komplexität der Identitätsverwaltung und schafft eine robuste, zentrale Sicherheitsinfrastruktur.
## Modernisierungen (August 2025) ## Modernisierungen (September 2025)
### Technische Verbesserungen ### Technische Verbesserungen
@@ -68,6 +125,21 @@ Diese Architektur entkoppelt die Fach-Services von der Komplexität der Identit
- Entfernung von `Thread.sleep()` für zuverlässigere Tests - Entfernung von `Thread.sleep()` für zuverlässigere Tests
- Bessere Expired-Token-Tests mit eindeutigen Zeitstempel-Differenzen - Bessere Expired-Token-Tests mit eindeutigen Zeitstempel-Differenzen
### Token Claims und Struktur
Empfohlene Claims im JWT (Beispiel):
- sub: Benutzer-ID (UUID)
- pid: Personen-ID (UUID)
- preferred_username: Loginname (derzeit intern als Claim "username" umgesetzt)
- email: E-Mail-Adresse
- roles: Liste von Rollen (`RolleE`)
- perms: Liste von Berechtigungen (`BerechtigungE`)
- iss: Issuer (z.B. meldestelle-auth-server)
- aud: Audience (z.B. meldestelle-services)
- iat/exp: Ausstellungs- und Ablaufzeitpunkt
Diese Claims werden vom `auth-client` gelesen und in typsichere Modelle abgebildet.
### API-Änderungen ### API-Änderungen
**Neue Result-basierte APIs:** **Neue Result-basierte APIs:**
@@ -255,6 +327,14 @@ Das Auth-Modul wurde von **kritisch untergetestet** auf **umfassend getestet** t
└── kotlinx-serialization-json (JSON Serialization) └── kotlinx-serialization-json (JSON Serialization)
``` ```
## Aktualitäts-Check (Repo-Stand September 2025)
- `auth-client` enthält `AuthenticationService` mit suspend-Funktionen und versiegelten Result-Typen (Success/Failure/Locked; PasswordChangeResult inkl. WeakPassword). Diese Schnittstelle ist in dieser README beschrieben und aktuell.
- `auth-server` stellt `JwtService` via `AuthServerConfiguration` bereit und liest Properties unter `auth.jwt.*`. Beispielkonfiguration ist oben dokumentiert und entspricht dem Code.
- Das API-Gateway besitzt einen `JwtAuthenticationFilter`, der derzeit eine vereinfachte Tokenprüfung implementiert. Geplante nächste Stufe: Verwendung des `auth-client` zur echten JWT-Validierung und Claim-Extraktion. Property-Schalter: `gateway.security.jwt.enabled`.
Diese README wurde am 03.09.2025 aktualisiert und spiegelt den aktuellen Stand der Implementierung wider.
## Production-Readiness Status ## Production-Readiness Status
### ✅ Production-Ready Bereiche ### ✅ Production-Ready Bereiche
@@ -305,7 +385,7 @@ Das infrastructure/auth Modul ist **production-ready** und umfassend modernisier
Die Transformation von "kritisch untergetestet" zu "production-ready" ist vollständig abgeschlossen und erfüllt alle Anforderungen für ein sicherheitskritisches Authentifizierungs-System in einer Microservice-Landschaft. Die Transformation von "kritisch untergetestet" zu "production-ready" ist vollständig abgeschlossen und erfüllt alle Anforderungen für ein sicherheitskritisches Authentifizierungs-System in einer Microservice-Landschaft.
--- ---
**Letzte Aktualisierung**: 15. August 2025 **Letzte Aktualisierung**: 3. September 2025
**Status**: Production-Ready mit umfassender Test-Abdeckung **Status**: Production-Ready mit umfassender Test-Abdeckung
**Dokumentation**: Vollständig konsolidiert aus allen Teilbereichen **Dokumentation**: Vollständig konsolidiert aus allen Teilbereichen
**Validierung**: Sicherheitstests erfolgreich bestanden (15.08.2025) **Validierung**: Sicherheitstests erfolgreich bestanden (15.08.2025)
@@ -9,6 +9,21 @@ plugins {
alias(libs.plugins.spring.dependencyManagement) alias(libs.plugins.spring.dependencyManagement)
} }
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}
tasks.test {
useJUnitPlatform()
}
java {
withJavadocJar()
withSourcesJar()
}
dependencies { dependencies {
// Stellt sicher, dass alle Versionen aus der zentralen BOM kommen. // Stellt sicher, dass alle Versionen aus der zentralen BOM kommen.
@@ -5,37 +5,37 @@ import com.benasher44.uuid.Uuid
import java.time.LocalDateTime import java.time.LocalDateTime
/** /**
* Service for user authentication and password management. * Service für Benutzerauthentifizierung und Passwortverwaltung.
*/ */
interface AuthenticationService { interface AuthenticationService {
/** /**
* Authenticates a user with the given username and password. * Authentifiziert einen Benutzer mit Benutzernamen und Passwort.
* *
* @param username The username * @param username Der Benutzername
* @param password The password * @param password Das Passwort
* @return The authentication result * @return Das Authentifizierungsergebnis
*/ */
suspend fun authenticate(username: String, password: String): AuthResult suspend fun authenticate(username: String, password: String): AuthResult
/** /**
* Changes a user's password. * Ändert das Passwort eines Benutzers.
* *
* @param userId The user ID * @param userId Die Benutzer-ID
* @param currentPassword The current password * @param currentPassword Das aktuelle Passwort
* @param newPassword The new password * @param newPassword Das neue Passwort
* @return The password change result * @return Das Ergebnis der Passwortänderung
*/ */
suspend fun changePassword(userId: Uuid, currentPassword: String, newPassword: String): PasswordChangeResult suspend fun changePassword(userId: Uuid, currentPassword: String, newPassword: String): PasswordChangeResult
/** /**
* Possible results of an authentication attempt. * Mögliche Ergebnisse eines Authentifizierungsversuchs.
*/ */
sealed class AuthResult { sealed class AuthResult {
/** /**
* Authentication was successful. * Authentifizierung war erfolgreich.
* *
* @param token The JWT token * @param token Das JWT-Token
* @param user The authenticated user * @param user Der authentifizierte Benutzer
*/ */
data class Success(val token: String, val user: AuthenticatedUser) : AuthResult() data class Success(val token: String, val user: AuthenticatedUser) : AuthResult()
@@ -23,6 +23,12 @@ class JwtService(
) { ) {
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
init {
require(secret.length >= 32) { "JWT secret must be at least 32 characters for HMAC512" }
require(issuer.isNotBlank()) { "JWT issuer must not be blank" }
require(audience.isNotBlank()) { "JWT audience must not be blank" }
}
private val algorithm = Algorithm.HMAC512(secret) private val algorithm = Algorithm.HMAC512(secret)
private val verifier = JWT.require(algorithm) private val verifier = JWT.require(algorithm)
.withIssuer(issuer) .withIssuer(issuer)
@@ -47,7 +47,7 @@ class JwtServiceTest {
@Test @Test
fun `validateToken should return false for token with wrong secret`() { fun `validateToken should return false for token with wrong secret`() {
// Arrange // Arrange
val otherService = JwtService("a-different-wrong-secret", testIssuer, testAudience) val otherService = JwtService("a-different-wrong-secret-that-is-long-enough-1234567890", testIssuer, testAudience)
val token = otherService.generateToken("user-123", "test", emptyList()) val token = otherService.generateToken("user-123", "test", emptyList())
// Act & Assert // Act & Assert
@@ -9,6 +9,12 @@ plugins {
alias(libs.plugins.spring.dependencyManagement) alias(libs.plugins.spring.dependencyManagement)
} }
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}
// Konfiguriert die Hauptklasse für das ausführbare JAR. // Konfiguriert die Hauptklasse für das ausführbare JAR.
springBoot { springBoot {
mainClass.set("at.mocode.infrastructure.auth.AuthServerApplicationKt") mainClass.set("at.mocode.infrastructure.auth.AuthServerApplicationKt")
@@ -5,21 +5,29 @@ import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import org.springframework.validation.annotation.Validated
import jakarta.validation.constraints.Min
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Size
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
/** /**
* Spring configuration for the Auth Server module. * Spring-Konfiguration für das Auth-Server-Modul.
* Provides the necessary beans and configuration for JWT handling and authentication. * Stellt die notwendigen Beans und Einstellungen für JWT-Verarbeitung und Authentifizierung bereit.
*/ */
@Configuration @Configuration
@EnableConfigurationProperties(AuthServerConfiguration.JwtProperties::class) @EnableConfigurationProperties(AuthServerConfiguration.JwtProperties::class)
class AuthServerConfiguration { class AuthServerConfiguration {
/** /**
* Creates a JwtService bean with configuration from application properties. * Erstellt einen JwtService-Bean mit Konfiguration aus den Application Properties.
*/ */
@Bean @Bean
fun jwtService(jwtProperties: JwtProperties): JwtService { fun jwtService(jwtProperties: JwtProperties): JwtService {
// Basic safeguard: warn if default secret is used
if (jwtProperties.secret == "default-secret-for-development-only-please-change-in-production") {
System.err.println("[SECURITY WARNING] Using default JWT secret DO NOT use this in production!")
}
return JwtService( return JwtService(
secret = jwtProperties.secret, secret = jwtProperties.secret,
issuer = jwtProperties.issuer, issuer = jwtProperties.issuer,
@@ -29,13 +37,19 @@ class AuthServerConfiguration {
} }
/** /**
* Configuration properties for JWT settings. * Konfigurationseigenschaften für JWT-Einstellungen.
*/ */
@ConfigurationProperties(prefix = "auth.jwt") @ConfigurationProperties(prefix = "auth.jwt")
@Validated
data class JwtProperties( data class JwtProperties(
@field:NotBlank
@field:Size(min = 32, message = "JWT secret must be at least 32 characters for HMAC512")
val secret: String = "default-secret-for-development-only-please-change-in-production", val secret: String = "default-secret-for-development-only-please-change-in-production",
@field:NotBlank
val issuer: String = "meldestelle-auth-server", val issuer: String = "meldestelle-auth-server",
@field:NotBlank
val audience: String = "meldestelle-services", val audience: String = "meldestelle-services",
@field:Min(1)
val expiration: Long = 60 // minutes val expiration: Long = 60 // minutes
) )
} }
-2
View File
@@ -1,2 +0,0 @@
// Infrastructure Auth Module Container
// This is a container module for authentication-related subprojects
+81 -462
View File
@@ -1,495 +1,114 @@
# Infrastructure/Cache Module - Comprehensive Documentation # Infrastructure/Cache Modulbeschreibung und Implementierungsleitfaden
*Letzte Aktualisierung: 15. August 2025* Letzte Aktualisierung: 03. September 2025
## Überblick ## Zweck und Aufgaben des Moduls
Das Infrastructure/Cache-Modul stellt eine einheitliche, technologieneutrale CacheSchnittstelle für alle Services bereit und liefert mit einer Redisbasierten AdapterImplementierung die produktionsreife Ausführung. Ziele:
- Antwortzeiten reduzieren und Primärdatenbanken entlasten.
- Einheitliche API für Lesen/Schreiben, BatchOperationen und TTLs.
- Resilienz bei RedisAusfällen durch lokalen Fallback.
- Operative Transparenz durch einfache Metriken, HealthInformationen und periodische Wartungsaufgaben.
Das **Cache-Modul** stellt eine zentrale, hochverfügbare und produktionsbereite Caching-Infrastruktur für alle Microservices bereit. Es dient der Verbesserung der Anwendungsperformance, der Reduzierung von Latenzen und der Entlastung der primären PostgreSQL-Datenbank. ## Architektur (PortAdapter)
- cacheapi: enthält die öffentlichen Verträge und Basistypen
- DistributedCache: zentrale PortSchnittstelle für CacheOperationen
- CacheEntry, CacheConfiguration, CacheSerializer
- ConnectionStatusTracker/ConnectionStateListener zur Verbindungsüberwachung
- rediscache: Adapter, der die PortSchnittstelle mit Spring Data Redis umsetzt
- RedisDistributedCache: konkrete Implementierung inkl. OfflineFallback, DirtySync, Batchs, KeyPrefixing, TTLHandling und einfachen Metriken
- JacksonCacheSerializer: serialisiert Werte und CacheEntry per Jackson
**Status: ✅ PRODUKTIONSBEREIT** - Vollständig getestet mit 39 Tests (94.7% Success Rate) ## Öffentliche API (Auszug)
DistributedCache
- get(key, clazz)/set(key, value, ttl?)
- delete(key), exists(key)
- multiGet(keys, clazz), multiSet(map, ttl?)
- multiDelete(keys)
- synchronize(keys?), markDirty(key), getDirtyKeys(), clear()
## Architektur: Port-Adapter-Muster Idiomatic Kotlin Extensions
- cache.get<T>(key)
- cache.multiGet<T>(keys)
Das Modul folgt streng dem **Port-Adapter-Muster** (Hexagonale Architektur), um eine saubere Trennung zwischen der Caching-Schnittstelle (dem "Port") und der konkreten Implementierung (dem "Adapter") zu gewährleisten. CacheConfiguration (DefaultCacheConfiguration vorhanden)
- defaultTtl?, localCacheMaxSize?, offlineModeEnabled, synchronizationInterval, offlineEntryMaxAge?, keyPrefix, compressionEnabled, compressionThreshold
### Module-Struktur Hinweis: Die Kompression wird aktuell durch den Serializer bereitgestellt; Schwellwerte/Flags sind für zukünftiges Tuning vorgesehen.
* **`:infrastructure:cache:cache-api`**: Definiert den abstrakten "Vertrag" für das Caching (`DistributedCache`-Interface), ohne sich um die zugrunde liegende Technologie zu kümmern. Die Fach-Services programmieren ausschließlich gegen dieses Interface. ## Implementierungsdetails (RedisDistributedCache)
* **`:infrastructure:cache:redis-cache`**: Die konkrete Implementierung des Vertrags, die **Redis** als hochperformantes Caching-Backend verwendet. Kapselt die gesamte Redis-spezifische Logik. - Lokaler Fallback: ConcurrentHashMap als lokaler Cache speichert CacheEntry inkl. expiresAt. Bei RedisAusfall werden Schreibvorgänge lokal gehalten und als „dirty“ markiert.
- DirtySynchronisation: Sobald die Verbindung wieder ONLINE ist, werden geänderte Schlüssel zu Redis synchronisiert (synchronize()).
- KeyPrefixing: Alle externen Keys werden mittels keyPrefix gekapselt, um Mandanten/Services zu isolieren.
- TTL/Expiration: TTL wird einheitlich über kotlin.time.Duration angegeben und für Redis in java.time.Duration konvertiert. Lokale Einträge enthalten expiresAt und werden periodisch bereinigt.
- BatchOperationen: multiGet/multiSet/multiDelete nutzen RedisBatching/Pipelining, lokal wird konsistent gespiegelt.
- Größenbegrenzung Local Cache (neu): Wenn localCacheMaxSize gesetzt ist, werden bei Überschreitung die am längsten nicht mehr modifizierten Einträge aus dem lokalen Cache entfernt (LRM least recently modified). Dadurch bleibt der lokale Fallback speichereffizient.
- Periodische Aufgaben (@Scheduled):
- Verbindungsprüfung: fixedDelayString = "${redis.connection-check-interval:10000}"
- Lokale Bereinigung: fixedDelayString = "${redis.local-cache-cleanup-interval:60000}"
- DirtySync: fixedDelayString = "${redis.sync-interval:300000}"
- MetrikenLog: fixedDelayString = "${redis.metrics-log-interval:300000}"
## Schlüsselfunktionen Wichtige Robustheitsdetails
- Alle RedisOperationen fangen RedisConnectionFailureException ab und schalten den ConnectionState auf DISCONNECTED. Beim nächsten erfolgreichen Zugriff wird CONNECTED gesetzt und eine Synchronisation der dirty keys ausgelöst.
- multiSet setzt TTLs bei Bedarf per Pipeline nach (pExpire); einzelne setOperationen nutzen expire via Duration.
### Core Features ## Verwendung (Beispiele)
* **Offline-Fähigkeit & Resilienz:** Das Modul verfügt über einen In-Memory-Cache, der bei einem Ausfall der Redis-Verbindung als Fallback dient. Schreib-Operationen werden lokal als "dirty" markiert und automatisch mit Redis synchronisiert, sobald die Verbindung wiederhergestellt ist. Einbinden: Projekte hängen gegen :infrastructure:cache:redis-cache und injizieren DistributedCache.
* **Idiomatische Kotlin-API:** Bietet neben der Standard-API auch ergonomische Erweiterungsfunktionen mit `reified`-Typen für eine saubere und typsichere Verwendung in Kotlin-Code (`cache.get<User>("key")`).
* **Projekweite Konsistenz:** Verwendet `kotlin.time.Duration` und `kotlin.time.Instant` für eine einheitliche Handhabung von Zeit- und Dauer-Angaben im gesamten Projekt.
* **Automatisierte Verbindungsüberwachung:** Überprüft periodisch den Zustand der Redis-Verbindung und informiert Listener über Statusänderungen (`CONNECTED`, `DISCONNECTED`).
### Enterprise Features
* **Multi-Tenant-Fähigkeit:** Key-Prefixes ermöglichen vollständige Isolation zwischen verschiedenen Anwendungen
* **Konfigurierbare Kompression:** Automatische Kompression für große Datenstrukturen (konfigurierbar ab 1KB)
* **Performance-Optimierung:** 5.000+ gleichzeitige Operationen mit >95% Erfolgsrate
* **Unicode-Vollunterstützung:** Internationale Deployment-fähig mit Emojis, Umlauten, Chinesisch, Arabisch
* **10MB+ Objektgrößen:** Automatische Kompression und Übertragung sehr großer Objekte
### Enhanced Monitoring & Operations (NEW)
* **Real-time Performance Metrics:** Automatisches Tracking aller Cache-Operationen mit Erfolgsraten
* **Strukturierte Metrics-Logging:** Periodische Performance-Reports mit detaillierten Metriken
* **Cache Warming Utilities:** Produktions-bereite Warming-Strategien für optimale Performance
* **Health Status Monitoring:** Umfassende Gesundheitschecks mit automatischer Status-Bewertung
* **Advanced Connection Tracking:** Erweiterte Verbindungsüberwachung mit detaillierten Zustandsinformationen
## Verwendung
Ein Microservice bindet `:infrastructure:cache:redis-cache` als Abhängigkeit ein und lässt sich das `DistributedCache`-Interface per Dependency Injection geben.
### Grundlegende Verwendung
Lesen/Schreiben mit TTL
```kotlin ```kotlin
@Service val user = cache.get<User>("user:42")
class MasterdataService( if (user == null) {
private val cache: DistributedCache // Nur das Interface wird verwendet! val loaded = userRepository.findById("42") ?: return null
) { cache.set("user:42", loaded, ttl = 1.hours)
fun findCountryById(id: String): Country? {
val cacheKey = "country:$id"
// 1. Versuche, aus dem Cache zu lesen (typsicher und sauber)
val cachedCountry = cache.get<Country>(cacheKey)
if (cachedCountry != null) {
return cachedCountry
}
// 2. Wenn nicht im Cache, aus der DB lesen
val dbCountry = countryRepository.findById(id)
// 3. Ergebnis in den Cache schreiben für zukünftige Anfragen
dbCountry?.let {
cache.set(cacheKey, it, ttl = 1.hours) // Cache für 1 Stunde
}
return dbCountry
}
} }
``` ```
### Erweiterte Verwendung BatchLesezugriff
```kotlin ```kotlin
// Batch-Operationen für bessere Performance val ids = listOf("user:1", "user:2", "user:3")
val userIds = listOf("user:1", "user:2", "user:3") val map = cache.multiGet<User>(ids)
val cachedUsers = cache.multiGet<User>(userIds)
// Bulk-Updates
val newUsers = mapOf(
"user:4" to User("Alice"),
"user:5" to User("Bob")
)
cache.multiSet(newUsers, ttl = 30.minutes)
// Connection-State-Monitoring
cache.registerConnectionListener(object : ConnectionStateListener {
override fun onConnectionStateChanged(newState: ConnectionState, timestamp: Instant) {
logger.info { "Cache connection state changed to: $newState" }
}
})
// Performance Monitoring (NEW)
val metrics = cache.getPerformanceMetrics()
logger.info { "Current performance: ${metrics["successRate"]} success rate, ${metrics["totalOperations"]} operations" }
// Health Status Checking (NEW)
val health = cache.getHealthStatus()
if (health["healthy"] as Boolean) {
logger.info { "Cache is healthy with ${health["successRate"]} success rate" }
} else {
logger.warn { "Cache health issue detected: ${health["connectionState"]}" }
}
// Cache Warming (NEW)
// Individual key warming with data loader
cache.warmCache(listOf("user:1", "user:2", "user:3")) { key ->
userService.loadUser(key.substringAfter(":"))
}
// Bulk cache warming
val preloadData = mapOf(
"config:app" to applicationConfig,
"config:features" to featureFlags
)
cache.warmCacheBulk(preloadData, ttl = 1.hours)
``` ```
## Test-Suite: Vollständige Produktionsabdeckung BulkSchreiben
### Test-Übersicht
-**39 Tests total** (12 Basis + 27 erweiterte Tests)
-**6 Test-Klassen** vollständig optimiert
-**94.7% Success Rate** (36/38 erfolgreich)
-**Professionelles SLF4J/kotlin-logging** durchgängig
### Test-Kategorien
| Kategorie | Tests | Zweck | Status |
|-----------|-------|-------|---------|
| **Basis-Funktionalität** | 12 | Core Cache Operations | ✅ Stabil |
| **Performance & Load** | 3 | Gleichzeitige Zugriffe, Speicherdruck, Bulk-Ops | ✅ Optimiert |
| **Edge Cases** | 6 | Serialisierung, große Daten, Unicode, null-Werte | ✅ Robust |
| **Resilience** | 6 | Timeouts, Verbindungsausfälle, Wiederverbindung | ✅ Resilient |
| **Configuration** | 6 | TTL, Kompression, Prefixes, Cache-Größen | ✅ Flexibel |
| **Integration** | 6 | Cross-Instance, Monitoring, Produktions-Szenarien | ✅ Produktionsready |
### Detaillierte Test-Abdeckung
#### Performance & Load Tests
- **`test cache performance with high concurrent access`**: 100 Coroutines mit je 50 Operationen (5.000 gleichzeitige Ops)
- **`test cache behavior under memory pressure`**: 500 Einträge mit kleinem Local-Cache (100)
- **`test bulk operations performance`**: 1000 Einträge mit multiSet/multiGet (1000+ Einträge/Sekunde)
#### Edge Cases & Error Handling
- **`test serialization with problematic objects`**: Zirkuläre Referenzen, tiefe Verschachtelung (50 Ebenen)
- **`test cache with extremely large values`**: 10MB Strings mit automatischer Kompression
- **`test special characters and unicode`**: Emojis, Umlaute, Chinesisch, Arabisch, gemischte Inhalte
- **`test cache with null and empty values`**: Leere Strings, null-Felder, leere Collections
- **`test complex nested objects`**: Verschachtelte Maps mit Listen und Metadaten
- **`test malformed data scenarios`**: Nicht-existierende Keys, gemischte Batch-Operationen
#### Resilience & Timeout Tests
- **`test connection timeout scenarios`**: 5-Sekunden-Delays simuliert, max. 10s Timeout
- **`test partial Redis failures`**: Intermittierende Ausfälle alle 3 Operationen
- **`test network partitioning simulation`**: Komplette Netzwerktrennung mit Offline-Mode
- **`test reconnection and synchronization`**: Automatische Wiederverbindung mit Dirty-Key-Sync
- **`test connection state listener notifications`**: Listener-Management und State-Tracking
- **`test Redis restart simulation`**: Neustart-Szenarien mit lokaler Pufferung
#### Configuration Tests
- **`test different cache configurations`**: Performance-, Storage- und Minimal-Configs
- **`test compression threshold behavior`**: 50-Byte-Schwelle konfigurierbar getestet
- **`test key prefix functionality`**: Vollständige Isolation zwischen "app1", "app2", ""
- **`test TTL configuration variations`**: null, 100ms, 30min TTLs flexibel konfigurierbar
- **`test offline mode configuration`**: Ein/Ausschalten des Offline-Modus
- **`test local cache size limits`**: 3 vs. unlimited vs. 1000 Einträge mit Redis-Fallback
#### Integration & Monitoring Tests
- **`test connection state listener functionality`**: Professionelles Listener-Management
- **`test different Redis configurations`**: Multi-Config-Isolation und Cross-Compatibility
- **`test cache warming scenarios`**: Bulk- (1000 Einträge <100ms), graduelle und selektive Vorwärmung
- **`test metrics and monitoring integration`**: State-Tracking, Dirty-Keys-Monitoring, Performance-Metriken
- **`test cross-instance synchronization`**: Multi-Instance-Datenaustausch mit kleinen Delays
- **`test production-like scenarios`**: User-Sessions (1000), Config-Caching, API-Responses (100)
### Produktionstauglichkeits-Validierung
#### ✅ **Performance-Benchmarks bestanden:**
- **5.000+ gleichzeitige Operationen** mit >95% Erfolgsrate
- **Sub-100ms Performance** für Standard-Operationen
- **1000+ Einträge/Sekunde** bei Bulk-Operationen
- **Cache-Warming: 1000 Einträge in <100ms** möglich
#### ✅ **Robustheit validiert:**
- **Graceful Degradation** bei allen Fehlersituationen
- **Automatische Wiederverbindung** mit Dirty-Key-Synchronisation
- **Speicher-effiziente** Local-Cache-Verwaltung mit Redis-Fallback
- **Cross-Instance-Synchronisation** zwischen Services funktionsfähig
#### ✅ **Enterprise-Features getestet:**
- **10MB+ Objektgrößen** mit automatischer Kompression
- **Unicode-Vollunterstützung** für internationale Deployments
- **Multi-Tenant-Fähigkeit** durch Key-Prefixes mit perfekter Isolation
- **Vollständige Offline-Fähigkeit** bei Redis-Ausfällen
## Logging-Architektur: Professionelle Standards
### Implementierte Standards
Das gesamte Modul verwendet professionelle SLF4J/kotlin-logging Standards:
```kotlin ```kotlin
// Konsistentes Pattern in allen Klassen: cache.multiSet(mapOf(
companion.object { "cfg:app" to appConfig,
private val logger = KotlinLogging.logger {} "cfg:features" to features
} ), ttl = 30.minutes)
// Strukturierte Logging-Calls:
logger.info { "Cache operation completed with metrics: $metrics" }
logger.warn { "Connection state changed: $oldState -> $newState" }
logger.debug { "Processing batch of $size entries with config: $config" }
``` ```
### Log-Level-Richtlinien Verbindungsstatus überwachen
| Level | Verwendung | Beispiel |
|-------|------------|----------|
| **INFO** | Cache-Operationen, State-Changes, Metriken | `logger.info { "Performance test completed: $metrics" }` |
| **DEBUG** | Detaillierte Ablaufinformationen | `logger.debug { "Processing batch of $size entries" }` |
| **WARN** | Verbindungsprobleme, Performance-Issues | `logger.warn { "Success rate below threshold: $rate" }` |
| **ERROR** | Kritische Fehler, Serialisierungsprobleme | `logger.error { "Unexpected exception in cache operation" }` |
### Logback-Konfiguration
```xml
<!-- Strukturierte Console-Ausgaben -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- Cache-spezifische Logger -->
<logger name="at.mocode.infrastructure.cache" level="DEBUG" />
<logger name="RedisDistributedCachePerformanceTest" level="INFO" />
<!-- Reduzierte Verbosity für externe Komponenten -->
<logger name="org.testcontainers" level="WARN" />
<logger name="io.lettuce" level="WARN" />
```
## Dependency-Management: Single Source of Truth
### Vollständige SINGLE SOURCE OF TRUTH Konformität
Alle Dependencies verwenden jetzt zentrale `libs.versions.toml` Verwaltung:
```toml
# Zentrale Versionen
[versions]
logback = "1.5.13"
kotlinLogging = "3.0.5"
[libraries]
kotlin-logging-jvm = { module = "io.github.microutils:kotlin-logging-jvm", version.ref = "kotlinLogging" }
logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
logback-core = { module = "ch.qos.logback:logback-core", version.ref = "logback" }
[bundles]
redis-cache = ["spring-boot-starter-data-redis", "lettuce-core", "jackson-module-kotlin", "jackson-datatype-jsr310"]
testing-jvm = ["junit-jupiter-api", "junit-jupiter-engine", "mockk", "assertj-core", "kotlinx-coroutines-test"]
```
### Build-Konfiguration
```kotlin
// redis-cache/build.gradle.kts - VOLLSTÄNDIG OPTIMIERT
dependencies {
// Alle Dependencies über libs-Referenzen
implementation(libs.bundles.redis.cache)
testImplementation(projects.platform.platformTesting)
testImplementation(libs.bundles.testing.jvm)
testImplementation(libs.kotlin.test)
testImplementation(libs.kotlin.logging.jvm)
testImplementation(libs.logback.classic)
testImplementation(libs.logback.core)
}
```
## Konfiguration & Deployment
### Cache-Konfigurationen für verschiedene Umgebungen
#### Performance-optimiert (High-Throughput)
```kotlin
val performanceConfig = DefaultCacheConfiguration(
keyPrefix = "perf",
defaultTtl = 5.minutes,
localCacheMaxSize = 50000,
compressionEnabled = false, // Für maximale Geschwindigkeit
compressionThreshold = Int.MAX_VALUE
)
```
#### Storage-optimiert (Kompression)
```kotlin
val storageConfig = DefaultCacheConfiguration(
keyPrefix = "storage",
defaultTtl = 7.days,
localCacheMaxSize = 1000,
compressionEnabled = true,
compressionThreshold = 100 // Kompression ab 100 Bytes
)
```
#### Minimal (Entwicklung)
```kotlin
val minimalConfig = DefaultCacheConfiguration(
keyPrefix = "dev",
defaultTtl = null, // Kein TTL
localCacheMaxSize = null, // Unbegrenzt
offlineModeEnabled = false // Für Entwicklung optional
)
```
## Monitoring & Observability
### Connection-State-Monitoring
```kotlin ```kotlin
cache.registerConnectionListener(object : ConnectionStateListener { cache.registerConnectionListener(object : ConnectionStateListener {
override fun onConnectionStateChanged(newState: ConnectionState, timestamp: Instant) { override fun onConnectionStateChanged(newState: ConnectionState, timestamp: Instant) {
when (newState) { logger.info("Cache connection state: $newState at $timestamp")
ConnectionState.CONNECTED -> {
logger.info { "Cache reconnected at $timestamp" }
metricsCollector.increment("cache.reconnects")
}
ConnectionState.DISCONNECTED -> {
logger.warn { "Cache disconnected at $timestamp" }
alerting.sendAlert("Cache offline", "Redis connection lost")
}
ConnectionState.RECONNECTING -> {
logger.info { "Cache attempting reconnection at $timestamp" }
}
}
} }
}) })
``` ```
### Performance-Metriken ## Konfiguration
```kotlin DefaultCacheConfiguration bietet sinnvolle Defaults. Relevante Properties (optional via Spring @Scheduled Platzhalter):
// Beispiel für strukturierte Metriken-Sammlung - redis.connection-check-interval: ms für Verbindungsprüfung (Default 10000)
val metrics = mapOf( - redis.local-cache-cleanup-interval: ms für lokale Bereinigung (Default 60000)
"totalOperations" to totalOperations, - redis.sync-interval: ms für Synchronisationsläufe (Default 300000)
"successRate" to successRate, - redis.metrics-log-interval: ms für periodisches MetrikenLogging (Default 300000)
"averageLatency" to averageLatency,
"operationsPerSecond" to opsPerSec,
"dirtyKeysCount" to cache.getDirtyKeys().size,
"connectionState" to cache.getConnectionState()
)
logger.info { "Cache performance metrics: $metrics" }
```
### CI/CD Integration Hinweise
```yaml - keyPrefix sollte pro Service gesetzt werden (z. B. "masterdata"), um Kollisionen zu vermeiden.
# Beispiel für GitHub Actions - localCacheMaxSize begrenzt die Größe des lokalen FallbackCaches. Bei null ist die Größe unbegrenzt.
- name: Run Cache Tests with Structured Logging
run: |
./gradlew :infrastructure:cache:redis-cache:test --info
# Log-Level für verschiedene Umgebungen: ## Betrieb & Monitoring
# Development: DEBUG (alle Details) - HealthInfos: getHealthStatus() liefert eine einfache Einschätzung basierend auf ConnectionState und Erfolgsrate der Operationen.
# CI/CD: INFO (wichtige Ereignisse) - Metriken: getPerformanceMetrics() liefert einfache Kennzahlen (Operations, SuccessRate, Größe lokaler Cache, Anzahl dirty Keys). Periodisches Logging per @Scheduled möglich.
# Production: WARN (nur Probleme) - Cache Warming: warmCache(keys, loader) und warmCacheBulk(map) helfen, HotKeys/gefragte Konfigurationen beim Start vorzuwärmen.
```
## Best Practices & Empfehlungen ## Grenzen & bekannte Punkte
- Kompression ist im Serializer implementiert; die konfigurierbaren Flags/Schwellenwerte sind derzeit nicht dynamisch an/ausgeschaltet.
- OfflineModus: Die Konfiguration offlineModeEnabled ist vorhanden; die Implementierung betreibt den lokalen Fallback standardmäßig bei Verbindungsproblemen. Eine harte Deaktivierung dieses Verhaltens ist aktuell nicht verdrahtet.
### Produktionseinsatz-Empfehlungen ## Changelog (Kurz)
- 20250903: Fehlerbehebungen für @ScheduledPlatzhalter, korrektes Logging im CacheWarming, lokale CacheGrößenbegrenzung (LRMEviction) hinzugefügt. Dokumentation aktualisiert (diese Datei).
#### **Priorität HOCH (sofort umsetzbar):** ## Fazit
1. **Performance-Monitoring:** Strukturierte Logs für Produktions-Metriken nutzen Das CacheModul bietet eine klare, wiederverwendbare CacheSchnittstelle mit einer robusten RedisImplementierung. Es unterstützt TTLs, BatchOperationen, lokalen Fallback bei Ausfällen und liefert einfache, praxistaugliche Betriebsinformationen. Mit keyPrefix und lokalen Limits ist der Einsatz in MultiServiceUmgebungen unkompliziert und stabil.
2. **Connection-State-Überwachung:** Listener für Alerting bei Redis-Ausfällen einrichten
3. **Cache-Warming:** Graduelle Warming-Strategien beim Service-Start implementieren
#### **Priorität MITTEL (mittelfristig):**
1. **Kompression-Tuning:** Threshold je nach Datenanforderungen anpassen (Standard: 1KB)
2. **Local-Cache-Größen:** Je nach verfügbarem RAM pro Service optimieren
3. **TTL-Strategien:** Spezifische TTLs für verschiedene Datentypen definieren
#### **Priorität NIEDRIG (langfristig):**
1. **Advanced Monitoring:** Integration mit Micrometer/Prometheus für detaillierte Metriken
2. **Multi-Redis-Cluster:** Unterstützung für Redis-Cluster-Konfigurationen
3. **Erweiterte Kompression:** Alternative Algorithmen (LZ4, Snappy) evaluieren
### Entwickler-Guidelines
#### **DO's ✅**
- Verwende `cache.get<Type>(key)` für typsichere Operationen
- Implementiere Connection-State-Listener für kritische Services
- Nutze Batch-Operationen (`multiGet`, `multiSet`) für bessere Performance
- Verwende aussagekräftige Key-Prefixes für Multi-Tenant-Szenarien
- Teste Cache-Warming-Strategien in Integration-Tests
#### **DON'Ts ❌**
- Niemals sensible Daten ohne Verschlüsselung cachen
- Vermeide sehr große TTLs ohne Begründung (>24h)
- Keine Hard-coded Cache-Keys - verwende Key-Factories
- Vermeide Blocking-Operations in Connection-State-Listeners
- Keine println() in Cache-bezogenem Code - verwende Logger
### Typische Anwendungsszenarien
#### User-Session-Caching
```kotlin
// TTL = Session-Timeout
cache.set("user:session:${sessionId}", userSession, ttl = 30.minutes)
```
#### API-Response-Caching
```kotlin
// Kurze TTL für häufig ändernde Daten
cache.set("api:response:${endpoint}", response, ttl = 5.minutes)
```
#### Configuration-Caching
```kotlin
// Lange TTL für stabile Konfiguration
cache.set("config:${service}", config, ttl = 1.hours)
```
#### Database-Result-Caching
```kotlin
// Mittlere TTL für Datenbankabfragen
cache.set("db:${query.hash()}", results, ttl = 15.minutes)
```
## Migration & Upgrade-Pfad
### Von Version < 1.0
1. **Dependencies aktualisieren:** Umstellung auf libs.versions.toml
2. **Logging modernisieren:** println() → SLF4J/kotlin-logging
3. **Test-Suite erweitern:** Neue Test-Kategorien hinzufügen
4. **Konfiguration migrieren:** Neue DefaultCacheConfiguration verwenden
### Backwards Compatibility
- ✅ Alle bestehenden API-Calls funktionieren weiterhin
- ✅ Bestehende Konfigurationen sind kompatibel
- ✅ Migration kann schrittweise erfolgen
## Changelog
### 2025-08-15 - Enhanced Monitoring & Operations Update v2.1
-**Real-time Performance Metrics:** Automatisches Tracking aller Cache-Operationen mit detaillierter Erfolgsraten-Überwachung
-**Strukturierte Metrics-Logging:** Periodische Performance-Reports alle 5 Minuten mit umfassenden Metriken
-**Cache Warming Utilities:** Produktions-bereite Warming-Strategien mit Individual- und Bulk-Operations
-**Health Status Monitoring:** Umfassende Gesundheitschecks mit automatischer Status-Bewertung (>90% Erfolgsrate)
-**Enhanced Connection Tracking:** Erweiterte Verbindungsüberwachung mit detaillierten Zustandsinformationen
-**Production-Ready Monitoring:** Integration hooks für Enterprise-Monitoring-Systeme
-**Performance Optimization:** Verbesserte Metriken-Sammlung ohne Performance-Impact
### 2025-08-14 - Major Update v2.0
-**Vollständige Test-Suite-Erweiterung:** Von 12 auf 39 Tests (94.7% Success Rate)
-**Professionelle Logging-Architektur:** Komplette Umstellung auf SLF4J/kotlin-logging
-**SINGLE SOURCE OF TRUTH:** Alle Dependencies über libs.versions.toml
-**Edge-Cases-Korrekturen:** Serialisierungstests von 71.4% auf 100% Success Rate
-**Enterprise-Features validiert:** 5.000+ concurrent operations, 10MB+ objects
-**Produktionstauglichkeit erreicht:** Vollständige Performance-, Resilience- und Integration-Tests
-**Erweiterte Konfigurierbarkeit:** Performance-, Storage- und Development-Presets
-**Advanced Monitoring:** Connection-State-Listener und strukturierte Metriken
### 2025-08-14 - Previous
- **Bug Fix:** Compiler-Warnungen in `JacksonCacheSerializer` bezüglich identity-sensitiver Operationen behoben
- **Verbesserung:** Objects.equals() für sichere nullable Instant-Vergleiche
## Testing-Strategie: Zweistufig & Umfassend
### Integrationstests mit Testcontainers
Die Kernfunktionalität wird gegen eine echte Redis-Datenbank getestet, die zur Laufzeit in einem Docker-Container gestartet wird. Dies garantiert 100%ige Kompatibilität und realistische Performance-Messungen.
### Unit-Tests mit MockK
Die komplexe Logik der Offline-Fähigkeit und Synchronisation wird durch das Mocking des RedisTemplate getestet. So können Verbindungsausfälle, Timeouts und Netzwerkpartitionierung zuverlässig simuliert werden.
### End-to-End Produktionstests
Production-like Scenarios testen realistische Anwendungsfälle:
- User-Session-Management (1000 Sessions)
- Configuration-Caching mit verschiedenen TTLs
- API-Response-Caching (100 Endpoints)
- Cross-Service-Kommunikation
## Fazit & Status
Das **Infrastructure/Cache-Modul** ist **vollständig produktionsbereit** und erfüllt alle Enterprise-Anforderungen:
-**94.7% Test Success Rate** mit 39 umfassenden Tests
-**Professionelle Logging-Architektur** durchgängig etabliert
-**Enterprise-Performance** validiert (5.000+ concurrent ops)
-**Vollständige Resilience** bei Netzwerk- und Redis-Ausfällen
-**SINGLE SOURCE OF TRUTH** für alle Dependencies
-**Internationale Deployment-Fähigkeit** mit Unicode-Support
-**Advanced Monitoring** mit Connection-State-Tracking
-**Multi-Tenant-Capable** durch Key-Prefix-Isolation
**Empfehlung: ✅ BEREIT FÜR PRODUKTIONSEINSATZ**
Das Modul kann sofort in produktiven Umgebungen eingesetzt werden. Die umfassende Test-Suite und professionelle Architektur gewährleisten höchste Zuverlässigkeit und Performance.
+13 -3
View File
@@ -5,6 +5,12 @@ plugins {
alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.jvm)
} }
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}
// Erlaubt die Verwendung der kotlin.time API im gesamten Modul // Erlaubt die Verwendung der kotlin.time API im gesamten Modul
kotlin { kotlin {
compilerOptions { compilerOptions {
@@ -12,11 +18,15 @@ kotlin {
} }
} }
tasks.test {
useJUnitPlatform()
}
dependencies { dependencies {
// Stellt sicher, dass alle Versionen aus der zentralen BOM kommen. // Stellt sicher, dass alle Versionen aus der zentralen BOM kommen.
implementation(platform(projects.platform.platformBom)) api(platform(projects.platform.platformBom))
// Stellt gemeinsame Abhängigkeiten wie Logging bereit. // Stellt gemeinsame Abhängigkeiten wie Logging bereit und exportiert sie für Konsumenten der API.
implementation(projects.platform.platformDependencies) api(projects.platform.platformDependencies)
// Stellt Test-Abhängigkeiten bereit. // Stellt Test-Abhängigkeiten bereit.
testImplementation(projects.platform.platformTesting) testImplementation(projects.platform.platformTesting)
@@ -1,56 +1,56 @@
package at.mocode.infrastructure.cache.api package at.mocode.infrastructure.cache.api
/** /**
* Interface for serializing and deserializing cache entries. * Schnittstelle zum Serialisieren und Deserialisieren von Cache-Einträgen.
*/ */
interface CacheSerializer { interface CacheSerializer {
/** /**
* Serializes a value to a byte array. * Serialisiert einen Wert zu einem Byte-Array.
* *
* @param value The value to serialize * @param value Der zu serialisierende Wert
* @return The serialized value as a byte array * @return Der serialisierte Wert als Byte-Array
*/ */
fun <T : Any> serialize(value: T): ByteArray fun <T : Any> serialize(value: T): ByteArray
/** /**
* Deserializes a byte array to a value. * Deserialisiert ein Byte-Array zu einem Wert.
* *
* @param bytes The byte array to deserialize * @param bytes Das zu deserialisierende Byte-Array
* @param clazz The class of the value to deserialize to * @param clazz Die Zielklasse des zu deserialisierenden Werts
* @return The deserialized value * @return Der deserialisierte Wert
*/ */
fun <T : Any> deserialize(bytes: ByteArray, clazz: Class<T>): T fun <T : Any> deserialize(bytes: ByteArray, clazz: Class<T>): T
/** /**
* Serializes a cache entry to a byte array. * Serialisiert einen Cache-Eintrag zu einem Byte-Array.
* *
* @param entry The cache entry to serialize * @param entry Der zu serialisierende Cache-Eintrag
* @return The serialized cache entry as a byte array * @return Der serialisierte Cache-Eintrag als Byte-Array
*/ */
fun <T : Any> serializeEntry(entry: CacheEntry<T>): ByteArray fun <T : Any> serializeEntry(entry: CacheEntry<T>): ByteArray
/** /**
* Deserializes a byte array to a cache entry. * Deserialisiert ein Byte-Array zu einem Cache-Eintrag.
* *
* @param bytes The byte array to deserialize * @param bytes Das zu deserialisierende Byte-Array
* @param valueClass The class of the value in the cache entry * @param valueClass Die Klasse des Werts im Cache-Eintrag
* @return The deserialized cache entry * @return Der deserialisierte Cache-Eintrag
*/ */
fun <T : Any> deserializeEntry(bytes: ByteArray, valueClass: Class<T>): CacheEntry<T> fun <T : Any> deserializeEntry(bytes: ByteArray, valueClass: Class<T>): CacheEntry<T>
/** /**
* Compresses a byte array. * Komprimiert ein Byte-Array.
* *
* @param bytes The byte array to compress * @param bytes Das zu komprimierende Byte-Array
* @return The compressed byte array * @return Das komprimierte Byte-Array
*/ */
fun compress(bytes: ByteArray): ByteArray fun compress(bytes: ByteArray): ByteArray
/** /**
* Decompresses a byte array. * Dekomprimiert ein Byte-Array.
* *
* @param bytes The byte array to decompress * @param bytes Das zu dekomprimierende Byte-Array
* @return The decompressed byte array * @return Das dekomprimierte Byte-Array
*/ */
fun decompress(bytes: ByteArray): ByteArray fun decompress(bytes: ByteArray): ByteArray
} }
@@ -1,20 +1,20 @@
package at.mocode.infrastructure.cache.api package at.mocode.infrastructure.cache.api
/** /**
* Kotlin-idiomatic extension function to retrieve a value from the cache * Kotlin-idiomatische Extension-Funktion, um einen Wert aus dem Cache zu lesen
* using reified types. * mit reified Typen.
* *
* Example: `val user = cache.get<User>("user:123")` * Beispiel: `val user = cache.get<User>("user:123")`
*/ */
inline fun <reified T : Any> DistributedCache.get(key: String): T? { inline fun <reified T : Any> DistributedCache.get(key: String): T? {
return this.get(key, T::class.java) return this.get(key, T::class.java)
} }
/** /**
* Kotlin-idiomatic extension function to retrieve multiple values from the cache * Kotlin-idiomatische Extension-Funktion, um mehrere Werte aus dem Cache zu lesen
* using reified types. * mit reified Typen.
* *
* Example: `val users = cache.multiGet<User>(listOf("user:123", "user:124"))` * Beispiel: `val users = cache.multiGet<User>(listOf("user:123", "user:124"))`
*/ */
inline fun <reified T : Any> DistributedCache.multiGet(keys: Collection<String>): Map<String, T> { inline fun <reified T : Any> DistributedCache.multiGet(keys: Collection<String>): Map<String, T> {
return this.multiGet(keys, T::class.java) return this.multiGet(keys, T::class.java)
+15 -4
View File
@@ -3,13 +3,24 @@
plugins { plugins {
alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.spring) alias(libs.plugins.kotlin.spring)
alias(libs.plugins.spring.boot) // Als Bibliothek benötigt dieses Modul das Spring Boot Plugin nicht.
alias(libs.plugins.spring.dependencyManagement) alias(libs.plugins.spring.dependencyManagement)
} }
// Deaktiviert die Erstellung eines ausführbaren Jars für dieses Bibliotheks-Modul. // Stellt sicher, dass ein normales JAR gebaut wird (Bibliotheks-Modul).
tasks.getByName("bootJar") { java {
enabled = false withJavadocJar()
withSourcesJar()
}
tasks.test {
useJUnitPlatform()
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
} }
dependencies { dependencies {
@@ -82,6 +82,7 @@ class RedisDistributedCache(
// Store in a local cache // Store in a local cache
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
localCache[prefixedKey] = entry as CacheEntry<Any> localCache[prefixedKey] = entry as CacheEntry<Any>
enforceLocalCacheSize()
trackOperation(true) trackOperation(true)
return entry.value return entry.value
@@ -109,6 +110,7 @@ class RedisDistributedCache(
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
localCache[prefixedKey] = entry as CacheEntry<Any> localCache[prefixedKey] = entry as CacheEntry<Any>
enforceLocalCacheSize()
if (!isConnected()) { if (!isConnected()) {
markDirty(key) markDirty(key)
@@ -231,6 +233,7 @@ class RedisDistributedCache(
// Store in a local cache // Store in a local cache
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
localCache[key] = entry as CacheEntry<Any> localCache[key] = entry as CacheEntry<Any>
enforceLocalCacheSize()
// Add to result // Add to result
result[removePrefix(key)] = entry.value result[removePrefix(key)] = entry.value
@@ -263,6 +266,7 @@ class RedisDistributedCache(
) )
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
localCache[prefixedKey] = entry as CacheEntry<Any> localCache[prefixedKey] = entry as CacheEntry<Any>
enforceLocalCacheSize()
redisBatch[prefixedKey] = serializer.serializeEntry(entry) redisBatch[prefixedKey] = serializer.serializeEntry(entry)
} }
@@ -437,6 +441,22 @@ class RedisDistributedCache(
return if (config.keyPrefix.isEmpty()) key else key.substring(config.keyPrefix.length + 1) return if (config.keyPrefix.isEmpty()) key else key.substring(config.keyPrefix.length + 1)
} }
/**
* Erzwingt die maximale Größe des lokalen Caches, indem die am längsten nicht
* mehr modifizierten Einträge entfernt werden.
*/
private fun enforceLocalCacheSize() {
val max = config.localCacheMaxSize ?: return
val overflow = localCache.size - max
if (overflow <= 0) return
val toEvict = localCache.entries
.sortedBy { it.value.lastModifiedAt }
.take(overflow)
.map { it.key }
toEvict.forEach { localCache.remove(it) }
logger.debug("Evicted ${toEvict.size} entries to enforce local cache size limit $max")
}
private fun handleConnectionFailure(e: Exception) { private fun handleConnectionFailure(e: Exception) {
logger.warn("Redis connection failure: ${e.message}") logger.warn("Redis connection failure: ${e.message}")
setConnectionState(ConnectionState.DISCONNECTED) setConnectionState(ConnectionState.DISCONNECTED)
@@ -468,9 +488,9 @@ class RedisDistributedCache(
} }
/** /**
* Periodically check the connection to Redis. * Prüft periodisch die Verbindung zu Redis.
*/ */
@Scheduled(fixedDelayString = $$"${redis.connection-check-interval:10000}") @Scheduled(fixedDelayString = "\${redis.connection-check-interval:10000}")
fun checkConnection() { fun checkConnection() {
try { try {
redisTemplate.hasKey("connection-test") redisTemplate.hasKey("connection-test")
@@ -481,9 +501,9 @@ class RedisDistributedCache(
} }
/** /**
* Periodically clean up expired entries from the local cache. * Bereinigt periodisch abgelaufene Einträge aus dem lokalen Cache.
*/ */
@Scheduled(fixedDelayString = $$"${redis.local-cache-cleanup-interval:60000}") @Scheduled(fixedDelayString = "\${redis.local-cache-cleanup-interval:60000}")
fun cleanupLocalCache() { fun cleanupLocalCache() {
val now = Clock.System.now() val now = Clock.System.now()
val expiredKeys = localCache.entries val expiredKeys = localCache.entries
@@ -498,9 +518,9 @@ class RedisDistributedCache(
} }
/** /**
* Periodically synchronize dirty keys when connected. * Synchronisiert periodisch schmutzige Schlüssel, sobald verbunden.
*/ */
@Scheduled(fixedDelayString = $$"${redis.sync-interval:300000}") @Scheduled(fixedDelayString = "\${redis.sync-interval:300000}")
fun scheduledSync() { fun scheduledSync() {
if (isConnected() && dirtyKeys.isNotEmpty()) { if (isConnected() && dirtyKeys.isNotEmpty()) {
synchronize(null) synchronize(null)
@@ -512,7 +532,7 @@ class RedisDistributedCache(
// //
/** /**
* Track a cache operation for metrics * Zeichnet eine Cache-Operation für Metriken auf.
*/ */
private fun trackOperation(success: Boolean) { private fun trackOperation(success: Boolean) {
synchronized(this) { synchronized(this) {
@@ -522,7 +542,7 @@ class RedisDistributedCache(
} }
/** /**
* Get current performance metrics * Liefert aktuelle Performance-Metriken.
*/ */
fun getPerformanceMetrics(): Map<String, Any> { fun getPerformanceMetrics(): Map<String, Any> {
val now = Clock.System.now() val now = Clock.System.now()
@@ -543,9 +563,9 @@ class RedisDistributedCache(
} }
/** /**
* Log performance metrics (called periodically) * Loggt Performance-Metriken (periodisch aufgerufen).
*/ */
@Scheduled(fixedDelayString = $$"${redis.metrics-log-interval:300000}") @Scheduled(fixedDelayString = "\${redis.metrics-log-interval:300000}")
fun logPerformanceMetrics() { fun logPerformanceMetrics() {
val metrics = getPerformanceMetrics() val metrics = getPerformanceMetrics()
logger.info("Cache performance metrics: $metrics") logger.info("Cache performance metrics: $metrics")
@@ -553,7 +573,7 @@ class RedisDistributedCache(
} }
/** /**
* Cache warming utility - preloads specified keys * Cache-Warming-Helfer lädt angegebene Schlüssel vor.
*/ */
fun warmCache(keys: Collection<String>, dataLoader: (String) -> Any?) { fun warmCache(keys: Collection<String>, dataLoader: (String) -> Any?) {
logger.info("Starting cache warming for ${keys.size} keys") logger.info("Starting cache warming for ${keys.size} keys")
@@ -571,11 +591,11 @@ class RedisDistributedCache(
} }
val duration = Clock.System.now() - startTime val duration = Clock.System.now() - startTime
logger.info("Cache warming completed: $warmedCount/$${keys.size} keys loaded in $duration") logger.info("Cache warming completed: $warmedCount/${keys.size} keys loaded in $duration")
} }
/** /**
* Bulk cache warming with batch operations * Bulk-Cache-Warming mit Batch-Operationen.
*/ */
fun warmCacheBulk(keyDataMap: Map<String, Any>, ttl: Duration? = null) { fun warmCacheBulk(keyDataMap: Map<String, Any>, ttl: Duration? = null) {
logger.info("Starting bulk cache warming for ${keyDataMap.size} entries") logger.info("Starting bulk cache warming for ${keyDataMap.size} entries")
@@ -588,7 +608,7 @@ class RedisDistributedCache(
} }
/** /**
* Get cache health status * Liefert den Cache-Gesundheitsstatus.
*/ */
fun getHealthStatus(): Map<String, Any> { fun getHealthStatus(): Map<String, Any> {
val metrics = getPerformanceMetrics() val metrics = getPerformanceMetrics()
@@ -12,49 +12,49 @@ import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource
import java.time.Duration import java.time.Duration
/** /**
* Enhanced reactive security configuration for the Gateway. * Erweiterte reaktive Sicherheitskonfiguration für das Gateway.
* *
* ARCHITECTURE OVERVIEW: * ARCHITEKTUR-ÜBERBLICK:
* ======================
* Diese Konfiguration stellt die grundlegende Sicherheits-Schicht für das Spring Cloud Gateway bereit.
* Sie arbeitet zusammen mit mehreren weiteren Sicherheitskomponenten:
*
* 1. JwtAuthenticationFilter (GlobalFilter) Validiert JWT-Tokens und authentifiziert Benutzer
* 2. RateLimitingFilter (GlobalFilter) Bietet IP-basiertes Rate-Limiting mit benutzerbezogenen Limits
* 3. CorrelationIdFilter (GlobalFilter) Fügt Request-Tracing-Fähigkeiten hinzu
* 4. EnhancedLoggingFilter (GlobalFilter) Liefert strukturiertes Request/Response-Logging
*
* SICHERHEITSSTRATEGIE:
* ===================== * =====================
* This configuration establishes the foundational security layer for the Spring Cloud Gateway. * Das Gateway verwendet einen mehrschichtigen Sicherheitsansatz:
* It works in conjunction with several other security components: * - Diese SecurityWebFilterChain liefert grundlegende Einstellungen (CORS, CSRF, Basis-Header)
* - Der JwtAuthenticationFilter übernimmt die eigentliche Authentifizierung, wenn per Property aktiviert
* - Die SecurityWebFilterChain bleibt permissiv (permitAll), damit der JWT-Filter den Zugriff steuert
* - Rate-Limiting- und Logging-Filter liefern operative Sicherheit und Monitoring
* *
* 1. JwtAuthenticationFilter (GlobalFilter) - Handles JWT token validation and user authentication * ENTWURFSBEGRÜNDUNG:
* 2. RateLimitingFilter (GlobalFilter) - Provides IP-based rate limiting with user-aware limits * ===================
* 3. CorrelationIdFilter (GlobalFilter) - Adds request tracing capabilities * - Während Tests ist Spring Security auf dem Classpath (testImplementation), was
* 4. EnhancedLoggingFilter (GlobalFilter) - Provides structured request/response logging * die Auto-Konfiguration aktiviert und alle Endpunkte sperren kann, sofern keine SecurityWebFilterChain bereitgestellt wird
* - Das Gateway erzwingt Authentifizierung über den JwtAuthenticationFilter (falls per Property aktiviert),
* daher sollte die SecurityWebFilterChain permissiv bleiben und sich auf grundlegende Belange konzentrieren
* - Explizite CORS-Konfiguration stellt eine korrekte Behandlung von Cross-Origin-Anfragen aus Web-Clients sicher
* - Konfigurierbare Properties erlauben umgebungsspezifische Sicherheitseinstellungen ohne Codeänderungen
* - CSRF-Schutz ist deaktiviert, da er für zustandslose JWT-basierte Authentifizierung nicht benötigt wird
* *
* SECURITY STRATEGY: * CORS-INTEGRATION:
* ==================
* The Gateway employs a layered security approach:
* - This SecurityWebFilterChain provides foundational settings (CORS, CSRF, basic headers)
* - JwtAuthenticationFilter handles actual authentication when enabled via property
* - The SecurityWebFilterChain remains permissive (permitAll) to let the JWT filter control access
* - Rate limiting and logging filters provide operational security and monitoring
*
* DESIGN RATIONALE:
* ================= * =================
* - During tests, Spring Security is on the classpath (testImplementation), which enables * Die CORS-Konfiguration arbeitet mit der bestehenden Filterkette zusammen:
* security autoconfiguration and can lock down all endpoints unless a SecurityWebFilterChain is provided * - Erlaubt Anfragen von konfigurierten Ursprüngen (Dev/Prod-Umgebungen)
* - The Gateway enforces authentication using JwtAuthenticationFilter when enabled via property, * - Gibt benutzerdefinierte Header aus Gateway-Filtern frei (Korrelations-IDs, Rate-Limits)
* so the SecurityWebFilterChain should stay permissive and focus on foundational concerns * - Unterstützt Credentials für JWT-Authentifizierung
* - Explicit CORS configuration ensures proper handling of cross-origin requests from web clients * - Cacht Preflight-Antworten für bessere Performance
* - Configurable properties allow environment-specific security settings without code changes
* - CSRF protection is disabled as it's not needed for stateless JWT-based authentication
* *
* CORS INTEGRATION: * TESTHINWEISE:
* ================= * =============
* The CORS configuration works with the existing filter chain: * - Die Konfiguration ist so gestaltet, dass sie nahtlos mit bestehenden Sicherheitstests funktioniert
* - Allows requests from configured origins (dev/prod environments) * - Das Test-Profil kann CORS-Einstellungen bei Bedarf überschreiben
* - Exposes custom headers from Gateway filters (correlation IDs, rate limits) * - Eine permissive Autorisierung stellt sicher, dass Tests sich auf die Sicherheit der Filterebene konzentrieren können
* - Supports credentials for JWT authentication
* - Caches preflight responses for performance
*
* TESTING CONSIDERATIONS:
* =======================
* - Configuration is designed to work seamlessly with existing security tests
* - Test profile can override CORS settings if needed
* - Permissive authorization ensures tests can focus on filter-level security
*/ */
@Configuration @Configuration
@EnableConfigurationProperties(GatewaySecurityProperties::class) @EnableConfigurationProperties(GatewaySecurityProperties::class)
@@ -63,113 +63,113 @@ class SecurityConfig(
) { ) {
/** /**
* Main Spring Security filter chain configuration. * Hauptkonfiguration der Spring-Security-Filterkette.
* *
* This method configures the reactive security filter chain with: * Diese Methode konfiguriert die reaktive Sicherheits-Filterkette mit:
* - CSRF disabled for stateless API operation * - CSRF deaktiviert für zustandslosen API-Betrieb
* - Explicit CORS configuration for cross-origin support * - Expliziter CORS-Konfiguration für Cross-Origin-Unterstützung
* - Permissive authorization (authentication handled by JWT filter) * - Permissiver Autorisierung (Authentifizierung durch den JWT-Filter)
* *
* The configuration maintains compatibility with the existing filter architecture * Die Konfiguration bleibt kompatibel mit der bestehenden Filterarchitektur
* while providing enhanced CORS control and configurability. * und bietet zugleich bessere CORS-Steuerung und Konfigurierbarkeit.
*/ */
@Bean @Bean
fun springSecurityFilterChain(): SecurityWebFilterChain { fun springSecurityFilterChain(): SecurityWebFilterChain {
return ServerHttpSecurity.http() return ServerHttpSecurity.http()
.csrf { csrf -> .csrf { csrf ->
// Disable CSRF for stateless API gateway // CSRF für zustandsloses API-Gateway deaktivieren
// CSRF protection is not required for JWT-based stateless authentication // CSRF-Schutz ist für JWT-basierte zustandslose Authentifizierung nicht erforderlich
// The Gateway operates as a stateless proxy with no session state // Das Gateway arbeitet als zustandsloser Proxy ohne Session-Zustand
csrf.disable() csrf.disable()
} }
.cors { cors -> .cors { cors ->
// Use explicit CORS configuration instead of default // Explizite CORS-Konfiguration anstelle des Defaults verwenden
// This provides better control over cross-origin access policies // Dies ermöglicht eine bessere Kontrolle über Cross-Origin-Zugriffsrichtlinien
cors.configurationSource(corsConfigurationSource()) cors.configurationSource(corsConfigurationSource())
} }
.httpBasic { basic -> .httpBasic { basic ->
// Disable HTTP Basic auth for stateless API // HTTP Basic Auth für zustandslose API deaktivieren
basic.disable() basic.disable()
} }
.formLogin { form -> .formLogin { form ->
// Disable form login for API gateway // Formular-Login für API-Gateway deaktivieren
form.disable() form.disable()
} }
.authorizeExchange { exchanges -> .authorizeExchange { exchanges ->
// Allow all requests through Spring Security // Alle Anfragen durch Spring Security erlauben
// Authentication and authorization are handled by JwtAuthenticationFilter // Authentifizierung und Autorisierung erfolgen durch den JwtAuthenticationFilter
// This approach maintains the existing security architecture while // Dieser Ansatz bewahrt die bestehende Sicherheitsarchitektur und
// allowing the JWT filter to make granular access control decisions // ermöglicht dem JWT-Filter granulare Zugriffskontroll-Entscheidungen
exchanges.anyExchange().permitAll() exchanges.anyExchange().permitAll()
} }
.build() .build()
} }
/** /**
* Explicit CORS configuration source. * Explizite CORS-Konfigurationsquelle.
* *
* This bean provides detailed control over cross-origin resource sharing settings, * Dieser Bean bietet eine detaillierte Steuerung der Cross-Origin-Resource-Sharing-Einstellungen
* replacing the default empty CORS configuration with explicit, configurable settings. * und ersetzt die leere Standard-CORS-Konfiguration durch explizite, konfigurierbare Einstellungen.
* *
* Key features: * Schlüsselfunktionen:
* - Environment-specific allowed origins * - Umgebungsspezifische erlaubte Ursprünge (Allowed Origins)
* - Comprehensive HTTP method support * - Umfassende Unterstützung für HTTP-Methoden
* - JWT-aware header configuration * - JWT-bewusste Header-Konfiguration
* - Integration with Gateway filter headers * - Integration mit Headern aus Gateway-Filtern
* - Performance-optimized preflight caching * - Performance-optimiertes Preflight-Caching
* *
* The configuration is designed to work with typical web application architectures * Die Konfiguration ist darauf ausgelegt, mit typischen Webanwendungs-Architekturen zu funktionieren,
* where a JavaScript frontend makes API calls to the Gateway. * bei denen ein JavaScript-Frontend API-Aufrufe an das Gateway sendet.
*/ */
@Bean @Bean
fun corsConfigurationSource(): CorsConfigurationSource { fun corsConfigurationSource(): CorsConfigurationSource {
val configuration = CorsConfiguration().apply { val configuration = CorsConfiguration().apply {
// Allowed origins - configurable per environment // Erlaubte Ursprünge pro Umgebung konfigurierbar
// Development: localhost URLs for local testing // Entwicklung: localhost-URLs für lokale Tests
// Production: domain-specific URLs for deployed applications // Produktion: domainspezifische URLs für ausgelieferte Anwendungen
allowedOrigins = securityProperties.cors.allowedOrigins.toList() allowedOrigins = securityProperties.cors.allowedOrigins.toList()
// Allowed HTTP methods - comprehensive REST API support // Erlaubte HTTP-Methoden umfassende REST-API-Unterstützung
// Includes all standard methods plus OPTIONS for preflight requests // Enthält alle Standardmethoden plus OPTIONS für Preflight-Anfragen
allowedMethods = securityProperties.cors.allowedMethods.toList() allowedMethods = securityProperties.cors.allowedMethods.toList()
// Allowed request headers - includes JWT and custom headers // Erlaubte Request-Header beinhaltet JWT und benutzerdefinierte Header
// Authorization: for JWT Bearer tokens // Authorization: für JWT Bearer Tokens
// X-Correlation-ID: for request tracing // X-Correlation-ID: für Request-Tracing
// Standard headers: Content-Type, Accept, etc. // Standard-Header: Content-Type, Accept, etc.
allowedHeaders = securityProperties.cors.allowedHeaders.toList() allowedHeaders = securityProperties.cors.allowedHeaders.toList()
// Exposed response headers - allows client access to custom headers // Sichtbare Response-Header ermöglicht Client-Zugriff auf benutzerdefinierte Header
// Includes headers added by Gateway filters: // Beinhaltet Header, die von Gateway-Filtern hinzugefügt werden:
// - X-Correlation-ID from CorrelationIdFilter // - X-Correlation-ID vom CorrelationIdFilter
// - X-RateLimit-* from RateLimitingFilter // - X-RateLimit-* vom RateLimitingFilter
exposedHeaders = securityProperties.cors.exposedHeaders.toList() exposedHeaders = securityProperties.cors.exposedHeaders.toList()
// Allow credentials - required for JWT authentication // Credentials erlauben erforderlich für JWT-Authentifizierung
// Enables cookies and authorization headers in cross-origin requests // Aktiviert Cookies und Authorization-Header in Cross-Origin-Anfragen
allowCredentials = securityProperties.cors.allowCredentials allowCredentials = securityProperties.cors.allowCredentials
// Preflight cache duration - performance optimization // Preflight-Cache-Dauer Performance-Optimierung
// Reduces the number of OPTIONS requests for repeated API calls // Reduziert die Anzahl an OPTIONS-Anfragen für wiederholte API-Aufrufe
maxAge = securityProperties.cors.maxAge.seconds maxAge = securityProperties.cors.maxAge.seconds
} }
return UrlBasedCorsConfigurationSource().apply { return UrlBasedCorsConfigurationSource().apply {
// Apply CORS configuration to all Gateway routes // CORS-Konfiguration auf alle Gateway-Routen anwenden
registerCorsConfiguration("/**", configuration) registerCorsConfiguration("/**", configuration)
} }
} }
} }
/** /**
* Configuration properties for Gateway security settings. * Konfigurationseigenschaften für die Sicherheits-Einstellungen des Gateways.
* *
* Enables environment-specific security configuration via application.yml/properties. * Ermöglicht umgebungsspezifische Sicherheitskonfiguration über application.yml/-properties.
* This approach allows different security settings across development, testing, and * Dieser Ansatz erlaubt unterschiedliche Sicherheitseinstellungen für Entwicklung, Test und
* production environments without requiring code changes. * Produktion, ohne Codeänderungen vornehmen zu müssen.
* *
* Example application.yml configuration: * Beispielkonfiguration in application.yml:
* ```yaml * ```yaml
* gateway: * gateway:
* security: * security:
@@ -192,26 +192,26 @@ data class GatewaySecurityProperties(
) )
/** /**
* CORS-specific configuration properties with sensible defaults. * CORS-spezifische Konfigurationseigenschaften mit sinnvollen Defaults.
* *
* Default values are chosen to work with typical development and production setups: * Die Default-Werte sind so gewählt, dass sie mit typischen Entwicklungs- und Produktions-Setups funktionieren:
* - Common development URLs (localhost with standard ports) * - Übliche Entwicklungs-URLs (localhost mit Standardports)
* - Production domain pattern matching * - Produktions-Domain-Muster
* - Full REST API method support * - Volle Unterstützung der REST-API-Methoden
* - JWT and Gateway filter header support * - Unterstützung für JWT- und Gateway-Filter-Header
* - Reasonable preflight cache duration * - Sinnvolle Preflight-Cache-Dauer
*/ */
data class CorsProperties( data class CorsProperties(
/** /**
* Allowed origins for CORS requests. * Erlaubte Ursprünge (Allowed Origins) für CORS-Anfragen.
* *
* Defaults support common development and production scenarios: * Defaults unterstützen gängige Entwicklungs- und Produktionsszenarien:
* - localhost:3000 - typical React development server * - localhost:3000 typischer React-Entwicklungsserver
* - localhost:8080 - common alternative development port * - localhost:8080 gängiger alternativer Entwicklungsport
* - localhost:4200 - typical Angular development server * - localhost:4200 typischer Angular-Entwicklungsserver
* - Specific meldestelle.at subdomains for production * - Spezifische Subdomains von meldestelle.at für die Produktion
* *
* Can be overridden per environment as needed. * Kann je Umgebung bei Bedarf überschrieben werden.
*/ */
val allowedOrigins: Set<String> = setOf( val allowedOrigins: Set<String> = setOf(
"http://localhost:3000", "http://localhost:3000",
@@ -224,23 +224,23 @@ data class CorsProperties(
/** /**
* Allowed HTTP methods for CORS requests. * Erlaubte HTTP-Methoden für CORS-Anfragen.
* *
* Includes all standard REST API methods plus OPTIONS for preflight * Enthält alle Standard-REST-API-Methoden sowie OPTIONS für Preflight-
* and HEAD for metadata requests. * und HEAD für Metadaten-Anfragen.
*/ */
val allowedMethods: Set<String> = setOf( val allowedMethods: Set<String> = setOf(
"GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH" "GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH"
), ),
/** /**
* Allowed request headers for CORS requests. * Erlaubte Request-Header für CORS-Anfragen.
* *
* Includes: * Beinhaltet:
* - Standard headers: Content-Type, Accept, etc. * - Standard-Header: Content-Type, Accept, etc.
* - JWT authentication: Authorization * - JWT-Authentifizierung: Authorization
* - Gateway tracing: X-Correlation-ID * - Gateway-Tracing: X-Correlation-ID
* - Cache control: Cache-Control, Pragma * - Cache-Steuerung: Cache-Control, Pragma
*/ */
val allowedHeaders: Set<String> = setOf( val allowedHeaders: Set<String> = setOf(
"Authorization", "Authorization",
@@ -254,13 +254,13 @@ data class CorsProperties(
), ),
/** /**
* Exposed response headers for CORS requests. * Sichtbare Response-Header für CORS-Anfragen.
* *
* Headers that client JavaScript can access in responses. * Header, auf die Client-JavaScript in Antworten zugreifen darf.
* Includes custom headers added by Gateway filters: * Beinhaltet benutzerdefinierte Header, die von Gateway-Filtern hinzugefügt werden:
* - X-Correlation-ID: request tracing (CorrelationIdFilter) * - X-Correlation-ID: Request-Tracing (CorrelationIdFilter)
* - X-RateLimit-*: rate limiting info (RateLimitingFilter) * - X-RateLimit-*: Informationen zum Rate-Limiting (RateLimitingFilter)
* - Standard headers: Content-Length, Date * - Standard-Header: Content-Length, Date
*/ */
val exposedHeaders: Set<String> = setOf( val exposedHeaders: Set<String> = setOf(
"X-Correlation-ID", "X-Correlation-ID",
@@ -272,21 +272,21 @@ data class CorsProperties(
), ),
/** /**
* Allow credentials in CORS requests. * Credentials in CORS-Anfragen erlauben.
* *
* Set to true to support: * Auf true setzen, um zu unterstützen:
* - JWT Bearer tokens in Authorization headers * - JWT Bearer Tokens im Authorization-Header
* - Cookies (if used) * - Cookies (falls verwendet)
* - Client certificates (if used) * - Client-Zertifikate (falls verwendet)
*/ */
val allowCredentials: Boolean = true, val allowCredentials: Boolean = true,
/** /**
* Maximum age for preflight request caching. * Maximales Alter für das Caching von Preflight-Anfragen.
* *
* Duration that browsers can cache preflight responses, reducing * Dauer, für die Browser Preflight-Antworten cachen können, wodurch
* the number of OPTIONS requests for repeated API calls. * die Anzahl der OPTIONS-Anfragen für wiederholte API-Aufrufe reduziert wird.
* Default: 1 hour (reasonable balance of performance vs. flexibility) * Default: 1 Stunde (guter Kompromiss zwischen Performance und Flexibilität)
*/ */
val maxAge: Duration = Duration.ofHours(1) val maxAge: Duration = Duration.ofHours(1)
) )
@@ -35,140 +35,142 @@ spring:
max-idle-time: 15s max-idle-time: 15s
max-life-time: 60s max-life-time: 60s
# Verbesserte CORS-Konfiguration # Verbesserte CORS-Konfiguration
globalcors:
corsConfigurations:
'[/**]':
allowedOriginPatterns:
- "https://*.meldestelle.at"
- "http://localhost:*"
allowedMethods:
- GET
- POST
- PUT
- DELETE
- PATCH
- OPTIONS
allowedHeaders:
- "*"
allowCredentials: true
maxAge: 3600
# Antwort-Header bereinigen und globale Filter # Antwort-Header bereinigen und globale Filter
default-filters:
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
- name: CircuitBreaker
args:
name: defaultCircuitBreaker
fallbackUri: forward:/fallback
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY,GATEWAY_TIMEOUT
methods: GET,POST,PUT,DELETE
backoff:
firstBackoff: 50ms
maxBackoff: 500ms
factor: 2
basedOnPreviousValue: false
# Security Headers for enhanced protection
- name: AddResponseHeader
args:
name: X-Content-Type-Options
value: nosniff
- name: AddResponseHeader
args:
name: X-Frame-Options
value: DENY
- name: AddResponseHeader
args:
name: X-XSS-Protection
value: 1; mode=block
- name: AddResponseHeader
args:
name: Referrer-Policy
value: strict-origin-when-cross-origin
- name: AddResponseHeader
args:
name: Cache-Control
value: no-cache, no-store, must-revalidate
# Route definitions with service discovery # Route definitions with service discovery
routes: server:
# Health Check und Gateway Info Routes webflux:
- id: gateway-info-route default-filters:
uri: http://localhost:${server.port} - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
predicates:
- Path=/
- Method=GET
filters:
- SetStatus=200
- SetResponseHeader=Content-Type,application/json
# Members Service Routes
- id: members-service-route
uri: lb://members-service
predicates:
- Path=/api/members/**
filters:
- StripPrefix=1
- name: CircuitBreaker - name: CircuitBreaker
args: args:
name: membersCircuitBreaker name: defaultCircuitBreaker
fallbackUri: forward:/fallback/members fallbackUri: forward:/fallback
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY,GATEWAY_TIMEOUT
methods: GET,POST,PUT,DELETE
backoff:
firstBackoff: 50ms
maxBackoff: 500ms
factor: 2
basedOnPreviousValue: false
# Security Headers for enhanced protection
- name: AddResponseHeader
args:
name: X-Content-Type-Options
value: nosniff
- name: AddResponseHeader
args:
name: X-Frame-Options
value: DENY
- name: AddResponseHeader
args:
name: X-XSS-Protection
value: 1; mode=block
- name: AddResponseHeader
args:
name: Referrer-Policy
value: strict-origin-when-cross-origin
- name: AddResponseHeader
args:
name: Cache-Control
value: no-cache, no-store, must-revalidate
routes:
# Health Check und Gateway Info Routes
- id: gateway-info-route
uri: http://localhost:${server.port}
predicates:
- Path=/
- Method=GET
filters:
- SetStatus=200
- SetResponseHeader=Content-Type,application/json
# Horses Service Routes # Members Service Routes
- id: horses-service-route - id: members-service-route
uri: lb://horses-service uri: lb://members-service
predicates: predicates:
- Path=/api/horses/** - Path=/api/members/**
filters: filters:
- StripPrefix=1 - StripPrefix=1
- name: CircuitBreaker - name: CircuitBreaker
args: args:
name: horsesCircuitBreaker name: membersCircuitBreaker
fallbackUri: forward:/fallback/horses fallbackUri: forward:/fallback/members
# Events Service Routes # Horses Service Routes
- id: events-service-route - id: horses-service-route
uri: lb://events-service uri: lb://horses-service
predicates: predicates:
- Path=/api/events/** - Path=/api/horses/**
filters: filters:
- StripPrefix=1 - StripPrefix=1
- name: CircuitBreaker - name: CircuitBreaker
args: args:
name: eventsCircuitBreaker name: horsesCircuitBreaker
fallbackUri: forward:/fallback/events fallbackUri: forward:/fallback/horses
# Masterdata Service Routes # Events Service Routes
- id: masterdata-service-route - id: events-service-route
uri: lb://masterdata-service uri: lb://events-service
predicates: predicates:
- Path=/api/masterdata/** - Path=/api/events/**
filters: filters:
- StripPrefix=1 - StripPrefix=1
- name: CircuitBreaker - name: CircuitBreaker
args: args:
name: masterdataCircuitBreaker name: eventsCircuitBreaker
fallbackUri: forward:/fallback/masterdata fallbackUri: forward:/fallback/events
# Auth Service Routes (if exists) # Masterdata Service Routes
- id: auth-service-route - id: masterdata-service-route
uri: lb://auth-service uri: lb://masterdata-service
predicates: predicates:
- Path=/api/auth/** - Path=/api/masterdata/**
filters: filters:
- StripPrefix=1 - StripPrefix=1
- name: CircuitBreaker - name: CircuitBreaker
args: args:
name: authCircuitBreaker name: masterdataCircuitBreaker
fallbackUri: forward:/fallback/auth fallbackUri: forward:/fallback/masterdata
# Ping Service Routes (existing) # Auth Service Routes (if exists)
- id: ping-service-route - id: auth-service-route
uri: lb://ping-service uri: lb://auth-service
predicates: predicates:
- Path=/api/ping/** - Path=/api/auth/**
filters: filters:
- StripPrefix=1 - StripPrefix=1
- name: CircuitBreaker
args:
name: authCircuitBreaker
fallbackUri: forward:/fallback/auth
# Ping Service Routes (existing)
- id: ping-service-route
uri: lb://ping-service
predicates:
- Path=/api/ping/**
filters:
- StripPrefix=1
globalcors:
cors-configurations:
'[/**]':
allowedOriginPatterns:
- "https://*.meldestelle.at"
- "http://localhost:*"
allowedMethods:
- GET
- POST
- PUT
- DELETE
- PATCH
- OPTIONS
allowedHeaders:
- "*"
allowCredentials: true
maxAge: 3600
# Circuit Breaker Configuration # Circuit Breaker Configuration
resilience4j: resilience4j:
@@ -228,14 +230,14 @@ management:
probes: probes:
enabled: true enabled: true
metrics: metrics:
enabled: true access: unrestricted
info: info:
enabled: true access: unrestricted
prometheus: prometheus:
enabled: true access: unrestricted
gateway: gateway:
enabled: true access: unrestricted
circuitbreakers: circuit breakers:
enabled: true enabled: true
metrics: metrics:
export: export: