refactoring: Docker-Dateien Api-Gateway
This commit is contained in:
@@ -34,14 +34,14 @@ PGADMIN_EMAIL=user@domain.com
|
|||||||
PGADMIN_PASSWORD=strong-password
|
PGADMIN_PASSWORD=strong-password
|
||||||
PGADMIN_PORT=8888:80
|
PGADMIN_PORT=8888:80
|
||||||
|
|
||||||
|
# --- PROMETHEUS (Metriken) ---
|
||||||
|
PROMETHEUS_PORT=9090:9090
|
||||||
|
|
||||||
# --- GRAFANA (Monitoring GUI) ---
|
# --- GRAFANA (Monitoring GUI) ---
|
||||||
GF_ADMIN_USER=gf-admin
|
GF_ADMIN_USER=gf-admin
|
||||||
GF_ADMIN_PASSWORD=gf-password
|
GF_ADMIN_PASSWORD=gf-password
|
||||||
GF_PORT=3000:3000
|
GF_PORT=3000:3000
|
||||||
|
|
||||||
# --- PROMETHEUS (Metriken) ---
|
|
||||||
PROMETHEUS_PORT=9090:9090
|
|
||||||
|
|
||||||
# --- SERVICE DISCOVERY (Consul) ---
|
# --- SERVICE DISCOVERY (Consul) ---
|
||||||
CONSUL_PORT=8500:8500
|
CONSUL_PORT=8500:8500
|
||||||
|
|
||||||
@@ -50,3 +50,6 @@ CONSUL_PORT=8500:8500
|
|||||||
GATEWAY_PORT=8081
|
GATEWAY_PORT=8081
|
||||||
# Debug Port für IntelliJ (Remote JVM Debug)
|
# Debug Port für IntelliJ (Remote JVM Debug)
|
||||||
GATEWAY_DEBUG_PORT=5005
|
GATEWAY_DEBUG_PORT=5005
|
||||||
|
|
||||||
|
# --- MICROSERVICES ---
|
||||||
|
PING_SERVICE_PORT=8082:8082
|
||||||
|
|||||||
@@ -202,6 +202,48 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- meldestelle-network
|
- 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:
|
volumes:
|
||||||
postgres-data:
|
postgres-data:
|
||||||
pgadmin-data:
|
pgadmin-data:
|
||||||
|
|||||||
@@ -1,73 +1,76 @@
|
|||||||
// 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 {
|
||||||
alias(libs.plugins.kotlinJvm)
|
alias(libs.plugins.kotlinJvm)
|
||||||
alias(libs.plugins.kotlinSpring)
|
alias(libs.plugins.kotlinSpring)
|
||||||
alias(libs.plugins.kotlinJpa)
|
alias(libs.plugins.kotlinJpa)
|
||||||
alias(libs.plugins.spring.boot)
|
alias(libs.plugins.spring.boot)
|
||||||
alias(libs.plugins.spring.dependencyManagement)
|
alias(libs.plugins.spring.dependencyManagement)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.gateway.GatewayApplicationKt")
|
mainClass.set("at.mocode.infrastructure.gateway.GatewayApplicationKt")
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(platform(projects.platform.platformBom))
|
implementation(platform(projects.platform.platformBom))
|
||||||
|
|
||||||
// === Core Dependencies ===
|
// === Core Dependencies ===
|
||||||
implementation(projects.core.coreUtils)
|
implementation(projects.core.coreUtils)
|
||||||
implementation(projects.platform.platformDependencies)
|
implementation(projects.platform.platformDependencies)
|
||||||
implementation(projects.infrastructure.monitoring.monitoringClient)
|
implementation(projects.infrastructure.monitoring.monitoringClient)
|
||||||
|
|
||||||
// === GATEWAY-SPEZIFISCHE ABHÄNGIGKEITEN ===
|
// === GATEWAY-SPEZIFISCHE ABHÄNGIGKEITEN ===
|
||||||
implementation(libs.bundles.spring.cloud.gateway)
|
implementation(libs.bundles.spring.cloud.gateway)
|
||||||
implementation(libs.bundles.spring.boot.security)
|
implementation(libs.bundles.spring.boot.security)
|
||||||
implementation(libs.bundles.resilience)
|
implementation(libs.bundles.resilience)
|
||||||
implementation("org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j")
|
implementation("org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j")
|
||||||
implementation(libs.spring.boot.starter.actuator) // Wichtig für Health & Metrics
|
implementation(libs.spring.boot.starter.actuator) // Wichtig für Health & Metrics
|
||||||
implementation(libs.bundles.logging)
|
implementation(libs.bundles.logging)
|
||||||
implementation(libs.bundles.jackson.kotlin)
|
implementation(libs.bundles.jackson.kotlin)
|
||||||
|
|
||||||
// === Test Dependencies ===
|
// WICHTIG: PostgreSQL Treiber hinzufügen!
|
||||||
testImplementation(projects.platform.platformTesting)
|
implementation(libs.postgresql.driver)
|
||||||
testImplementation(libs.bundles.testing.jvm)
|
|
||||||
|
// === Test Dependencies ===
|
||||||
|
testImplementation(projects.platform.platformTesting)
|
||||||
|
testImplementation(libs.bundles.testing.jvm)
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.test {
|
tasks.test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Konfiguration für Integration Tests
|
// Konfiguration für Integration Tests
|
||||||
sourceSets {
|
sourceSets {
|
||||||
val integrationTest by creating {
|
val integrationTest by creating {
|
||||||
compileClasspath += sourceSets.main.get().output
|
compileClasspath += sourceSets.main.get().output
|
||||||
runtimeClasspath += sourceSets.main.get().output
|
runtimeClasspath += sourceSets.main.get().output
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val integrationTestImplementation by configurations.getting {
|
val integrationTestImplementation by configurations.getting {
|
||||||
extendsFrom(configurations.testImplementation.get())
|
extendsFrom(configurations.testImplementation.get())
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register<Test>("integrationTest") {
|
tasks.register<Test>("integrationTest") {
|
||||||
description = "Führt die Integration Tests aus"
|
description = "Führt die Integration Tests aus"
|
||||||
group = "verification"
|
group = "verification"
|
||||||
|
|
||||||
testClassesDirs = sourceSets["integrationTest"].output.classesDirs
|
testClassesDirs = sourceSets["integrationTest"].output.classesDirs
|
||||||
classpath = sourceSets["integrationTest"].runtimeClasspath
|
classpath = sourceSets["integrationTest"].runtimeClasspath
|
||||||
|
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
|
|
||||||
shouldRunAfter("test")
|
shouldRunAfter("test")
|
||||||
|
|
||||||
testLogging {
|
testLogging {
|
||||||
events("passed", "skipped", "failed")
|
events("passed", "skipped", "failed")
|
||||||
showStandardStreams = false
|
showStandardStreams = false
|
||||||
showExceptions = true
|
showExceptions = true
|
||||||
showCauses = true
|
showCauses = true
|
||||||
showStackTraces = true
|
showStackTraces = true
|
||||||
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
|
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+109
-109
@@ -17,126 +17,126 @@ import java.time.Duration
|
|||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
class GatewayHealthIndicator(
|
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 {
|
) : HealthIndicator {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val CRITICAL_SERVICES = setOf(
|
private val CRITICAL_SERVICES = setOf(
|
||||||
"members-service",
|
"ping-service"
|
||||||
"horses-service",
|
)
|
||||||
"events-service",
|
|
||||||
"masterdata-service",
|
private val OPTIONAL_SERVICES = setOf(
|
||||||
"auth-service"
|
"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(
|
details["discoveredServices"] = discoveredServices
|
||||||
"ping-service"
|
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 {
|
CRITICAL_SERVICES.forEach { serviceName ->
|
||||||
val builder = Health.up()
|
val status = checkServiceHealth(serviceName)
|
||||||
val details = mutableMapOf<String, Any>()
|
criticalServiceStatus[serviceName] = status
|
||||||
|
if (status != "UP") {
|
||||||
try {
|
hasCriticalFailure = true
|
||||||
// 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}"
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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 {
|
details["criticalServices"] = criticalServiceStatus
|
||||||
return try {
|
details["optionalServices"] = optionalServiceStatus
|
||||||
val instances = discoveryClient.getInstances(serviceName)
|
|
||||||
|
|
||||||
if (instances.isEmpty()) {
|
// Gateway Status basierend auf kritischen Services
|
||||||
"NO_INSTANCES"
|
val isTestEnvironment = environment.activeProfiles.contains("test")
|
||||||
} else {
|
val isDevEnvironment = environment.activeProfiles.contains("dev")
|
||||||
// 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()
|
if (hasCriticalFailure && !isTestEnvironment && !isDevEnvironment) {
|
||||||
val response = client.get()
|
builder.down()
|
||||||
.uri(healthUrl)
|
details["status"] = "DOWN"
|
||||||
.retrieve()
|
details["reason"] = "Ein oder mehrere kritische Services sind nicht verfügbar"
|
||||||
.bodyToMono(Map::class.java)
|
} else {
|
||||||
.timeout(HEALTH_CHECK_TIMEOUT)
|
details["status"] = "UP"
|
||||||
.onErrorReturn(mapOf("status" to "DOWN"))
|
details["reason"] = when {
|
||||||
.block()
|
isTestEnvironment -> "Gesundheitsprüfung erfolgreich (Testumgebung)"
|
||||||
|
isDevEnvironment -> "Gesundheitsprüfung erfolgreich (Entwicklungsumgebung - nicht alle Services erforderlich)"
|
||||||
val status = response?.get("status")?.toString() ?: "UNKNOWN"
|
else -> "Alle kritischen Services sind verfügbar"
|
||||||
if (status == "UP") "UP" else "DOWN"
|
|
||||||
}
|
|
||||||
} catch (exception: WebClientResponseException) {
|
|
||||||
when (exception.statusCode.value()) {
|
|
||||||
404 -> "NO_HEALTH_ENDPOINT"
|
|
||||||
503 -> "DOWN"
|
|
||||||
else -> "ERROR"
|
|
||||||
}
|
|
||||||
} catch (_: Exception) {
|
|
||||||
"ERROR"
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} 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
|
@EnableWebFluxSecurity
|
||||||
@EnableConfigurationProperties(GatewaySecurityProperties::class)
|
@EnableConfigurationProperties(GatewaySecurityProperties::class)
|
||||||
class SecurityConfig(
|
class SecurityConfig(
|
||||||
private val securityProperties: GatewaySecurityProperties
|
private val securityProperties: GatewaySecurityProperties
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Konfiguriert die zentrale Security-Filter-Kette für das Gateway.
|
* Konfiguriert die zentrale Security-Filter-Kette für das Gateway.
|
||||||
*
|
*
|
||||||
* Diese Konfiguration nutzt den Standard-OAuth2-Resource-Server von Spring Security,
|
* Diese Konfiguration nutzt den Standard-OAuth2-Resource-Server von Spring Security,
|
||||||
* um JWTs (z.B. von Keycloak) automatisch zu validieren.
|
* um JWTs (z.B. von Keycloak) automatisch zu validieren.
|
||||||
*/
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||||
return http { // Start der modernen Kotlin-DSL
|
return http { // Start der modernen Kotlin-DSL
|
||||||
// 1. CORS-Konfiguration anwenden
|
// 1. CORS-Konfiguration anwenden
|
||||||
cors { }
|
cors { }
|
||||||
|
|
||||||
// 2. CSRF deaktivieren (für zustandslose APIs)
|
// 2. CSRF deaktivieren (für zustandslose APIs)
|
||||||
csrf { disable() }
|
csrf { disable() }
|
||||||
|
|
||||||
// 3. Routen-Berechtigungen definieren
|
// 3. Routen-Berechtigungen definieren
|
||||||
authorizeExchange {
|
authorizeExchange {
|
||||||
// Öffentlich zugängliche Pfade aus der .yml-Datei laden
|
// Öffentlich zugängliche Pfade aus der .yml-Datei laden
|
||||||
authorize(
|
authorize(
|
||||||
pathMatchers(*securityProperties.publicPaths.toTypedArray()),
|
pathMatchers(*securityProperties.publicPaths.toTypedArray()),
|
||||||
permitAll
|
permitAll
|
||||||
)
|
)
|
||||||
// Alle anderen Pfade erfordern eine Authentifizierung
|
// Alle anderen Pfade erfordern eine Authentifizierung
|
||||||
authorize(anyExchange, authenticated)
|
authorize(anyExchange, authenticated)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. JWT-Validierung via Keycloak aktivieren
|
// 4. JWT-Validierung via Keycloak aktivieren
|
||||||
oauth2ResourceServer {
|
oauth2ResourceServer {
|
||||||
jwt { }
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
return UrlBasedCorsConfigurationSource().apply {
|
||||||
* Erstellt einen ReactiveJwtDecoder für die JWT-Validierung.
|
registerCorsConfiguration("/**", configuration)
|
||||||
*
|
|
||||||
* 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -118,25 +118,26 @@ class SecurityConfig(
|
|||||||
*/
|
*/
|
||||||
@ConfigurationProperties(prefix = "gateway.security")
|
@ConfigurationProperties(prefix = "gateway.security")
|
||||||
data class GatewaySecurityProperties(
|
data class GatewaySecurityProperties(
|
||||||
val cors: CorsProperties = CorsProperties(),
|
val cors: CorsProperties = CorsProperties(),
|
||||||
val publicPaths: List<String> = listOf(
|
val publicPaths: List<String> = listOf(
|
||||||
"/",
|
"/",
|
||||||
"/fallback/**",
|
"/fallback/**",
|
||||||
"/actuator/**",
|
"/actuator/**",
|
||||||
"/webjars/**",
|
"/webjars/**",
|
||||||
"/v3/api-docs/**",
|
"/v3/api-docs/**",
|
||||||
"/api/auth/**" // Alle Auth-Endpunkte
|
"/api/auth/**", // Alle Auth-Endpunkte
|
||||||
)
|
"/api/ping/**"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DTO für CORS-Properties mit sinnvollen Standardwerten.
|
* DTO für CORS-Properties mit sinnvollen Standardwerten.
|
||||||
*/
|
*/
|
||||||
data class CorsProperties(
|
data class CorsProperties(
|
||||||
val allowedOriginPatterns: Set<String> = setOf("http://localhost:[*]", "https://*.meldestelle.at"),
|
val allowedOriginPatterns: Set<String> = setOf("http://localhost:[*]", "https://*.meldestelle.at"),
|
||||||
val allowedMethods: Set<String> = setOf("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"),
|
val allowedMethods: Set<String> = setOf("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"),
|
||||||
val allowedHeaders: Set<String> = setOf("*"),
|
val allowedHeaders: Set<String> = setOf("*"),
|
||||||
val exposedHeaders: Set<String> = setOf("X-Correlation-ID", "X-RateLimit-Limit", "X-RateLimit-Remaining"),
|
val exposedHeaders: Set<String> = setOf("X-Correlation-ID", "X-RateLimit-Limit", "X-RateLimit-Remaining"),
|
||||||
val allowCredentials: Boolean = true,
|
val allowCredentials: Boolean = true,
|
||||||
val maxAge: Duration = Duration.ofHours(1)
|
val maxAge: Duration = Duration.ofHours(1)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -73,6 +73,10 @@ spring:
|
|||||||
name: Cache-Control
|
name: Cache-Control
|
||||||
value: no-cache, no-store, must-revalidate
|
value: no-cache, no-store, must-revalidate
|
||||||
routes:
|
routes:
|
||||||
|
|
||||||
|
# ==============================================================
|
||||||
|
# --- Gateway-Info-Route (optional) ---
|
||||||
|
# ==============================================================
|
||||||
- id: gateway-info-route
|
- id: gateway-info-route
|
||||||
uri: http://localhost:${server.port}
|
uri: http://localhost:${server.port}
|
||||||
predicates:
|
predicates:
|
||||||
@@ -81,56 +85,80 @@ spring:
|
|||||||
filters:
|
filters:
|
||||||
- SetStatus=200
|
- SetStatus=200
|
||||||
- SetResponseHeader=Content-Type,application/json
|
- SetResponseHeader=Content-Type,application/json
|
||||||
- id: members-service-route
|
|
||||||
uri: lb://members-service
|
# ==============================================================
|
||||||
predicates:
|
# --- Members-Service-Integration (optional) ---
|
||||||
- Path=/api/members/**
|
# ==============================================================
|
||||||
filters:
|
# - id: members-service-route
|
||||||
- StripPrefix=1
|
# uri: lb://members-service
|
||||||
- name: CircuitBreaker
|
# predicates:
|
||||||
args:
|
# - Path=/api/members/**
|
||||||
name: membersCircuitBreaker
|
# filters:
|
||||||
fallbackUri: forward:/fallback/members
|
# - StripPrefix=1
|
||||||
- id: horses-service-route
|
# - name: CircuitBreaker
|
||||||
uri: lb://horses-service
|
# args:
|
||||||
predicates:
|
# name: membersCircuitBreaker
|
||||||
- Path=/api/horses/**
|
# fallbackUri: forward:/fallback/members
|
||||||
filters:
|
|
||||||
- StripPrefix=1
|
# ==============================================================
|
||||||
- name: CircuitBreaker
|
# --- Horses-Service-Integration (optional) ---
|
||||||
args:
|
# ==============================================================
|
||||||
name: horsesCircuitBreaker
|
# - id: horses-service-route
|
||||||
fallbackUri: forward:/fallback/horses
|
# uri: lb://horses-service
|
||||||
- id: events-service-route
|
# predicates:
|
||||||
uri: lb://events-service
|
# - Path=/api/horses/**
|
||||||
predicates:
|
# filters:
|
||||||
- Path=/api/events/**
|
# - StripPrefix=1
|
||||||
filters:
|
# - name: CircuitBreaker
|
||||||
- StripPrefix=1
|
# args:
|
||||||
- name: CircuitBreaker
|
# name: horsesCircuitBreaker
|
||||||
args:
|
# fallbackUri: forward:/fallback/horses
|
||||||
name: eventsCircuitBreaker
|
|
||||||
fallbackUri: forward:/fallback/events
|
# ==============================================================
|
||||||
- id: masterdata-service-route
|
# --- Events-Service-Integration (optional) ---
|
||||||
uri: lb://masterdata-service
|
# ==============================================================
|
||||||
predicates:
|
# - id: events-service-route
|
||||||
- Path=/api/masterdata/**
|
# uri: lb://events-service
|
||||||
filters:
|
# predicates:
|
||||||
- StripPrefix=1
|
# - Path=/api/events/**
|
||||||
- name: CircuitBreaker
|
# filters:
|
||||||
args:
|
# - StripPrefix=1
|
||||||
name: masterdataCircuitBreaker
|
# - name: CircuitBreaker
|
||||||
fallbackUri: forward:/fallback/masterdata
|
# args:
|
||||||
- id: auth-service-route
|
# name: eventsCircuitBreaker
|
||||||
uri: lb://auth-service
|
# fallbackUri: forward:/fallback/events
|
||||||
predicates:
|
|
||||||
- Path=/api/auth/**
|
# ==============================================================
|
||||||
filters:
|
# --- Masterdata-Service-Integration (optional) ---
|
||||||
- StripPrefix=1
|
# ==============================================================
|
||||||
- name: CircuitBreaker
|
# - id: masterdata-service-route
|
||||||
args:
|
# uri: lb://masterdata-service
|
||||||
name: authCircuitBreaker
|
# predicates:
|
||||||
fallbackUri: forward:/fallback/auth
|
# - 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
|
- id: ping-service-route
|
||||||
uri: lb://ping-service
|
uri: lb://ping-service
|
||||||
predicates:
|
predicates:
|
||||||
|
|||||||
@@ -1,54 +1,60 @@
|
|||||||
// Optimized Spring Boot ping service for testing microservice architecture
|
// Optimized Spring Boot ping service for testing microservice architecture
|
||||||
// This service demonstrates circuit breaker patterns, service discovery, and monitoring
|
// This service demonstrates circuit breaker patterns, service discovery, and monitoring
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlinJvm)
|
alias(libs.plugins.kotlinJvm)
|
||||||
alias(libs.plugins.kotlinSpring)
|
alias(libs.plugins.kotlinSpring)
|
||||||
alias(libs.plugins.kotlinJpa)
|
alias(libs.plugins.kotlinJpa)
|
||||||
alias(libs.plugins.spring.boot)
|
alias(libs.plugins.spring.boot)
|
||||||
alias(libs.plugins.spring.dependencyManagement)
|
alias(libs.plugins.spring.dependencyManagement)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure the main class for the executable JAR
|
// Configure the main class for the executable JAR
|
||||||
springBoot {
|
springBoot {
|
||||||
mainClass.set("at.mocode.ping.service.PingServiceApplicationKt")
|
mainClass.set("at.mocode.ping.service.PingServiceApplicationKt")
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Platform BOM für zentrale Versionsverwaltung
|
// Platform BOM für zentrale Versionsverwaltung
|
||||||
implementation(platform(projects.platform.platformBom))
|
implementation(platform(projects.platform.platformBom))
|
||||||
|
|
||||||
// Platform und Core Dependencies
|
// Platform und Core Dependencies
|
||||||
implementation(projects.platform.platformDependencies)
|
implementation(projects.platform.platformDependencies)
|
||||||
implementation(projects.services.ping.pingApi)
|
implementation(projects.services.ping.pingApi)
|
||||||
implementation(projects.infrastructure.monitoring.monitoringClient)
|
implementation(projects.infrastructure.monitoring.monitoringClient)
|
||||||
|
|
||||||
// Spring Boot Service Complete Bundle
|
// Spring Boot Service Complete Bundle
|
||||||
// Provides: web, validation, actuator, security, oauth2-client, oauth2-resource-server,
|
// Provides: web, validation, actuator, security, oauth2-client, oauth2-resource-server,
|
||||||
// data-jpa, data-redis, micrometer-prometheus, tracing, zipkin
|
// data-jpa, data-redis, micrometer-prometheus, tracing, zipkin
|
||||||
implementation(libs.bundles.spring.boot.service.complete)
|
implementation(libs.bundles.spring.boot.service.complete)
|
||||||
|
|
||||||
// Jackson Kotlin Support Bundle
|
// Datenbank (PostgresQL) Driver
|
||||||
implementation(libs.bundles.jackson.kotlin)
|
implementation(libs.postgresql.driver)
|
||||||
|
|
||||||
// Kotlin Reflection (now from version catalog)
|
// Web-Server (Tomcat) explizit hinzufügen!
|
||||||
implementation(libs.kotlin.reflect)
|
implementation(libs.spring.boot.starter.web)
|
||||||
|
|
||||||
// Service Discovery
|
// Jackson Kotlin Support Bundle
|
||||||
implementation(libs.spring.cloud.starter.consul.discovery)
|
implementation(libs.bundles.jackson.kotlin)
|
||||||
|
|
||||||
// Caching (Caffeine for Spring Cloud LoadBalancer)
|
// Kotlin Reflection (now from version catalog)
|
||||||
implementation(libs.caffeine)
|
implementation(libs.kotlin.reflect)
|
||||||
implementation(libs.spring.web) // Provides spring-context-support
|
|
||||||
|
|
||||||
// Resilience4j Bundle (Circuit Breaker, Reactor, AOP)
|
// Service Discovery
|
||||||
implementation(libs.bundles.resilience)
|
implementation(libs.spring.cloud.starter.consul.discovery)
|
||||||
|
|
||||||
// OpenAPI Documentation
|
// Caching (Caffeine for Spring Cloud LoadBalancer)
|
||||||
implementation(libs.springdoc.openapi.starter.webmvc.ui)
|
implementation(libs.caffeine)
|
||||||
|
implementation(libs.spring.web) // Provides spring-context-support
|
||||||
|
|
||||||
// Test Dependencies
|
// Resilience4j Bundle (Circuit Breaker, Reactor, AOP)
|
||||||
testImplementation(projects.platform.platformTesting)
|
implementation(libs.bundles.resilience)
|
||||||
testImplementation(libs.bundles.testing.jvm)
|
|
||||||
testImplementation(libs.spring.boot.starter.test)
|
// OpenAPI Documentation
|
||||||
testImplementation(libs.spring.boot.starter.web)
|
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