This commit is contained in:
2025-12-31 00:20:29 +01:00
parent 9283f26df1
commit e38b693847
179 changed files with 3061 additions and 1440 deletions
@@ -0,0 +1,11 @@
package org.springframework.boot.data.redis.autoconfigure;
import org.springframework.context.annotation.Configuration;
/**
* Dummy class to satisfy Spring Cloud Gateway 2025.1.0 imports which expect this class
* to be present at this location, even though Spring Boot 3.5.9 moved it.
*/
@Configuration
public class DataRedisReactiveAutoConfiguration {
}
@@ -0,0 +1,11 @@
package org.springframework.boot.webflux.autoconfigure;
import org.springframework.context.annotation.Configuration;
/**
* Dummy class to satisfy Spring Cloud Gateway 2025.1.0 imports which expect this class
* to be present at this location, even though Spring Boot 3.5.9 moved it.
*/
@Configuration
public class HttpHandlerAutoConfiguration {
}
@@ -1,11 +1,24 @@
package at.mocode.infrastructure.gateway
import org.slf4j.LoggerFactory
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.core.env.Environment
@SpringBootApplication
class GatewayApplication
fun main(args: Array<String>) {
runApplication<GatewayApplication>(*args)
val context = runApplication<GatewayApplication>(*args)
val logger = LoggerFactory.getLogger(GatewayApplication::class.java)
val env = context.getBean(Environment::class.java)
val port = env.getProperty("server.port") ?: "8081"
logger.info("""
----------------------------------------------------------
Application 'Gateway' is running!
Port: $port
Profiles: ${env.activeProfiles.joinToString(", ").ifEmpty { "default" }}
----------------------------------------------------------
""".trimIndent())
}
@@ -1,5 +1,6 @@
package at.mocode.infrastructure.gateway.config
import org.slf4j.LoggerFactory
import org.springframework.cloud.gateway.filter.GatewayFilterChain
import org.springframework.cloud.gateway.filter.GlobalFilter
import org.springframework.core.Ordered
@@ -18,6 +19,8 @@ import java.util.*
@Component
class CorrelationIdFilter : GlobalFilter, Ordered {
private val logger = LoggerFactory.getLogger(CorrelationIdFilter::class.java)
companion object {
const val CORRELATION_ID_HEADER = "X-Correlation-ID"
}
@@ -39,6 +42,9 @@ class CorrelationIdFilter : GlobalFilter, Ordered {
mutatedExchange.response.headers.add(CORRELATION_ID_HEADER, correlationId)
return chain.filter(mutatedExchange)
.doOnError { ex ->
logger.error("Error in CorrelationIdFilter for request {}: {}", request.uri, ex.message)
}
}
override fun getOrder(): Int = Ordered.HIGHEST_PRECEDENCE
@@ -1,6 +1,7 @@
package at.mocode.infrastructure.gateway.config
import at.mocode.infrastructure.gateway.config.CorrelationIdFilter.Companion.CORRELATION_ID_HEADER
import org.slf4j.LoggerFactory
import org.slf4j.MDC
import org.springframework.cloud.gateway.filter.GatewayFilterChain
import org.springframework.cloud.gateway.filter.GlobalFilter
@@ -19,6 +20,8 @@ import reactor.core.publisher.Mono
@Component
class MdcCorrelationFilter : GlobalFilter, Ordered {
private val logger = LoggerFactory.getLogger(MdcCorrelationFilter::class.java)
override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain): Mono<Void> {
val correlationId = exchange.request.headers.getFirst(CORRELATION_ID_HEADER)
if (correlationId != null) {
@@ -26,6 +29,9 @@ class MdcCorrelationFilter : GlobalFilter, Ordered {
}
return chain.filter(exchange)
.doOnError { ex ->
logger.error("Error in MdcCorrelationFilter: {}", ex.message)
}
// Bei Abschluss säubern, um Leaks über Thread-Grenzen zu vermeiden
.doFinally { MDC.remove(CORRELATION_ID_HEADER) }
}
@@ -1,6 +1,7 @@
package at.mocode.infrastructure.gateway.error
import com.fasterxml.jackson.databind.ObjectMapper
import org.slf4j.LoggerFactory
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
@@ -15,12 +16,16 @@ import reactor.core.publisher.Mono
@Component
class ProblemDetailsExceptionHandler : ErrorWebExceptionHandler {
private val logger = LoggerFactory.getLogger(ProblemDetailsExceptionHandler::class.java)
private val mapper = ObjectMapper()
override fun handle(exchange: ServerWebExchange, ex: Throwable): Mono<Void> {
// Versuche, Status aus Attributen zu lesen, ansonsten 500
val status = exchange.response.statusCode?.value() ?: HttpStatus.INTERNAL_SERVER_ERROR.value()
val traceId = exchange.request.headers.getFirst("X-Correlation-ID")
logger.error("Gateway error [{}]: {} (TraceId: {})", status, ex.message, traceId, ex)
val body = mapOf(
"type" to "about:blank",
"title" to (ex.message ?: "Unexpected error"),
@@ -20,10 +20,12 @@ import java.time.Duration
@Component
class GatewayHealthIndicator(
private val discoveryClient: DiscoveryClient,
private val webClient: WebClient.Builder,
webClientBuilder: WebClient.Builder,
private val environment: Environment
) : ReactiveHealthIndicator {
private val webClient = webClientBuilder.build()
companion object {
private val CRITICAL_SERVICES = setOf(
"ping-service"
@@ -120,8 +122,7 @@ class GatewayHealthIndicator(
} else {
val instance = instances.first()
val healthUrl = "http://${instance.host}:${instance.port}/actuator/health"
val client = webClient.build()
client.get()
webClient.get()
.uri(healthUrl)
.retrieve()
.bodyToMono(Map::class.java)
@@ -1,5 +1,6 @@
package at.mocode.infrastructure.gateway.security
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.EnableConfigurationProperties
@@ -24,6 +25,8 @@ class SecurityConfig(
private val securityProperties: GatewaySecurityProperties
) {
private val logger = LoggerFactory.getLogger(SecurityConfig::class.java)
/**
* Konfiguriert die zentrale Security-Filter-Kette für das Gateway.
*
@@ -78,12 +81,12 @@ class SecurityConfig(
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")
logger.warn("Failed to configure JWT decoder with JWK Set URI: {} - {}", jwkSetUri, e.message)
logger.warn("JWT authentication will not work until Keycloak is available")
createNoOpJwtDecoder()
}
} else {
println("INFO: No JWK Set URI configured, using no-op JWT decoder")
logger.info("No JWK Set URI configured, using no-op JWT decoder")
createNoOpJwtDecoder()
}
}
@@ -106,10 +109,11 @@ class SecurityConfig(
fun realmRolesJwtAuthenticationConverter(): org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter {
val converter = org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter()
converter.setJwtGrantedAuthoritiesConverter { jwt ->
val roles = (jwt.claims["realm_access"] as? Map<*, *>)?.get("roles") as? Collection<*> ?: emptyList<Any>()
val realmAccess = jwt.claims["realm_access"] as? Map<*, *>
val roles = realmAccess?.get("roles") as? Collection<*> ?: emptyList<Any>()
roles
.filterIsInstance<String>()
.map { role -> org.springframework.security.core.authority.SimpleGrantedAuthority("ROLE_" + role.lowercase()) }
.map { role -> org.springframework.security.core.authority.SimpleGrantedAuthority("ROLE_${role.uppercase()}") }
}
return org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter(converter)
}
@@ -4,6 +4,7 @@ spring:
autoconfigure:
exclude:
- org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration
- org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration
cloud:
gateway:
httpclient: