upgrade Java-25 Kotlin-2.3.0 usw.
This commit is contained in:
+1
-6
@@ -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.
|
||||
|
||||
+2
@@ -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 }
|
||||
|
||||
+26
@@ -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)
|
||||
}
|
||||
}
|
||||
+38
@@ -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))
|
||||
}
|
||||
}
|
||||
+27
@@ -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()
|
||||
)
|
||||
)
|
||||
}
|
||||
+23
-3
@@ -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
-11
@@ -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)
|
||||
Reference in New Issue
Block a user