refactoring: Docker-Dateien Api-Gateway
This commit is contained in:
@@ -34,14 +34,14 @@ PGADMIN_EMAIL=user@domain.com
|
||||
PGADMIN_PASSWORD=strong-password
|
||||
PGADMIN_PORT=8888:80
|
||||
|
||||
# --- PROMETHEUS (Metriken) ---
|
||||
PROMETHEUS_PORT=9090:9090
|
||||
|
||||
# --- GRAFANA (Monitoring GUI) ---
|
||||
GF_ADMIN_USER=gf-admin
|
||||
GF_ADMIN_PASSWORD=gf-password
|
||||
GF_PORT=3000:3000
|
||||
|
||||
# --- PROMETHEUS (Metriken) ---
|
||||
PROMETHEUS_PORT=9090:9090
|
||||
|
||||
# --- SERVICE DISCOVERY (Consul) ---
|
||||
CONSUL_PORT=8500:8500
|
||||
|
||||
@@ -50,3 +50,6 @@ CONSUL_PORT=8500:8500
|
||||
GATEWAY_PORT=8081
|
||||
# Debug Port für IntelliJ (Remote JVM Debug)
|
||||
GATEWAY_DEBUG_PORT=5005
|
||||
|
||||
# --- MICROSERVICES ---
|
||||
PING_SERVICE_PORT=8082:8082
|
||||
|
||||
@@ -202,6 +202,48 @@ services:
|
||||
networks:
|
||||
- meldestelle-network
|
||||
|
||||
# ==========================================
|
||||
# MICROSERVICES
|
||||
# ==========================================
|
||||
ping-service:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: dockerfiles/services/ping-service/Dockerfile
|
||||
args:
|
||||
GRADLE_VERSION: 9.1.0
|
||||
JAVA_VERSION: 21
|
||||
VERSION: 1.0.0
|
||||
BUILD_DATE: "2025-11-20"
|
||||
container_name: ${COMPOSE_PROJECT_NAME}-ping-service
|
||||
restart: no # "${RESTART_POLICY:-unless-stopped}"
|
||||
ports:
|
||||
- "${PING_SERVICE_PORT}"
|
||||
- "5006:5005" # Debug Port
|
||||
environment:
|
||||
SPRING_PROFILES_ACTIVE: docker
|
||||
DEBUG: "true"
|
||||
SERVER_PORT: 8082
|
||||
|
||||
# --- CONSUL ---
|
||||
SPRING_CLOUD_CONSUL_HOST: consul
|
||||
SPRING_CLOUD_CONSUL_PORT: 8500
|
||||
SPRING_CLOUD_CONSUL_DISCOVERY_HOSTNAME: ping-service
|
||||
|
||||
# --- DATENBANK VERBINDUNG (Das hat gefehlt!) ---
|
||||
# Wir nutzen die Container-Namen aus deiner .env Variable
|
||||
SPRING_DATASOURCE_URL: jdbc:postgresql://${COMPOSE_PROJECT_NAME}-postgres:5432/${POSTGRES_DB}
|
||||
SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER}
|
||||
SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
# WICHTIG: Wir wollen nur validieren, nichts erstellen.
|
||||
SPRING_JPA_HIBERNATE_DDL_AUTO: validate
|
||||
|
||||
# --- REDIS (DAS HAT GEFEHLT!) ---
|
||||
# Wir nutzen den Service-Namen, genau wie bei Postgres
|
||||
SPRING_DATA_REDIS_HOST: ${COMPOSE_PROJECT_NAME}-redis
|
||||
SPRING_DATA_REDIS_PORT: 6379
|
||||
networks:
|
||||
- meldestelle-network
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
pgadmin-data:
|
||||
|
||||
@@ -1,73 +1,76 @@
|
||||
// Dieses Modul ist das API-Gateway und der einzige öffentliche Einstiegspunkt
|
||||
// für alle externen Anfragen an das Meldestelle-System.
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinJvm)
|
||||
alias(libs.plugins.kotlinSpring)
|
||||
alias(libs.plugins.kotlinJpa)
|
||||
alias(libs.plugins.spring.boot)
|
||||
alias(libs.plugins.spring.dependencyManagement)
|
||||
alias(libs.plugins.kotlinJvm)
|
||||
alias(libs.plugins.kotlinSpring)
|
||||
alias(libs.plugins.kotlinJpa)
|
||||
alias(libs.plugins.spring.boot)
|
||||
alias(libs.plugins.spring.dependencyManagement)
|
||||
}
|
||||
|
||||
// Konfiguriert die Hauptklasse für das ausführbare JAR
|
||||
springBoot {
|
||||
mainClass.set("at.mocode.infrastructure.gateway.GatewayApplicationKt")
|
||||
mainClass.set("at.mocode.infrastructure.gateway.GatewayApplicationKt")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(platform(projects.platform.platformBom))
|
||||
implementation(platform(projects.platform.platformBom))
|
||||
|
||||
// === Core Dependencies ===
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(projects.platform.platformDependencies)
|
||||
implementation(projects.infrastructure.monitoring.monitoringClient)
|
||||
// === Core Dependencies ===
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(projects.platform.platformDependencies)
|
||||
implementation(projects.infrastructure.monitoring.monitoringClient)
|
||||
|
||||
// === GATEWAY-SPEZIFISCHE ABHÄNGIGKEITEN ===
|
||||
implementation(libs.bundles.spring.cloud.gateway)
|
||||
implementation(libs.bundles.spring.boot.security)
|
||||
implementation(libs.bundles.resilience)
|
||||
implementation("org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j")
|
||||
implementation(libs.spring.boot.starter.actuator) // Wichtig für Health & Metrics
|
||||
implementation(libs.bundles.logging)
|
||||
implementation(libs.bundles.jackson.kotlin)
|
||||
// === GATEWAY-SPEZIFISCHE ABHÄNGIGKEITEN ===
|
||||
implementation(libs.bundles.spring.cloud.gateway)
|
||||
implementation(libs.bundles.spring.boot.security)
|
||||
implementation(libs.bundles.resilience)
|
||||
implementation("org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j")
|
||||
implementation(libs.spring.boot.starter.actuator) // Wichtig für Health & Metrics
|
||||
implementation(libs.bundles.logging)
|
||||
implementation(libs.bundles.jackson.kotlin)
|
||||
|
||||
// === Test Dependencies ===
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
testImplementation(libs.bundles.testing.jvm)
|
||||
// WICHTIG: PostgreSQL Treiber hinzufügen!
|
||||
implementation(libs.postgresql.driver)
|
||||
|
||||
// === Test Dependencies ===
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
testImplementation(libs.bundles.testing.jvm)
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
// Konfiguration für Integration Tests
|
||||
sourceSets {
|
||||
val integrationTest by creating {
|
||||
compileClasspath += sourceSets.main.get().output
|
||||
runtimeClasspath += sourceSets.main.get().output
|
||||
}
|
||||
val integrationTest by creating {
|
||||
compileClasspath += sourceSets.main.get().output
|
||||
runtimeClasspath += sourceSets.main.get().output
|
||||
}
|
||||
}
|
||||
|
||||
val integrationTestImplementation by configurations.getting {
|
||||
extendsFrom(configurations.testImplementation.get())
|
||||
extendsFrom(configurations.testImplementation.get())
|
||||
}
|
||||
|
||||
tasks.register<Test>("integrationTest") {
|
||||
description = "Führt die Integration Tests aus"
|
||||
group = "verification"
|
||||
description = "Führt die Integration Tests aus"
|
||||
group = "verification"
|
||||
|
||||
testClassesDirs = sourceSets["integrationTest"].output.classesDirs
|
||||
classpath = sourceSets["integrationTest"].runtimeClasspath
|
||||
testClassesDirs = sourceSets["integrationTest"].output.classesDirs
|
||||
classpath = sourceSets["integrationTest"].runtimeClasspath
|
||||
|
||||
useJUnitPlatform()
|
||||
useJUnitPlatform()
|
||||
|
||||
shouldRunAfter("test")
|
||||
shouldRunAfter("test")
|
||||
|
||||
testLogging {
|
||||
events("passed", "skipped", "failed")
|
||||
showStandardStreams = false
|
||||
showExceptions = true
|
||||
showCauses = true
|
||||
showStackTraces = true
|
||||
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
|
||||
}
|
||||
testLogging {
|
||||
events("passed", "skipped", "failed")
|
||||
showStandardStreams = false
|
||||
showExceptions = true
|
||||
showCauses = true
|
||||
showStackTraces = true
|
||||
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
|
||||
}
|
||||
}
|
||||
|
||||
+109
-109
@@ -17,126 +17,126 @@ import java.time.Duration
|
||||
*/
|
||||
@Component
|
||||
class GatewayHealthIndicator(
|
||||
private val discoveryClient: DiscoveryClient,
|
||||
private val webClient: WebClient.Builder,
|
||||
private val environment: Environment
|
||||
private val discoveryClient: DiscoveryClient,
|
||||
private val webClient: WebClient.Builder,
|
||||
private val environment: Environment
|
||||
) : HealthIndicator {
|
||||
|
||||
companion object {
|
||||
private val CRITICAL_SERVICES = setOf(
|
||||
"members-service",
|
||||
"horses-service",
|
||||
"events-service",
|
||||
"masterdata-service",
|
||||
"auth-service"
|
||||
companion object {
|
||||
private val CRITICAL_SERVICES = setOf(
|
||||
"ping-service"
|
||||
)
|
||||
|
||||
private val OPTIONAL_SERVICES = setOf(
|
||||
"members-service",
|
||||
"horses-service",
|
||||
"events-service",
|
||||
"masterdata-service",
|
||||
"auth-service"
|
||||
)
|
||||
|
||||
private val HEALTH_CHECK_TIMEOUT = Duration.ofSeconds(5)
|
||||
}
|
||||
|
||||
override fun health(): Health {
|
||||
val builder = Health.up()
|
||||
val details = mutableMapOf<String, Any>()
|
||||
|
||||
try {
|
||||
// Prüfe alle registrierten Services in Consul
|
||||
val allServices = discoveryClient.services
|
||||
val discoveredServices = mutableMapOf<String, Any>()
|
||||
|
||||
allServices.forEach { serviceName ->
|
||||
val instances = discoveryClient.getInstances(serviceName)
|
||||
discoveredServices[serviceName] = mapOf(
|
||||
"instanceCount" to instances.size,
|
||||
"instances" to instances.map { "${it.host}:${it.port}" }
|
||||
)
|
||||
}
|
||||
|
||||
private val OPTIONAL_SERVICES = setOf(
|
||||
"ping-service"
|
||||
)
|
||||
details["discoveredServices"] = discoveredServices
|
||||
details["totalServices"] = allServices.size
|
||||
|
||||
private val HEALTH_CHECK_TIMEOUT = Duration.ofSeconds(5)
|
||||
}
|
||||
// Prüfe kritische Services
|
||||
val criticalServiceStatus = mutableMapOf<String, String>()
|
||||
var hasCriticalFailure = false
|
||||
|
||||
override fun health(): Health {
|
||||
val builder = Health.up()
|
||||
val details = mutableMapOf<String, Any>()
|
||||
|
||||
try {
|
||||
// Prüfe alle registrierten Services in Consul
|
||||
val allServices = discoveryClient.services
|
||||
val discoveredServices = mutableMapOf<String, Any>()
|
||||
|
||||
allServices.forEach { serviceName ->
|
||||
val instances = discoveryClient.getInstances(serviceName)
|
||||
discoveredServices[serviceName] = mapOf(
|
||||
"instanceCount" to instances.size,
|
||||
"instances" to instances.map { "${it.host}:${it.port}" }
|
||||
)
|
||||
}
|
||||
|
||||
details["discoveredServices"] = discoveredServices
|
||||
details["totalServices"] = allServices.size
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// Prüfe optionale Services
|
||||
val optionalServiceStatus = mutableMapOf<String, String>()
|
||||
OPTIONAL_SERVICES.forEach { serviceName ->
|
||||
optionalServiceStatus[serviceName] = checkServiceHealth(serviceName)
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
} catch (exception: Exception) {
|
||||
builder.down()
|
||||
.withException(exception)
|
||||
details["status"] = "DOWN"
|
||||
details["reason"] = "Fehler beim Prüfen der nachgelagerten Services: ${exception.message}"
|
||||
CRITICAL_SERVICES.forEach { serviceName ->
|
||||
val status = checkServiceHealth(serviceName)
|
||||
criticalServiceStatus[serviceName] = status
|
||||
if (status != "UP") {
|
||||
hasCriticalFailure = true
|
||||
}
|
||||
}
|
||||
|
||||
return builder.withDetails(details).build()
|
||||
}
|
||||
// Prüfe optionale Services
|
||||
val optionalServiceStatus = mutableMapOf<String, String>()
|
||||
OPTIONAL_SERVICES.forEach { serviceName ->
|
||||
optionalServiceStatus[serviceName] = checkServiceHealth(serviceName)
|
||||
}
|
||||
|
||||
private fun checkServiceHealth(serviceName: String): String {
|
||||
return try {
|
||||
val instances = discoveryClient.getInstances(serviceName)
|
||||
details["criticalServices"] = criticalServiceStatus
|
||||
details["optionalServices"] = optionalServiceStatus
|
||||
|
||||
if (instances.isEmpty()) {
|
||||
"NO_INSTANCES"
|
||||
} else {
|
||||
// Versuche Health-Check für die erste verfügbare Instanz
|
||||
val instance = instances.first()
|
||||
val healthUrl = "http://${instance.host}:${instance.port}/actuator/health"
|
||||
// Gateway Status basierend auf kritischen Services
|
||||
val isTestEnvironment = environment.activeProfiles.contains("test")
|
||||
val isDevEnvironment = environment.activeProfiles.contains("dev")
|
||||
|
||||
val client = webClient.build()
|
||||
val response = client.get()
|
||||
.uri(healthUrl)
|
||||
.retrieve()
|
||||
.bodyToMono(Map::class.java)
|
||||
.timeout(HEALTH_CHECK_TIMEOUT)
|
||||
.onErrorReturn(mapOf("status" to "DOWN"))
|
||||
.block()
|
||||
|
||||
val status = response?.get("status")?.toString() ?: "UNKNOWN"
|
||||
if (status == "UP") "UP" else "DOWN"
|
||||
}
|
||||
} catch (exception: WebClientResponseException) {
|
||||
when (exception.statusCode.value()) {
|
||||
404 -> "NO_HEALTH_ENDPOINT"
|
||||
503 -> "DOWN"
|
||||
else -> "ERROR"
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
"ERROR"
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
} 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 {
|
||||
return try {
|
||||
val instances = discoveryClient.getInstances(serviceName)
|
||||
|
||||
if (instances.isEmpty()) {
|
||||
"NO_INSTANCES"
|
||||
} else {
|
||||
// Versuche Health-Check für die erste verfügbare Instanz
|
||||
val instance = instances.first()
|
||||
val healthUrl = "http://${instance.host}:${instance.port}/actuator/health"
|
||||
|
||||
val client = webClient.build()
|
||||
val response = client.get()
|
||||
.uri(healthUrl)
|
||||
.retrieve()
|
||||
.bodyToMono(Map::class.java)
|
||||
.timeout(HEALTH_CHECK_TIMEOUT)
|
||||
.onErrorReturn(mapOf("status" to "DOWN"))
|
||||
.block()
|
||||
|
||||
val status = response?.get("status")?.toString() ?: "UNKNOWN"
|
||||
if (status == "UP") "UP" else "DOWN"
|
||||
}
|
||||
} catch (exception: WebClientResponseException) {
|
||||
when (exception.statusCode.value()) {
|
||||
404 -> "NO_HEALTH_ENDPOINT"
|
||||
503 -> "DOWN"
|
||||
else -> "ERROR"
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
"ERROR"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+98
-97
@@ -21,96 +21,96 @@ import java.time.Duration
|
||||
@EnableWebFluxSecurity
|
||||
@EnableConfigurationProperties(GatewaySecurityProperties::class)
|
||||
class SecurityConfig(
|
||||
private val securityProperties: GatewaySecurityProperties
|
||||
private val securityProperties: GatewaySecurityProperties
|
||||
) {
|
||||
|
||||
/**
|
||||
* Konfiguriert die zentrale Security-Filter-Kette für das Gateway.
|
||||
*
|
||||
* Diese Konfiguration nutzt den Standard-OAuth2-Resource-Server von Spring Security,
|
||||
* um JWTs (z.B. von Keycloak) automatisch zu validieren.
|
||||
*/
|
||||
@Bean
|
||||
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
return http { // Start der modernen Kotlin-DSL
|
||||
// 1. CORS-Konfiguration anwenden
|
||||
cors { }
|
||||
/**
|
||||
* Konfiguriert die zentrale Security-Filter-Kette für das Gateway.
|
||||
*
|
||||
* Diese Konfiguration nutzt den Standard-OAuth2-Resource-Server von Spring Security,
|
||||
* um JWTs (z.B. von Keycloak) automatisch zu validieren.
|
||||
*/
|
||||
@Bean
|
||||
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
return http { // Start der modernen Kotlin-DSL
|
||||
// 1. CORS-Konfiguration anwenden
|
||||
cors { }
|
||||
|
||||
// 2. CSRF deaktivieren (für zustandslose APIs)
|
||||
csrf { disable() }
|
||||
// 2. CSRF deaktivieren (für zustandslose APIs)
|
||||
csrf { disable() }
|
||||
|
||||
// 3. Routen-Berechtigungen definieren
|
||||
authorizeExchange {
|
||||
// Öffentlich zugängliche Pfade aus der .yml-Datei laden
|
||||
authorize(
|
||||
pathMatchers(*securityProperties.publicPaths.toTypedArray()),
|
||||
permitAll
|
||||
)
|
||||
// Alle anderen Pfade erfordern eine Authentifizierung
|
||||
authorize(anyExchange, authenticated)
|
||||
}
|
||||
// 3. Routen-Berechtigungen definieren
|
||||
authorizeExchange {
|
||||
// Öffentlich zugängliche Pfade aus der .yml-Datei laden
|
||||
authorize(
|
||||
pathMatchers(*securityProperties.publicPaths.toTypedArray()),
|
||||
permitAll
|
||||
)
|
||||
// Alle anderen Pfade erfordern eine Authentifizierung
|
||||
authorize(anyExchange, authenticated)
|
||||
}
|
||||
|
||||
// 4. JWT-Validierung via Keycloak aktivieren
|
||||
oauth2ResourceServer {
|
||||
jwt { }
|
||||
}
|
||||
}
|
||||
// 4. JWT-Validierung via Keycloak aktivieren
|
||||
oauth2ResourceServer {
|
||||
jwt { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen ReactiveJwtDecoder für die JWT-Validierung.
|
||||
*
|
||||
* Verwendet die JWK Set URI aus der Konfiguration, um die öffentlichen Schlüssel
|
||||
* von Keycloak zu laden. Falls die URI nicht konfiguriert ist oder Keycloak
|
||||
* nicht erreichbar ist, wird trotzdem ein Bean erstellt, um Startfehler zu vermeiden.
|
||||
*/
|
||||
@Bean
|
||||
fun reactiveJwtDecoder(
|
||||
@Value($$"${spring.security.oauth2.resourceserver.jwt.jwk-set-uri:}") jwkSetUri: String
|
||||
): ReactiveJwtDecoder {
|
||||
return if (jwkSetUri.isNotBlank()) {
|
||||
try {
|
||||
NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build()
|
||||
} catch (e: Exception) {
|
||||
// Log warning and return a no-op decoder to allow startup
|
||||
println("WARN: Failed to configure JWT decoder with JWK Set URI: $jwkSetUri - ${e.message}")
|
||||
println("WARN: JWT authentication will not work until Keycloak is available")
|
||||
createNoOpJwtDecoder()
|
||||
}
|
||||
} else {
|
||||
println("INFO: No JWK Set URI configured, using no-op JWT decoder")
|
||||
createNoOpJwtDecoder()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen No-Op JWT Decoder für Fälle, in denen Keycloak nicht verfügbar ist.
|
||||
* Dieser Decoder lehnt alle Token ab, erlaubt aber den Anwendungsstart.
|
||||
*/
|
||||
private fun createNoOpJwtDecoder(): ReactiveJwtDecoder {
|
||||
return ReactiveJwtDecoder { token ->
|
||||
throw IllegalStateException("JWT validation is not available - Keycloak may not be running")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Definiert die zentrale und einzige CORS-Konfiguration für das Gateway.
|
||||
*/
|
||||
@Bean
|
||||
fun corsConfigurationSource(): CorsConfigurationSource {
|
||||
val configuration = CorsConfiguration().apply {
|
||||
allowedOriginPatterns = securityProperties.cors.allowedOriginPatterns.toList()
|
||||
allowedMethods = securityProperties.cors.allowedMethods.toList()
|
||||
allowedHeaders = securityProperties.cors.allowedHeaders.toList()
|
||||
exposedHeaders = securityProperties.cors.exposedHeaders.toList()
|
||||
allowCredentials = securityProperties.cors.allowCredentials
|
||||
maxAge = securityProperties.cors.maxAge.seconds
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen ReactiveJwtDecoder für die JWT-Validierung.
|
||||
*
|
||||
* Verwendet die JWK Set URI aus der Konfiguration, um die öffentlichen Schlüssel
|
||||
* von Keycloak zu laden. Falls die URI nicht konfiguriert ist oder Keycloak
|
||||
* nicht erreichbar ist, wird trotzdem ein Bean erstellt, um Startfehler zu vermeiden.
|
||||
*/
|
||||
@Bean
|
||||
fun reactiveJwtDecoder(
|
||||
@Value($$"${spring.security.oauth2.resourceserver.jwt.jwk-set-uri:}") jwkSetUri: String
|
||||
): ReactiveJwtDecoder {
|
||||
return if (jwkSetUri.isNotBlank()) {
|
||||
try {
|
||||
NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build()
|
||||
} catch (e: Exception) {
|
||||
// Log warning and return a no-op decoder to allow startup
|
||||
println("WARN: Failed to configure JWT decoder with JWK Set URI: $jwkSetUri - ${e.message}")
|
||||
println("WARN: JWT authentication will not work until Keycloak is available")
|
||||
createNoOpJwtDecoder()
|
||||
}
|
||||
} else {
|
||||
println("INFO: No JWK Set URI configured, using no-op JWT decoder")
|
||||
createNoOpJwtDecoder()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen No-Op JWT Decoder für Fälle, in denen Keycloak nicht verfügbar ist.
|
||||
* Dieser Decoder lehnt alle Token ab, erlaubt aber den Anwendungsstart.
|
||||
*/
|
||||
private fun createNoOpJwtDecoder(): ReactiveJwtDecoder {
|
||||
return ReactiveJwtDecoder { token ->
|
||||
throw IllegalStateException("JWT validation is not available - Keycloak may not be running")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Definiert die zentrale und einzige CORS-Konfiguration für das Gateway.
|
||||
*/
|
||||
@Bean
|
||||
fun corsConfigurationSource(): CorsConfigurationSource {
|
||||
val configuration = CorsConfiguration().apply {
|
||||
allowedOriginPatterns = securityProperties.cors.allowedOriginPatterns.toList()
|
||||
allowedMethods = securityProperties.cors.allowedMethods.toList()
|
||||
allowedHeaders = securityProperties.cors.allowedHeaders.toList()
|
||||
exposedHeaders = securityProperties.cors.exposedHeaders.toList()
|
||||
allowCredentials = securityProperties.cors.allowCredentials
|
||||
maxAge = securityProperties.cors.maxAge.seconds
|
||||
}
|
||||
|
||||
return UrlBasedCorsConfigurationSource().apply {
|
||||
registerCorsConfiguration("/**", configuration)
|
||||
}
|
||||
return UrlBasedCorsConfigurationSource().apply {
|
||||
registerCorsConfiguration("/**", configuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,25 +118,26 @@ class SecurityConfig(
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "gateway.security")
|
||||
data class GatewaySecurityProperties(
|
||||
val cors: CorsProperties = CorsProperties(),
|
||||
val publicPaths: List<String> = listOf(
|
||||
"/",
|
||||
"/fallback/**",
|
||||
"/actuator/**",
|
||||
"/webjars/**",
|
||||
"/v3/api-docs/**",
|
||||
"/api/auth/**" // Alle Auth-Endpunkte
|
||||
)
|
||||
val cors: CorsProperties = CorsProperties(),
|
||||
val publicPaths: List<String> = listOf(
|
||||
"/",
|
||||
"/fallback/**",
|
||||
"/actuator/**",
|
||||
"/webjars/**",
|
||||
"/v3/api-docs/**",
|
||||
"/api/auth/**", // Alle Auth-Endpunkte
|
||||
"/api/ping/**"
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* DTO für CORS-Properties mit sinnvollen Standardwerten.
|
||||
*/
|
||||
data class CorsProperties(
|
||||
val allowedOriginPatterns: Set<String> = setOf("http://localhost:[*]", "https://*.meldestelle.at"),
|
||||
val allowedMethods: Set<String> = setOf("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"),
|
||||
val allowedHeaders: Set<String> = setOf("*"),
|
||||
val exposedHeaders: Set<String> = setOf("X-Correlation-ID", "X-RateLimit-Limit", "X-RateLimit-Remaining"),
|
||||
val allowCredentials: Boolean = true,
|
||||
val maxAge: Duration = Duration.ofHours(1)
|
||||
val allowedOriginPatterns: Set<String> = setOf("http://localhost:[*]", "https://*.meldestelle.at"),
|
||||
val allowedMethods: Set<String> = setOf("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"),
|
||||
val allowedHeaders: Set<String> = setOf("*"),
|
||||
val exposedHeaders: Set<String> = setOf("X-Correlation-ID", "X-RateLimit-Limit", "X-RateLimit-Remaining"),
|
||||
val allowCredentials: Boolean = true,
|
||||
val maxAge: Duration = Duration.ofHours(1)
|
||||
)
|
||||
|
||||
@@ -73,6 +73,10 @@ spring:
|
||||
name: Cache-Control
|
||||
value: no-cache, no-store, must-revalidate
|
||||
routes:
|
||||
|
||||
# ==============================================================
|
||||
# --- Gateway-Info-Route (optional) ---
|
||||
# ==============================================================
|
||||
- id: gateway-info-route
|
||||
uri: http://localhost:${server.port}
|
||||
predicates:
|
||||
@@ -81,56 +85,80 @@ spring:
|
||||
filters:
|
||||
- SetStatus=200
|
||||
- SetResponseHeader=Content-Type,application/json
|
||||
- id: members-service-route
|
||||
uri: lb://members-service
|
||||
predicates:
|
||||
- Path=/api/members/**
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
- name: CircuitBreaker
|
||||
args:
|
||||
name: membersCircuitBreaker
|
||||
fallbackUri: forward:/fallback/members
|
||||
- id: horses-service-route
|
||||
uri: lb://horses-service
|
||||
predicates:
|
||||
- Path=/api/horses/**
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
- name: CircuitBreaker
|
||||
args:
|
||||
name: horsesCircuitBreaker
|
||||
fallbackUri: forward:/fallback/horses
|
||||
- id: events-service-route
|
||||
uri: lb://events-service
|
||||
predicates:
|
||||
- Path=/api/events/**
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
- name: CircuitBreaker
|
||||
args:
|
||||
name: eventsCircuitBreaker
|
||||
fallbackUri: forward:/fallback/events
|
||||
- id: masterdata-service-route
|
||||
uri: lb://masterdata-service
|
||||
predicates:
|
||||
- Path=/api/masterdata/**
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
- name: CircuitBreaker
|
||||
args:
|
||||
name: masterdataCircuitBreaker
|
||||
fallbackUri: forward:/fallback/masterdata
|
||||
- id: auth-service-route
|
||||
uri: lb://auth-service
|
||||
predicates:
|
||||
- Path=/api/auth/**
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
- name: CircuitBreaker
|
||||
args:
|
||||
name: authCircuitBreaker
|
||||
fallbackUri: forward:/fallback/auth
|
||||
|
||||
# ==============================================================
|
||||
# --- Members-Service-Integration (optional) ---
|
||||
# ==============================================================
|
||||
# - id: members-service-route
|
||||
# uri: lb://members-service
|
||||
# predicates:
|
||||
# - Path=/api/members/**
|
||||
# filters:
|
||||
# - StripPrefix=1
|
||||
# - name: CircuitBreaker
|
||||
# args:
|
||||
# name: membersCircuitBreaker
|
||||
# fallbackUri: forward:/fallback/members
|
||||
|
||||
# ==============================================================
|
||||
# --- Horses-Service-Integration (optional) ---
|
||||
# ==============================================================
|
||||
# - id: horses-service-route
|
||||
# uri: lb://horses-service
|
||||
# predicates:
|
||||
# - Path=/api/horses/**
|
||||
# filters:
|
||||
# - StripPrefix=1
|
||||
# - name: CircuitBreaker
|
||||
# args:
|
||||
# name: horsesCircuitBreaker
|
||||
# fallbackUri: forward:/fallback/horses
|
||||
|
||||
# ==============================================================
|
||||
# --- Events-Service-Integration (optional) ---
|
||||
# ==============================================================
|
||||
# - id: events-service-route
|
||||
# uri: lb://events-service
|
||||
# predicates:
|
||||
# - Path=/api/events/**
|
||||
# filters:
|
||||
# - StripPrefix=1
|
||||
# - name: CircuitBreaker
|
||||
# args:
|
||||
# name: eventsCircuitBreaker
|
||||
# fallbackUri: forward:/fallback/events
|
||||
|
||||
# ==============================================================
|
||||
# --- Masterdata-Service-Integration (optional) ---
|
||||
# ==============================================================
|
||||
# - id: masterdata-service-route
|
||||
# uri: lb://masterdata-service
|
||||
# predicates:
|
||||
# - Path=/api/masterdata/**
|
||||
# filters:
|
||||
# - StripPrefix=1
|
||||
# - name: CircuitBreaker
|
||||
# args:
|
||||
# name: masterdataCircuitBreaker
|
||||
# fallbackUri: forward:/fallback/masterdata
|
||||
|
||||
# ==============================================================
|
||||
# --- Auth-Service-Integration (optional) ---
|
||||
# ==============================================================
|
||||
# - id: auth-service-route
|
||||
# uri: lb://auth-service
|
||||
# predicates:
|
||||
# - Path=/api/auth/**
|
||||
# filters:
|
||||
# - StripPrefix=1
|
||||
# - name: CircuitBreaker
|
||||
# args:
|
||||
# name: authCircuitBreaker
|
||||
# fallbackUri: forward:/fallback/auth
|
||||
|
||||
# ==============================================================
|
||||
# --- Ping-Service-Integration (optional) ---
|
||||
# ==============================================================
|
||||
- id: ping-service-route
|
||||
uri: lb://ping-service
|
||||
predicates:
|
||||
|
||||
@@ -1,54 +1,60 @@
|
||||
// Optimized Spring Boot ping service for testing microservice architecture
|
||||
// This service demonstrates circuit breaker patterns, service discovery, and monitoring
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinJvm)
|
||||
alias(libs.plugins.kotlinSpring)
|
||||
alias(libs.plugins.kotlinJpa)
|
||||
alias(libs.plugins.spring.boot)
|
||||
alias(libs.plugins.spring.dependencyManagement)
|
||||
alias(libs.plugins.kotlinJvm)
|
||||
alias(libs.plugins.kotlinSpring)
|
||||
alias(libs.plugins.kotlinJpa)
|
||||
alias(libs.plugins.spring.boot)
|
||||
alias(libs.plugins.spring.dependencyManagement)
|
||||
}
|
||||
|
||||
// Configure the main class for the executable JAR
|
||||
springBoot {
|
||||
mainClass.set("at.mocode.ping.service.PingServiceApplicationKt")
|
||||
mainClass.set("at.mocode.ping.service.PingServiceApplicationKt")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Platform BOM für zentrale Versionsverwaltung
|
||||
implementation(platform(projects.platform.platformBom))
|
||||
// Platform BOM für zentrale Versionsverwaltung
|
||||
implementation(platform(projects.platform.platformBom))
|
||||
|
||||
// Platform und Core Dependencies
|
||||
implementation(projects.platform.platformDependencies)
|
||||
implementation(projects.services.ping.pingApi)
|
||||
implementation(projects.infrastructure.monitoring.monitoringClient)
|
||||
// Platform und Core Dependencies
|
||||
implementation(projects.platform.platformDependencies)
|
||||
implementation(projects.services.ping.pingApi)
|
||||
implementation(projects.infrastructure.monitoring.monitoringClient)
|
||||
|
||||
// Spring Boot Service Complete Bundle
|
||||
// Provides: web, validation, actuator, security, oauth2-client, oauth2-resource-server,
|
||||
// data-jpa, data-redis, micrometer-prometheus, tracing, zipkin
|
||||
implementation(libs.bundles.spring.boot.service.complete)
|
||||
// Spring Boot Service Complete Bundle
|
||||
// Provides: web, validation, actuator, security, oauth2-client, oauth2-resource-server,
|
||||
// data-jpa, data-redis, micrometer-prometheus, tracing, zipkin
|
||||
implementation(libs.bundles.spring.boot.service.complete)
|
||||
|
||||
// Jackson Kotlin Support Bundle
|
||||
implementation(libs.bundles.jackson.kotlin)
|
||||
// Datenbank (PostgresQL) Driver
|
||||
implementation(libs.postgresql.driver)
|
||||
|
||||
// Kotlin Reflection (now from version catalog)
|
||||
implementation(libs.kotlin.reflect)
|
||||
// Web-Server (Tomcat) explizit hinzufügen!
|
||||
implementation(libs.spring.boot.starter.web)
|
||||
|
||||
// Service Discovery
|
||||
implementation(libs.spring.cloud.starter.consul.discovery)
|
||||
// Jackson Kotlin Support Bundle
|
||||
implementation(libs.bundles.jackson.kotlin)
|
||||
|
||||
// Caching (Caffeine for Spring Cloud LoadBalancer)
|
||||
implementation(libs.caffeine)
|
||||
implementation(libs.spring.web) // Provides spring-context-support
|
||||
// Kotlin Reflection (now from version catalog)
|
||||
implementation(libs.kotlin.reflect)
|
||||
|
||||
// Resilience4j Bundle (Circuit Breaker, Reactor, AOP)
|
||||
implementation(libs.bundles.resilience)
|
||||
// Service Discovery
|
||||
implementation(libs.spring.cloud.starter.consul.discovery)
|
||||
|
||||
// OpenAPI Documentation
|
||||
implementation(libs.springdoc.openapi.starter.webmvc.ui)
|
||||
// Caching (Caffeine for Spring Cloud LoadBalancer)
|
||||
implementation(libs.caffeine)
|
||||
implementation(libs.spring.web) // Provides spring-context-support
|
||||
|
||||
// Test Dependencies
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
testImplementation(libs.bundles.testing.jvm)
|
||||
testImplementation(libs.spring.boot.starter.test)
|
||||
testImplementation(libs.spring.boot.starter.web)
|
||||
// Resilience4j Bundle (Circuit Breaker, Reactor, AOP)
|
||||
implementation(libs.bundles.resilience)
|
||||
|
||||
// OpenAPI Documentation
|
||||
implementation(libs.springdoc.openapi.starter.webmvc.ui)
|
||||
|
||||
// Test Dependencies
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
testImplementation(libs.bundles.testing.jvm)
|
||||
testImplementation(libs.spring.boot.starter.test)
|
||||
testImplementation(libs.spring.boot.starter.web)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user