upgrade Java-25 Kotlin-2.3.0 usw.

This commit is contained in:
2025-12-24 13:54:22 +01:00
parent 98a9504cbd
commit 84c3cfd787
9 changed files with 228 additions and 344 deletions
+12 -11
View File
@@ -1,3 +1,5 @@
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
// Dieses Modul ist das API-Gateway und der einzige öffentliche Einstiegspunkt // Dieses Modul ist das API-Gateway und der einzige öffentliche Einstiegspunkt
// für alle externen Anfragen an das Meldestelle-System. // für alle externen Anfragen an das Meldestelle-System.
plugins { plugins {
@@ -22,16 +24,15 @@ dependencies {
implementation(projects.backend.infrastructure.monitoring.monitoringClient) implementation(projects.backend.infrastructure.monitoring.monitoringClient)
// === GATEWAY-SPEZIFISCHE ABHÄNGIGKEITEN === // === GATEWAY-SPEZIFISCHE ABHÄNGIGKEITEN ===
implementation(libs.bundles.spring.cloud.gateway) // Kern-Gateway inkl. Security, Actuator, CircuitBreaker, Discovery
implementation(libs.bundles.spring.boot.security) implementation(libs.bundles.gateway.core)
implementation(libs.bundles.resilience) // Ergänzende Observability (Logging, Jackson)
implementation("org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j") implementation(libs.bundles.gateway.observability)
implementation(libs.spring.boot.starter.actuator) // Wichtig für Health & Metrics // Redis-Unterstützung für verteiltes Rate Limiting (RequestRateLimiter)
implementation(libs.bundles.logging) // Umgestellt auf das spezifische Gateway-Redis-Bundle (einfach, leicht zu konfigurieren)
implementation(libs.bundles.jackson.kotlin) implementation(libs.bundles.gateway.redis)
// WICHTIG: PostgreSQL Treiber hinzufügen! // Hinweis: Der Gateway benötigt keinen Datenbanktreiber → entfernt
implementation(libs.postgresql.driver)
// === Test Dependencies === // === Test Dependencies ===
testImplementation(projects.platform.platformTesting) testImplementation(projects.platform.platformTesting)
@@ -50,7 +51,7 @@ sourceSets {
} }
} }
val integrationTestImplementation by configurations.getting { val integrationTestImplementation: Configuration? by configurations.getting {
extendsFrom(configurations.testImplementation.get()) extendsFrom(configurations.testImplementation.get())
} }
@@ -71,6 +72,6 @@ tasks.register<Test>("integrationTest") {
showExceptions = true showExceptions = true
showCauses = true showCauses = true
showStackTraces = true showStackTraces = true
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL exceptionFormat = TestExceptionFormat.FULL
} }
} }
@@ -1,5 +1,3 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.infrastructure.gateway.config package at.mocode.infrastructure.gateway.config
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -13,7 +11,7 @@ import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange import org.springframework.web.server.ServerWebExchange
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import kotlin.uuid.Uuid import java.util.UUID
/** /**
* Gateway-Konfiguration für erweiterte Funktionalitäten wie Logging, Rate Limiting und Security. * Gateway-Konfiguration für erweiterte Funktionalitäten wie Logging, Rate Limiting und Security.
@@ -32,7 +30,7 @@ class CorrelationIdFilter : GlobalFilter, Ordered {
override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain): Mono<Void> { override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain): Mono<Void> {
val request = exchange.request val request = exchange.request
val correlationId = request.headers.getFirst(CORRELATION_ID_HEADER) val correlationId = request.headers.getFirst(CORRELATION_ID_HEADER)
?: Uuid.random().toString() ?: UUID.randomUUID().toString()
val mutatedRequest = request.mutate() val mutatedRequest = request.mutate()
.header(CORRELATION_ID_HEADER, correlationId) .header(CORRELATION_ID_HEADER, correlationId)
@@ -50,207 +48,3 @@ class CorrelationIdFilter : GlobalFilter, Ordered {
override fun getOrder(): Int = Ordered.HIGHEST_PRECEDENCE override fun getOrder(): Int = Ordered.HIGHEST_PRECEDENCE
} }
/**
* Enhanced Logging Filter für strukturiertes Logging mit Request/Response Details.
*/
@Component
class EnhancedLoggingFilter : GlobalFilter, Ordered {
private val logger = LoggerFactory.getLogger(EnhancedLoggingFilter::class.java)
override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain): Mono<Void> {
val startTime = System.currentTimeMillis()
val request = exchange.request
val correlationId = request.headers.getFirst(CorrelationIdFilter.CORRELATION_ID_HEADER)
logRequest(request, correlationId)
return chain.filter(exchange)
.doOnSuccess {
val responseTime = System.currentTimeMillis() - startTime
logResponse(exchange.response, correlationId, responseTime)
}
.doOnError { error ->
val responseTime = System.currentTimeMillis() - startTime
logError(error, correlationId, responseTime)
}
}
private fun logRequest(request: ServerHttpRequest, correlationId: String?) {
logger.info("""
[REQUEST] [{}]
Method: {}
URI: {}
RemoteAddress: {}
UserAgent: {}
""".trimIndent(),
correlationId,
request.method,
request.uri,
request.remoteAddress,
request.headers.getFirst("User-Agent")
)
}
private fun logResponse(response: ServerHttpResponse, correlationId: String?, responseTime: Long) {
logger.info("""
[RESPONSE] [{}]
Status: {}
ResponseTime: {}ms
""".trimIndent(),
correlationId,
response.statusCode,
responseTime
)
}
private fun logError(error: Throwable, correlationId: String?, responseTime: Long) {
logger.error("""
[ERROR] [{}]
Error: {}
ResponseTime: {}ms
""".trimIndent(),
correlationId,
error.message,
responseTime,
error
)
}
override fun getOrder(): Int = Ordered.HIGHEST_PRECEDENCE + 1
}
/**
* Rate Limiting Filter basierend auf IP-Adresse und User-Typ.
*
* Optimierungen:
* - Memory-Leak-Schutz durch regelmäßige Bereinigung alter Einträge
* - Sichere Rollenvalidierung basierend auf JWT-Authentifizierung
* - Bessere Verteilung der Rate-Limits basierend auf Benutzerrollen
*/
@Component
class RateLimitingFilter : GlobalFilter, Ordered {
private val requestCounts = ConcurrentHashMap<String, RequestCounter>()
private val logger = LoggerFactory.getLogger(RateLimitingFilter::class.java)
// Timestamp der letzten Bereinigung
@Volatile
private var lastCleanup = System.currentTimeMillis()
companion object {
const val RATE_LIMIT_ENABLED_HEADER = "X-RateLimit-Enabled"
const val RATE_LIMIT_LIMIT_HEADER = "X-RateLimit-Limit"
const val RATE_LIMIT_REMAINING_HEADER = "X-RateLimit-Remaining"
// Rate Limits pro Minute
const val ANONYMOUS_LIMIT = 50
const val AUTHENTICATED_LIMIT = 200
const val ADMIN_LIMIT = 500
const val AUTH_ENDPOINT_LIMIT = 20
const val DEFAULT_LIMIT = 100
// Bereinigungsintervall: alle 5 Minuten
const val CLEANUP_INTERVAL_MS = 5 * 60 * 1000L
// Einträge, die älter als 10 Minuten sind, werden entfernt
const val ENTRY_MAX_AGE_MS = 10 * 60 * 1000L
}
data class RequestCounter(
var count: Int = 0,
var lastReset: Long = System.currentTimeMillis()
)
override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain): Mono<Void> {
val request = exchange.request
val response = exchange.response
val clientIp = getClientIp(request)
val path = request.path.value()
// Periodische Bereinigung des Caches zur Vermeidung von memory Leaks
performPeriodicCleanup()
val limit = determineRateLimit(request, path)
val counter = requestCounts.computeIfAbsent(clientIp) { RequestCounter() }
// Zähler zurücksetzen, wenn mehr als eine Minute vergangen ist
val now = System.currentTimeMillis()
if (now - counter.lastReset > 60_000) {
counter.count = 0
counter.lastReset = now
}
counter.count++
// Rate-Limit-Header hinzufügen
response.headers.add(RATE_LIMIT_ENABLED_HEADER, "true")
response.headers.add(RATE_LIMIT_LIMIT_HEADER, limit.toString())
response.headers.add(RATE_LIMIT_REMAINING_HEADER, maxOf(0, limit - counter.count).toString())
return if (counter.count > limit) {
response.statusCode = HttpStatus.TOO_MANY_REQUESTS
response.setComplete()
} else {
chain.filter(exchange)
}
}
private fun getClientIp(request: ServerHttpRequest): String {
return request.headers.getFirst("X-Forwarded-For")?.split(",")?.first()?.trim()
?: request.headers.getFirst("X-Real-IP")
?: request.remoteAddress?.address?.hostAddress
?: "unknown"
}
private fun determineRateLimit(request: ServerHttpRequest, path: String): Int {
return when {
path.startsWith("/api/auth") -> AUTH_ENDPOINT_LIMIT
isAdminUser(request) -> ADMIN_LIMIT
isAuthenticatedUser(request) -> AUTHENTICATED_LIMIT
else -> ANONYMOUS_LIMIT
}
}
private fun isAuthenticatedUser(request: ServerHttpRequest): Boolean {
return request.headers.getFirst("Authorization") != null
}
private fun isAdminUser(request: ServerHttpRequest): Boolean {
// Sichere Rollenvalidierung basierend auf JWT-Authentifizierung
// die X-User-Role wird vom JwtAuthenticationFilter nach erfolgreicher JWT-Validierung gesetzt
val userRole = request.headers.getFirst("X-User-Role")
val userId = request.headers.getFirst("X-User-ID")
// Zusätzliche Sicherheitsprüfung: Beide Header müssen vorhanden sein.
// Dies reduziert die Wahrscheinlichkeit von Header-Spoofing
return userRole == "ADMIN" && userId != null
}
/**
* Bereinigt alte Einträge aus dem requestCounts Cache zur Vermeidung von memory Leaks.
* Wird nur alle CLEANUP_INTERVAL_MS ausgeführt für bessere Performance.
*/
private fun performPeriodicCleanup() {
val now = System.currentTimeMillis()
if (now - lastCleanup > CLEANUP_INTERVAL_MS) {
val sizeBefore = requestCounts.size
val cutoffTime = now - ENTRY_MAX_AGE_MS
// Entferne alle Einträge, die älter als ENTRY_MAX_AGE_MS sind
requestCounts.entries.removeIf { (_, counter) ->
counter.lastReset < cutoffTime
}
lastCleanup = now
val sizeAfter = requestCounts.size
if (sizeBefore > sizeAfter) {
logger.debug("Rate limit cache cleanup: removed {} old entries, {} entries remaining",
sizeBefore - sizeAfter, sizeAfter)
}
}
}
override fun getOrder(): Int = Ordered.HIGHEST_PRECEDENCE + 2
}
@@ -0,0 +1,34 @@
package at.mocode.infrastructure.gateway.config
import at.mocode.infrastructure.gateway.config.CorrelationIdFilter.Companion.CORRELATION_ID_HEADER
import org.slf4j.MDC
import org.springframework.cloud.gateway.filter.GatewayFilterChain
import org.springframework.cloud.gateway.filter.GlobalFilter
import org.springframework.core.Ordered
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import reactor.core.publisher.Mono
/**
* Minimaler MDC-Filter: schreibt die vorhandene X-Correlation-ID in den MDC,
* damit Logs die ID automatisch mitführen. Keine Body-/PII-Logs, nur Header-ID.
*
* Reihenfolge: direkt nach dem CorrelationIdFilter ausführen, damit die ID
* bereits gesetzt ist. Daher Order = HIGHEST_PRECEDENCE + 1.
*/
@Component
class MdcCorrelationFilter : GlobalFilter, Ordered {
override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain): Mono<Void> {
val correlationId = exchange.request.headers.getFirst(CORRELATION_ID_HEADER)
if (correlationId != null) {
MDC.put(CORRELATION_ID_HEADER, correlationId)
}
return chain.filter(exchange)
// Bei Abschluss säubern, um Leaks über Thread-Grenzen zu vermeiden
.doFinally { MDC.remove(CORRELATION_ID_HEADER) }
}
override fun getOrder(): Int = Ordered.HIGHEST_PRECEDENCE + 1
}
@@ -0,0 +1,29 @@
package at.mocode.infrastructure.gateway.config
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import reactor.core.publisher.Mono
import java.security.Principal
@Configuration
class RateLimiterConfig {
/**
* KeyResolver basierend auf authentifiziertem Principal; Fallback auf Client-IP.
* Funktioniert out-of-the-box mit Keycloak (Resource Server), sofern Security aktiv ist.
*/
@Bean
fun principalNameKeyResolver(): KeyResolver = KeyResolver { exchange ->
exchange.getPrincipal<Principal>()
.map { it.name }
.switchIfEmpty(
Mono.just(
exchange.request.headers.getFirst("X-Forwarded-For")?.split(",")?.first()?.trim()
?: exchange.request.headers.getFirst("X-Real-IP")
?: exchange.request.remoteAddress?.address?.hostAddress
?: "unknown"
)
)
}
}
@@ -20,6 +20,11 @@ class FallbackController {
return createFallbackResponse("members-service", "Member operations are temporarily unavailable") return createFallbackResponse("members-service", "Member operations are temporarily unavailable")
} }
@RequestMapping(value = ["/ping"], method = [RequestMethod.GET, RequestMethod.POST])
fun pingFallback(): ResponseEntity<ErrorResponse> {
return createFallbackResponse("ping-service", "Ping service is temporarily unavailable")
}
@RequestMapping(value = ["/horses"], method = [RequestMethod.GET, RequestMethod.POST]) @RequestMapping(value = ["/horses"], method = [RequestMethod.GET, RequestMethod.POST])
fun horsesFallback(): ResponseEntity<ErrorResponse> { fun horsesFallback(): ResponseEntity<ErrorResponse> {
return createFallbackResponse("horses-service", "Horse registry operations are temporarily unavailable") return createFallbackResponse("horses-service", "Horse registry operations are temporarily unavailable")
@@ -1,12 +1,14 @@
package at.mocode.infrastructure.gateway.health package at.mocode.infrastructure.gateway.health
import org.springframework.boot.actuate.health.Health import org.springframework.boot.actuate.health.Health
import org.springframework.boot.actuate.health.HealthIndicator import org.springframework.boot.actuate.health.ReactiveHealthIndicator
import org.springframework.cloud.client.discovery.DiscoveryClient import org.springframework.cloud.client.discovery.DiscoveryClient
import org.springframework.core.env.Environment import org.springframework.core.env.Environment
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.WebClientResponseException import org.springframework.web.reactive.function.client.WebClientResponseException
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import java.time.Duration import java.time.Duration
/** /**
@@ -20,7 +22,7 @@ class GatewayHealthIndicator(
private val discoveryClient: DiscoveryClient, private val discoveryClient: DiscoveryClient,
private val webClient: WebClient.Builder, private val webClient: WebClient.Builder,
private val environment: Environment private val environment: Environment
) : HealthIndicator { ) : ReactiveHealthIndicator {
companion object { companion object {
private val CRITICAL_SERVICES = setOf( private val CRITICAL_SERVICES = setOf(
@@ -38,105 +40,105 @@ class GatewayHealthIndicator(
private val HEALTH_CHECK_TIMEOUT = Duration.ofSeconds(5) private val HEALTH_CHECK_TIMEOUT = Duration.ofSeconds(5)
} }
override fun health(): Health { override fun health(): Mono<Health> {
val builder = Health.up() val builder = Health.up()
val details = mutableMapOf<String, Any>() val details = mutableMapOf<String, Any>()
try { return Mono.fromCallable { discoveryClient.services }
// Prüfe alle registrierten Services in Consul .flatMapMany { services ->
val allServices = discoveryClient.services details["totalServices"] = services.size
val discoveredServices = mutableMapOf<String, Any>() Flux.fromIterable(services)
}
allServices.forEach { serviceName -> .flatMap({ serviceName ->
val instances = discoveryClient.getInstances(serviceName) val instances = discoveryClient.getInstances(serviceName)
discoveredServices[serviceName] = mapOf( val instanceDetails = mapOf(
"instanceCount" to instances.size, "instanceCount" to instances.size,
"instances" to instances.map { "${it.host}:${it.port}" } "instances" to instances.map { "${it.host}:${it.port}" }
) )
} // Für Health-Check nur auf definierte Services gehen
val checkMono = when {
details["discoveredServices"] = discoveredServices CRITICAL_SERVICES.contains(serviceName) || OPTIONAL_SERVICES.contains(serviceName) ->
details["totalServices"] = allServices.size checkServiceHealthReactive(serviceName)
else -> Mono.just("SKIPPED")
// Prüfe kritische Services
val criticalServiceStatus = mutableMapOf<String, String>()
var hasCriticalFailure = false
CRITICAL_SERVICES.forEach { serviceName ->
val status = checkServiceHealth(serviceName)
criticalServiceStatus[serviceName] = status
if (status != "UP") {
hasCriticalFailure = true
} }
} checkMono
.map { status -> Triple(serviceName, status, instanceDetails) }
}, 8) // begrenze Parallelität
.collectList()
.map { results ->
val discoveredServices = mutableMapOf<String, Any>()
val criticalServiceStatus = mutableMapOf<String, String>()
val optionalServiceStatus = mutableMapOf<String, String>()
// Prüfe optionale Services results.forEach { (serviceName, status, instanceDetails) ->
val optionalServiceStatus = mutableMapOf<String, String>() discoveredServices[serviceName] = instanceDetails
OPTIONAL_SERVICES.forEach { serviceName -> if (CRITICAL_SERVICES.contains(serviceName)) {
optionalServiceStatus[serviceName] = checkServiceHealth(serviceName) criticalServiceStatus[serviceName] = status
} } else if (OPTIONAL_SERVICES.contains(serviceName)) {
optionalServiceStatus[serviceName] = status
details["criticalServices"] = criticalServiceStatus }
details["optionalServices"] = optionalServiceStatus
// Gateway Status basierend auf kritischen Services
val isTestEnvironment = environment.activeProfiles.contains("test")
val isDevEnvironment = environment.activeProfiles.contains("dev")
if (hasCriticalFailure && !isTestEnvironment && !isDevEnvironment) {
builder.down()
details["status"] = "DOWN"
details["reason"] = "Ein oder mehrere kritische Services sind nicht verfügbar"
} else {
details["status"] = "UP"
details["reason"] = when {
isTestEnvironment -> "Gesundheitsprüfung erfolgreich (Testumgebung)"
isDevEnvironment -> "Gesundheitsprüfung erfolgreich (Entwicklungsumgebung - nicht alle Services erforderlich)"
else -> "Alle kritischen Services sind verfügbar"
} }
details["discoveredServices"] = discoveredServices
details["criticalServices"] = criticalServiceStatus
details["optionalServices"] = optionalServiceStatus
val isTestEnvironment = environment.activeProfiles.contains("test")
val isDevEnvironment = environment.activeProfiles.contains("dev")
val hasCriticalFailure = criticalServiceStatus.values.any { it != "UP" }
if (hasCriticalFailure && !isTestEnvironment && !isDevEnvironment) {
builder.down()
details["status"] = "DOWN"
details["reason"] = "Ein oder mehrere kritische Services sind nicht verfügbar"
} else {
details["status"] = "UP"
details["reason"] = when {
isTestEnvironment -> "Gesundheitsprüfung erfolgreich (Testumgebung)"
isDevEnvironment -> "Gesundheitsprüfung erfolgreich (Entwicklungsumgebung - nicht alle Services erforderlich)"
else -> "Alle kritischen Services sind verfügbar oder optional"
}
}
builder.withDetails(details).build()
}
.onErrorResume { ex ->
Mono.just(
Health.down(ex)
.withDetail("status", "DOWN")
.withDetail("reason", "Fehler beim Prüfen der nachgelagerten Services: ${ex.message}")
.build()
)
} }
} catch (exception: Exception) {
builder.down()
.withException(exception)
details["status"] = "DOWN"
details["reason"] = "Fehler beim Prüfen der nachgelagerten Services: ${exception.message}"
}
return builder.withDetails(details).build()
} }
private fun checkServiceHealth(serviceName: String): String { private fun checkServiceHealthReactive(serviceName: String): Mono<String> {
return try { return Mono.fromCallable { discoveryClient.getInstances(serviceName) }
val instances = discoveryClient.getInstances(serviceName) .flatMap { instances ->
if (instances.isEmpty()) {
if (instances.isEmpty()) { Mono.just("NO_INSTANCES")
"NO_INSTANCES" } else {
} else { val instance = instances.first()
// Versuche Health-Check für die erste verfügbare Instanz val healthUrl = "http://${instance.host}:${instance.port}/actuator/health"
val instance = instances.first() val client = webClient.build()
val healthUrl = "http://${instance.host}:${instance.port}/actuator/health" client.get()
.uri(healthUrl)
val client = webClient.build() .retrieve()
val response = client.get() .bodyToMono(Map::class.java)
.uri(healthUrl) .timeout(HEALTH_CHECK_TIMEOUT)
.retrieve() .map { it["status"]?.toString() ?: "UNKNOWN" }
.bodyToMono(Map::class.java) .map { status -> if (status == "UP") "UP" else "DOWN" }
.timeout(HEALTH_CHECK_TIMEOUT) .onErrorResume { ex ->
.onErrorReturn(mapOf("status" to "DOWN")) when (ex) {
.block() is WebClientResponseException -> when (ex.statusCode.value()) {
404 -> Mono.just("NO_HEALTH_ENDPOINT")
val status = response?.get("status")?.toString() ?: "UNKNOWN" 503 -> Mono.just("DOWN")
if (status == "UP") "UP" else "DOWN" else -> Mono.just("ERROR")
}
else -> Mono.just("ERROR")
}
}
}
} }
} catch (exception: WebClientResponseException) {
when (exception.statusCode.value()) {
404 -> "NO_HEALTH_ENDPOINT"
503 -> "DOWN"
else -> "ERROR"
}
} catch (_: Exception) {
"ERROR"
}
} }
} }
@@ -16,6 +16,11 @@ spring:
user: user:
name: ${GATEWAY_ADMIN_USER:admin} name: ${GATEWAY_ADMIN_USER:admin}
password: ${GATEWAY_ADMIN_PASSWORD:admin} password: ${GATEWAY_ADMIN_PASSWORD:admin}
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
timeout: 3s
cloud: cloud:
consul: consul:
host: ${CONSUL_HOST:localhost} host: ${CONSUL_HOST:localhost}
@@ -38,18 +43,14 @@ spring:
max-life-time: 60s max-life-time: 60s
default-filters: default-filters:
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
- name: CircuitBreaker
args:
name: defaultCircuitBreaker
fallbackUri: forward:/fallback
- name: Retry - name: Retry
args: args:
retries: 3 retries: 2
statuses: BAD_GATEWAY,GATEWAY_TIMEOUT statuses: BAD_GATEWAY,GATEWAY_TIMEOUT
methods: GET,POST,PUT,DELETE methods: GET
backoff: backoff:
firstBackoff: 50ms firstBackoff: 100ms
maxBackoff: 500ms maxBackoff: 1000ms
factor: 2 factor: 2
basedOnPreviousValue: false basedOnPreviousValue: false
- name: AddResponseHeader - name: AddResponseHeader
@@ -60,10 +61,6 @@ spring:
args: args:
name: X-Frame-Options name: X-Frame-Options
value: DENY value: DENY
- name: AddResponseHeader
args:
name: X-XSS-Protection
value: 1; mode=block
- name: AddResponseHeader - name: AddResponseHeader
args: args:
name: Referrer-Policy name: Referrer-Policy
@@ -100,6 +97,15 @@ spring:
- Path=/api/ping/** - Path=/api/ping/**
filters: filters:
- StripPrefix=1 - StripPrefix=1
- name: CircuitBreaker
args:
name: pingCircuitBreaker
fallbackUri: forward:/fallback/ping
- name: RequestRateLimiter
args:
key-resolver: "#{@principalNameKeyResolver}"
redis-rate-limiter.replenishRate: ${PING_RATE_LIMIT_REPLENISH_RATE:50}
redis-rate-limiter.burstCapacity: ${PING_RATE_LIMIT_BURST:100}
# ============================================================== # ==============================================================
# --- Entries-Service-Integration (MP-27) --- # --- Entries-Service-Integration (MP-27) ---
@@ -215,6 +221,8 @@ resilience4j:
instances: instances:
defaultCircuitBreaker: defaultCircuitBreaker:
baseConfig: default baseConfig: default
pingCircuitBreaker:
baseConfig: default
membersCircuitBreaker: membersCircuitBreaker:
baseConfig: default baseConfig: default
slidingWindowSize: 50 slidingWindowSize: 50
@@ -248,8 +256,8 @@ management:
allow-credentials: true allow-credentials: true
endpoint: endpoint:
health: health:
show-details: always show-details: when_authorized
show-components: always show-components: when_authorized
probes: probes:
enabled: true enabled: true
metrics: metrics:
@@ -304,21 +312,10 @@ management:
logging: logging:
level: level:
org.springframework.cloud.gateway: INFO org.springframework.cloud.gateway: INFO
org.springframework.cloud.loadbalancer: DEBUG org.springframework.cloud.loadbalancer: INFO
org.springframework.cloud.consul: INFO org.springframework.cloud.consul: INFO
at.mocode.infrastructure.gateway: DEBUG at.mocode.infrastructure.gateway: INFO
io.github.resilience4j: INFO io.github.resilience4j: INFO
reactor.netty.http.client: INFO reactor.netty.http.client: INFO
org.springframework.security: WARN
org.springframework.web: INFO # (Redis-Konfiguration wurde in den bestehenden spring.data.redis-Block oben integriert)
pattern:
console: "%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr([%X{correlationId:-}]){yellow} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{correlationId:-}] %logger{36} - %msg%n"
file:
name: infrastructure/gateway/logs/gateway.log
logback:
rolling policy:
clean-history-on-start: true
max-file-size: 100MB
total-size-cap: 1GB
max-history: 30
@@ -27,10 +27,8 @@ import org.springframework.test.context.ActiveProfiles
"gateway.security.jwt.enabled=false", "gateway.security.jwt.enabled=false",
// Reaktiven Web-Anwendungstyp verwenden // Reaktiven Web-Anwendungstyp verwenden
"spring.main.web-application-type=reactive", "spring.main.web-application-type=reactive",
// Gateway Discovery deaktivieren // Gateway Discovery deaktivieren (korrekte Property)
"spring.cloud.gateway.server.webflux.discovery.locator.enabled=false", "spring.cloud.gateway.discovery.locator.enabled=false",
// Actuator Security deaktivieren
"management.security.enabled=false",
// Zufälligen Port setzen // Zufälligen Port setzen
"server.port=0" "server.port=0"
] ]
+28 -4
View File
@@ -157,6 +157,8 @@ springdoc-openapi-starter-webmvc-ui = { module = "org.springdoc:springdoc-openap
# --- Spring Cloud --- # --- Spring Cloud ---
spring-cloud-starter-gateway-server-webflux = { module = "org.springframework.cloud:spring-cloud-starter-gateway-server-webflux" } spring-cloud-starter-gateway-server-webflux = { module = "org.springframework.cloud:spring-cloud-starter-gateway-server-webflux" }
spring-cloud-starter-consul-discovery = { module = "org.springframework.cloud:spring-cloud-starter-consul-discovery" } spring-cloud-starter-consul-discovery = { module = "org.springframework.cloud:spring-cloud-starter-consul-discovery" }
spring-cloud-starter-circuitbreaker-resilience4j = { module = "org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j" }
# --- Database & Persistence --- # --- Database & Persistence ---
exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" } exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" }
@@ -318,8 +320,34 @@ spring-cloud-gateway = [
"spring-cloud-starter-consul-discovery" "spring-cloud-starter-consul-discovery"
] ]
# --- Gateway spezifische Bundles ---
# Kernabhängigkeiten des Gateways, ohne optionale Observability/DB.
gateway-core = [
"spring-cloud-starter-gateway-server-webflux",
"spring-cloud-starter-consul-discovery",
"spring-boot-starter-actuator",
"spring-boot-starter-security",
"spring-boot-starter-oauth2-resource-server",
"spring-security-oauth2-jose",
"spring-cloud-starter-circuitbreaker-resilience4j"
]
# Ergänzende Bundles, die das Gateway häufig nutzt (getrennt für klare Steuerung)
gateway-observability = [
"kotlin-logging-jvm",
"logback-classic",
"logback-core",
"jackson-module-kotlin",
"jackson-datatype-jsr310"
]
# --- NEW BUNDLES --- # --- NEW BUNDLES ---
# Redis für Gateway (Rate Limiting, einfache Konfiguration)
gateway-redis = [
"spring-boot-starter-data-redis"
]
# Ktor Server bundles # Ktor Server bundles
ktor-server-common = [ ktor-server-common = [
"ktor-server-core", "ktor-server-core",
@@ -516,8 +544,4 @@ spring-dependencyManagement = { id = "io.spring.dependency-management", version.
foojayResolver = { id = "org.gradle.toolchains.foojay-resolver-convention", version.ref = "foojayResolver" } foojayResolver = { id = "org.gradle.toolchains.foojay-resolver-convention", version.ref = "foojayResolver" }
# Dokka plugin # Dokka plugin
# Version pinned to work with Kotlin 2.2.x
# See: https://github.com/Kotlin/dokka
# Note: dokka 2.0.0+ matches Kotlin 2.0+; verify compatibility if bumping Kotlin
# Using latest stable known compatible as of 2025-10
dokka = { id = "org.jetbrains.dokka", version = "2.1.0" } dokka = { id = "org.jetbrains.dokka", version = "2.1.0" }