upgrade Java-25 Kotlin-2.3.0 usw.

This commit is contained in:
2025-12-30 16:08:40 +01:00
parent 84c3cfd787
commit 9283f26df1
31 changed files with 745 additions and 550 deletions
@@ -1,17 +1,12 @@
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
import org.springframework.http.HttpStatus
import org.springframework.http.server.reactive.ServerHttpRequest
import org.springframework.http.server.reactive.ServerHttpResponse
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import reactor.core.publisher.Mono
import java.util.concurrent.ConcurrentHashMap
import java.util.UUID
import java.util.*
/**
* Gateway-Konfiguration für erweiterte Funktionalitäten wie Logging, Rate Limiting und Security.
@@ -1,6 +1,7 @@
package at.mocode.infrastructure.gateway.config
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import reactor.core.publisher.Mono
@@ -14,6 +15,7 @@ class RateLimiterConfig {
* Funktioniert out-of-the-box mit Keycloak (Resource Server), sofern Security aktiv ist.
*/
@Bean
@ConditionalOnProperty(prefix = "gateway.ratelimit.principal-key-resolver", name = ["enabled"], havingValue = "true", matchIfMissing = false)
fun principalNameKeyResolver(): KeyResolver = KeyResolver { exchange ->
exchange.getPrincipal<Principal>()
.map { it.name }
@@ -0,0 +1,26 @@
package at.mocode.infrastructure.gateway.config
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver
import org.springframework.context.annotation.Primary
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import reactor.core.publisher.Mono
@Configuration
class RateLimitingConfig {
/**
* Einfache IP-basierte KeyResolver-Implementierung für das RequestRateLimiter-Filter.
* Nutzt X-Forwarded-For, wenn vorhanden, sonst die Remote-Adresse.
*/
@Bean
@Primary
fun ipAddressKeyResolver(): KeyResolver = KeyResolver { exchange ->
val forwardedFor = exchange.request.headers.getFirst("X-Forwarded-For")
?.split(',')?.firstOrNull()?.trim()
val ip = forwardedFor
?: exchange.request.remoteAddress?.address?.hostAddress
?: "unknown"
Mono.just(ip)
}
}
@@ -0,0 +1,38 @@
package at.mocode.infrastructure.gateway.error
import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import reactor.core.publisher.Mono
/**
* Einfacher ProblemDetails-Handler für unerwartete Fehler im Gateway.
* Gibt application/problem+json zurück mit Correlation-ID als traceId.
*/
@Component
class ProblemDetailsExceptionHandler : ErrorWebExceptionHandler {
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")
val body = mapOf(
"type" to "about:blank",
"title" to (ex.message ?: "Unexpected error"),
"status" to status,
"traceId" to traceId
)
exchange.response.statusCode = HttpStatus.valueOf(status)
exchange.response.headers.contentType = MediaType.APPLICATION_PROBLEM_JSON
val bytes = mapper.writeValueAsBytes(body)
val buffer = exchange.response.bufferFactory().wrap(bytes)
return exchange.response.writeWith(Mono.just(buffer))
}
}
@@ -0,0 +1,27 @@
package at.mocode.infrastructure.gateway.fallback
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import java.time.Instant
/**
* Alternative FallbackController (deaktiviert per Default), nur aktivierbar über
* property `gateway.customFallback.enabled=true`. Standardmäßig existiert bereits
* ein FallbackController unter `...gateway.controller.FallbackController`.
*/
@RestController
@ConditionalOnProperty(prefix = "gateway.customFallback", name = ["enabled"], havingValue = "true", matchIfMissing = false)
class FallbackController {
@RequestMapping("/fallback/ping")
fun pingFallback(): ResponseEntity<Map<String, Any>> =
ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(
mapOf(
"message" to "Ping service unavailable",
"timestamp" to Instant.now().toString()
)
)
}
@@ -46,13 +46,18 @@ class SecurityConfig(
pathMatchers(*securityProperties.publicPaths.toTypedArray()),
permitAll
)
// Ping-API erfordert Admin-Rolle (Realm-Rolle "admin")
authorize(pathMatchers("/api/ping/**"), hasRole("admin"))
// Alle anderen Pfade erfordern eine Authentifizierung
authorize(anyExchange, authenticated)
}
// 4. JWT-Validierung via Keycloak aktivieren
oauth2ResourceServer {
jwt { }
jwt {
// Realm-Rollen (Keycloak) -> ROLE_* Authorities
jwtAuthenticationConverter = realmRolesJwtAuthenticationConverter()
}
}
}
}
@@ -93,6 +98,22 @@ class SecurityConfig(
}
}
/**
* Konvertiert Keycloak Realm-Rollen (realm_access.roles) in Spring Authorities (ROLE_*),
* sodass hasRole("admin") funktioniert.
*/
@Bean
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>()
roles
.filterIsInstance<String>()
.map { role -> org.springframework.security.core.authority.SimpleGrantedAuthority("ROLE_" + role.lowercase()) }
}
return org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter(converter)
}
/**
* Definiert die zentrale und einzige CORS-Konfiguration für das Gateway.
*/
@@ -125,8 +146,7 @@ data class GatewaySecurityProperties(
"/actuator/**",
"/webjars/**",
"/v3/api-docs/**",
"/api/auth/**", // Alle Auth-Endpunkte
"/api/ping/**"
"/api/auth/**" // Alle Auth-Endpunkte
)
)
@@ -1,23 +1,13 @@
# ===================================================================
# Keycloak Profile Configuration
# ===================================================================
# This profile configures OAuth2/JWT authentication with Keycloak.
# Uses Spring Security's oauth2ResourceServer for secure JWT validation.
# ===================================================================
# migrated from application-keycloak.yml (standardized to .yaml)
spring:
security:
oauth2:
resourceserver:
jwt:
# Issuer URI for JWT validation - Docker internal: keycloak:8080, External: localhost:8180
issuer-uri: ${KEYCLOAK_ISSUER_URI:http://keycloak:8180/realms/meldestelle}
# JWK Set URI for fetching public keys to validate JWT signatures
jwk-set-uri: ${KEYCLOAK_JWK_SET_URI:http://keycloak:8180/realms/meldestelle/protocol/openid-connect/certs}
# Keycloak-spezifische Konfiguration
keycloak:
# Internal Docker service name, external via port 8180
server-url: ${KEYCLOAK_SERVER_URL:http://keycloak:8180}
issuer-uri: ${KEYCLOAK_ISSUER_URI:http://keycloak:8180/realms/meldestelle}
jwk-set-uri: ${KEYCLOAK_JWK_SET_URI:http://keycloak:8180/realms/meldestelle/protocol/openid-connect/certs}
@@ -0,0 +1,35 @@
spring:
application:
name: gateway
autoconfigure:
exclude:
- org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration
cloud:
gateway:
httpclient:
connect-timeout: 3000
response-timeout: 5s
routes:
- id: ping-service
uri: http://ping-service:8080
predicates:
- Path=/api/ping/**
filters:
- StripPrefix=1
- name: CircuitBreaker
args:
name: pingServiceCB
fallbackUri: forward:/fallback/ping
management:
endpoints:
web:
exposure:
include: health,info,prometheus
# Gateway-spezifische Einstellungen
gateway:
ratelimit:
enabled: false # Start: ausgeschaltet; zum Aktivieren default-filters plus RequestRateLimiter in YAML hinzufügen
replenish-rate: 10
burst-capacity: 20
@@ -1,321 +0,0 @@
# Port, auf dem das Gateway läuft
server:
port: ${GATEWAY_SERVER_PORT:8081}
# Optimierte Netty-Konfiguration für reaktive Anwendungen
netty:
connection-timeout: 5s
idle-timeout: 15s
# Der Name, unter dem sich das Gateway in Consul registriert
spring:
application:
name: api-gateway
profiles:
active: ${SPRING_PROFILES_ACTIVE:dev}
security:
user:
name: ${GATEWAY_ADMIN_USER:admin}
password: ${GATEWAY_ADMIN_PASSWORD:admin}
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
timeout: 3s
cloud:
consul:
host: ${CONSUL_HOST:localhost}
port: ${CONSUL_PORT:8500}
enabled: ${CONSUL_ENABLED:true}
discovery:
enabled: ${CONSUL_ENABLED:true}
register: ${CONSUL_ENABLED:true}
health-check-path: /actuator/health
health-check-interval: 10s
instance-id: ${spring.application.name}-${server.port}-${random.uuid}
gateway:
server:
webflux:
httpclient:
connect-timeout: 5000
response-timeout: 30s
pool:
max-idle-time: 15s
max-life-time: 60s
default-filters:
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
- name: Retry
args:
retries: 2
statuses: BAD_GATEWAY,GATEWAY_TIMEOUT
methods: GET
backoff:
firstBackoff: 100ms
maxBackoff: 1000ms
factor: 2
basedOnPreviousValue: false
- name: AddResponseHeader
args:
name: X-Content-Type-Options
value: nosniff
- name: AddResponseHeader
args:
name: X-Frame-Options
value: DENY
- name: AddResponseHeader
args:
name: Referrer-Policy
value: strict-origin-when-cross-origin
- name: AddResponseHeader
args:
name: Cache-Control
value: no-cache, no-store, must-revalidate
routes:
# ==============================================================
# --- Gateway-Info-Route (optional) ---
# ==============================================================
- id: gateway-info-route
uri: http://localhost:${server.port}
predicates:
- Method=GET
- Path=/gateway-info
filters:
- name: SetStatus
args:
status: 200
- name: SetResponseHeader
args:
name: Content-Type
value: application/json
# ==============================================================
# --- Ping-Service-Integration (optional) ---
# ==============================================================
- id: ping-service-route
uri: lb://ping-service
predicates:
- Path=/api/ping/**
filters:
- 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) ---
# ==============================================================
- id: entries-service-route
uri: lb://entries-service
predicates:
- Path=/api/entries/**
filters:
- StripPrefix=1
# Mappe das Root "/api/entries" explizit auf die Service-Root "/"
- id: entries-service-root
uri: lb://entries-service
predicates:
- Path=/api/entries
filters:
- SetPath=/
- id: entries-service-root-slash
uri: lb://entries-service
predicates:
- Path=/api/entries/
filters:
- SetPath=/
# ==============================================================
# --- 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
# Circuit Breaker Konfiguration
resilience4j:
circuitbreaker:
configs:
default:
registerHealthIndicator: true
slidingWindowSize: 100
minimumNumberOfCalls: 20
permittedNumberOfCallsInHalfOpenState: 3
automaticTransitionFromOpenToHalfOpenEnabled: true
waitDurationInOpenState: 5s
failureRateThreshold: 50
eventConsumerBufferSize: 10
recordExceptions:
- org.springframework.web.client.HttpServerErrorException
- java.util.concurrent.TimeoutException
- java.io.IOException
instances:
defaultCircuitBreaker:
baseConfig: default
pingCircuitBreaker:
baseConfig: default
membersCircuitBreaker:
baseConfig: default
slidingWindowSize: 50
horsesCircuitBreaker:
baseConfig: default
slidingWindowSize: 50
eventsCircuitBreaker:
baseConfig: default
slidingWindowSize: 75
masterdataCircuitBreaker:
baseConfig: default
slidingWindowSize: 30
authCircuitBreaker:
baseConfig: default
slidingWindowSize: 20
failureRateThreshold: 30
# Management und Monitoring
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus,gateway,circuitbreakers
base-path: /actuator
cors:
allowed-origins:
- "https://*.meldestelle.at"
- "http://localhost:*"
allowed-methods: GET,POST
allowed-headers: "*"
allow-credentials: true
endpoint:
health:
show-details: when_authorized
show-components: when_authorized
probes:
enabled: true
metrics:
access: unrestricted
info:
access: unrestricted
prometheus:
access: unrestricted
gateway:
access: unrestricted
circuitbreakers:
enabled: true
metrics:
distribution:
percentiles-histogram:
http.server.requests: true
percentiles:
http.server.requests: 0.5,0.90,0.95,0.99
minimum-expected-value:
http.server.requests: 1ms
maximum-expected-value:
http.server.requests: 30s
tags:
application: ${spring.application.name}
environment: ${spring.profiles.active}
instance: ${spring.cloud.consul.discovery.instance-id}
service: gateway
component: infrastructure
gateway: api-gateway
info:
env:
enabled: true
git:
mode: full
build:
enabled: true
java:
enabled: true
# Tracing-Konfiguration - Aktiviert (Micrometer Tracing + Zipkin)
tracing:
enabled: ${TRACING_ENABLED:false}
sampling:
probability: ${TRACING_SAMPLING_PROBABILITY:1.0}
zipkin:
tracing:
endpoint: ${ZIPKIN_TRACING_ENDPOINT:http://localhost:9411/api/v2/spans}
# Reduziert Verbindungsfehler, wenn Zipkin nicht verfügbar ist
connect-timeout: 1s
read-timeout: 10s
# Erweiterte Logging-Konfiguration
logging:
level:
org.springframework.cloud.gateway: INFO
org.springframework.cloud.loadbalancer: INFO
org.springframework.cloud.consul: INFO
at.mocode.infrastructure.gateway: INFO
io.github.resilience4j: INFO
reactor.netty.http.client: INFO
# (Redis-Konfiguration wurde in den bestehenden spring.data.redis-Block oben integriert)