API Gateway đóng vai trò quan trọng trong kiến trúc microservices của ứng dụng y tế dựa trên FHIR R5. API Gateway hoạt động như một điểm vào duy nhất cho tất cả các yêu cầu từ client, sau đó định tuyến chúng đến các microservice thích hợp. Tài liệu này hướng dẫn cách thiết lập Spring Cloud Gateway kết hợp với các công nghệ như Keycloak để bảo mật, Redis để quản lý session, và các công cụ giám sát.
Các tính năng chính của API Gateway
- Định tuyến yêu cầu: Chuyển tiếp yêu cầu đến microservice phù hợp
- Xác thực và ủy quyền: Tích hợp với Keycloak để SMART on FHIR Authentication
- Cân bằng tải: Phân phối traffic giữa các instance của microservice
- Giới hạn tốc độ: Bảo vệ API khỏi quá tải hoặc DOS attack
- Logging và monitoring: Theo dõi toàn bộ request/response
- Caching: Tăng hiệu suất cho các endpoint thường xuyên được truy cập
- Circuit breaking: Ngăn lỗi lan truyền giữa các service
- Transformation: Chuyển đổi request/response nếu cần thiết
Công nghệ sử dụng
- Spring Cloud Gateway: API Gateway framework nhẹ, hiệu suất cao
- Keycloak: Identity và Access Management cho SMART on FHIR
- Redis: Lưu trữ session, rate limiting và caching
- Resilience4j: Circuit breaking và fault tolerance
- Prometheus & Grafana: Monitoring và alerting
- ELK Stack: Logging tập trung
- Docker & Kubernetes: Container và orchestration
Các bước thiết lập
1. Tạo Spring Cloud Gateway Project
Tạo một Spring Boot project mới với các dependencies cần thiết:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>io.akitect.healthcare</groupId>
<artifactId>ApiGateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>ApiGateway</name>
<description>Using API Gateway with FHIR API</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>24</java.version>
<spring-cloud.version>2024.0.1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2. Cấu hình Gateway Routes
Cấu hình các routes trong file application.yml:
server:
port: ${PORT:8760}
spring:
application:
name: fhir-api-gateway
cloud:
gateway:
routes:
# Patient Service Routes
- id: patient-service
uri: lb://patient-service
predicates:
- Path=/api/patients/**, /api/related-persons/**
filters:
- name: CircuitBreaker
args:
name: patientServiceCircuitBreaker
fallbackUri: forward:/fallback/patient
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
# Encounter Service Routes
- id: encounter-service
uri: lb://encounter-service
predicates:
- Path=/api/encounters/**, /api/observations/**, /api/diagnostics/**
filters:
- name: CircuitBreaker
args:
name: encounterServiceCircuitBreaker
fallbackUri: forward:/fallback/encounter
# Appointment Service Routes
- id: appointment-service
uri: lb://appointment-service
predicates:
- Path=/api/appointments/**, /api/schedules/**
filters:
- name: CircuitBreaker
args:
name: appointmentServiceCircuitBreaker
fallbackUri: forward:/fallback/appointment
# Reporting Service Routes
- id: reporting-service
uri: lb://reporting-service
predicates:
- Path=/api/reports/**, /api/measures/**
filters:
- name: CircuitBreaker
args:
name: reportingServiceCircuitBreaker
fallbackUri: forward:/fallback/reporting
# FHIR Server Direct Access (secured with special permissions)
- id: fhir-server
uri: ${app.fhir-server-url}
predicates:
- Path=/fhir/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 5
redis-rate-limiter.burstCapacity: 10
key-resolver: "#{@ipKeyResolver}"
# Community Service Routes
- id: community-service
uri: lb://community-service
predicates:
- Path=/api/community/**, /api/groups/**
filters:
- name: CircuitBreaker
args:
name: communityServiceCircuitBreaker
fallbackUri: forward:/fallback/community
# Medication Service Routes
- id: medication-service
uri: lb://medication-service
predicates:
- Path=/api/medications/**, /api/prescriptions/**
filters:
- name: CircuitBreaker
args:
name: medicationServiceCircuitBreaker
fallbackUri: forward:/fallback/medication
# Notification Service Routes
- id: notification-service
uri: lb://notification-service
predicates:
- Path=/api/notifications/**
filters:
- name: CircuitBreaker
args:
name: notificationServiceCircuitBreaker
fallbackUri: forward:/fallback/notification
# Integration Service Routes (HIS connection)
- id: integration-service
uri: lb://integration-service
predicates:
- Path=/api/integration/**
filters:
- name: CircuitBreaker
args:
name: integrationServiceCircuitBreaker
fallbackUri: forward:/fallback/integration
# Frontend routes - React App
- id: frontend
uri: ${app.frontend-url}
predicates:
- Path=/**
filters:
- name: StripPrefix
args:
parts: 0
# Redis Configuration for Rate Limiting
security:
oauth2:
resourceserver:
jwt:
issuer-uri: ${KEYCLOAK_URL:http://localhost:8080}/realms/fhir-realm
jwk-set-uri: ${KEYCLOAK_URL:http://localhost:8080}/realms/fhir-realm/protocol/openid-connect/certs
data:
redis:
port: ${REDIS_PORT:6379}
host: ${REDIS_HOST:localhost}
password: ${REDIS_PASSWORD:admin}
# Service Discovery
eureka:
client:
enabled: true
service-url:
defaultZone: ${EUREKA_URI:http://admin:admin@localhost:8761/eureka/}
instance:
preferIpAddress: true
# Security Configuration
# Circuit Breaker Configuration
resilience4j:
circuitbreaker:
configs:
default:
slidingWindowSize: 10
failureRateThreshold: 50
waitDurationInOpenState: 10000
permittedNumberOfCallsInHalfOpenState: 5
instances:
patientServiceCircuitBreaker:
baseConfig: default
encounterServiceCircuitBreaker:
baseConfig: default
appointmentServiceCircuitBreaker:
baseConfig: default
reportingServiceCircuitBreaker:
baseConfig: default
communityServiceCircuitBreaker:
baseConfig: default
medicationServiceCircuitBreaker:
baseConfig: default
notificationServiceCircuitBreaker:
baseConfig: default
integrationServiceCircuitBreaker:
baseConfig: default
# Actuator Configuration for Monitoring
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always
prometheus:
metrics:
export:
enabled: true
# Application specific properties
app:
fhir-server-url: ${FHIR_SERVER_URL:http://localhost:8000}
frontend-url: ${FRONTEND_URL:http://localhost:3000}
3. Cấu hình bảo mật với SMART on FHIR (Keycloak)
Tạo class SecurityConfig để cấu hình bảo mật:
package io.akitect.healthcare.ApiGateway.config;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverter;
import org.springframework.security.web.server.SecurityWebFilterChain;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
return http
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.authorizeExchange(exchanges -> exchanges
.pathMatchers("/fhir/**").hasRole("FHIR_ADMIN")
.pathMatchers("/api/patients/**").hasAnyRole("PATIENT_READ", "PATIENT_ADMIN")
.pathMatchers("/api/encounters/**").hasAnyRole("ENCOUNTER_READ", "ENCOUNTER_ADMIN")
.pathMatchers("/actuator/**").permitAll()
.anyExchange().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
)
.build();
}
@Bean
public ReactiveJwtAuthenticationConverter jwtAuthenticationConverter() {
ReactiveJwtAuthenticationConverter converter = new ReactiveJwtAuthenticationConverter();
// The key fix: Return Flux<GrantedAuthority> instead of Mono<List<GrantedAuthority>>
converter.setJwtGrantedAuthoritiesConverter(jwt -> {
Map<String, Object> realmAccess = jwt.getClaimAsMap("realm_access");
if (realmAccess == null || !realmAccess.containsKey("roles")) {
return Flux.empty();
}
@SuppressWarnings("unchecked")
List<String> roles = (List<String>) realmAccess.get("roles");
// Convert to Flux<GrantedAuthority> directly
return Flux.fromIterable(roles)
.map(roleName -> "ROLE_" + roleName)
.map(SimpleGrantedAuthority::new);
});
return converter;
}
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(
Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getHostName()
);
}
}
4. Tạo Global Pre-Filter cho Logging và Tracing
@Component
public class LoggingFilter implements GlobalFilter {
private static final Logger log = LoggerFactory.getLogger(LoggingFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String requestId = UUID.randomUUID().toString();
log.info("Request: {} {} {} requestId: {}",
request.getMethod(),
request.getURI(),
request.getQueryParams(),
requestId);
// Add request ID to response header for correlation
return chain.filter(
exchange.mutate()
.request(exchange.getRequest().mutate()
.header("X-Request-ID", requestId)
.build())
.build())
.then(Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
log.info("Response: status={}, requestId={}",
response.getStatusCode(),
requestId);
}));
}
}
5. Triển khai Circuit Breaker và Fallback Endpoints
@RestController
@RequestMapping("/fallback")
public class FallbackController {
@GetMapping("/patient")
public Mono<ResponseEntity<Map<String, String>>> patientFallback() {
Map<String, String> response = new HashMap<>();
response.put("message", "Patient service is currently unavailable. Please try again later.");
response.put("status", "error");
return Mono.just(ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(response));
}
@GetMapping("/encounter")
public Mono<ResponseEntity<Map<String, String>>> encounterFallback() {
Map<String, String> response = new HashMap<>();
response.put("message", "Encounter service is currently unavailable. Please try again later.");
response.put("status", "error");
return Mono.just(ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(response));
}
// Add similar fallback methods for other services
@GetMapping("/**")
public Mono<ResponseEntity<Map<String, String>>> defaultFallback() {
Map<String, String> response = new HashMap<>();
response.put("message", "The requested service is currently unavailable. Please try again later.");
response.put("status", "error");
return Mono.just(ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(response));
}
}
6. Cấu hình CORS cho Frontend
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsWebFilter() {
CorsConfiguration corsConfig = new CorsConfiguration();
corsConfig.setAllowedOrigins(Arrays.asList(
"http://localhost:3000"
));
corsConfig.setMaxAge(3600L);
corsConfig.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
corsConfig.setAllowedHeaders(Arrays.asList("*"));
corsConfig.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfig);
return new CorsWebFilter(source);
}
}
7. Tạo Dockerfile và Docker Compose
Dockerfile
FROM openjdk:21-slim
WORKDIR /app
COPY target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
docker-compose.yml
version: '3.8'
services:
fhir-api-gateway:
build: .
container_name: fhir-api-gateway
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=docker
- EUREKA_HOST=eureka-server
- REDIS_HOST=redis
- KEYCLOAK_URL=http://keycloak:8080
- FHIR_SERVER_URL=http://hapi-fhir-server:8080
- FRONTEND_URL=http://frontend:3000
networks:
- fhir-network
depends_on:
- redis
- eureka-server
- keycloak
redis:
image: redis:7.0-alpine
container_name: fhir-redis
ports:
- "6379:6379"
networks:
- fhir-network
eureka-server:
image: fhir-eureka-server:latest
container_name: fhir-eureka-server
ports:
- "8761:8761"
networks:
- fhir-network
keycloak:
image: quay.io/keycloak/keycloak:latest
container_name: fhir-keycloak
environment:
- KEYCLOAK_ADMIN=admin
- KEYCLOAK_ADMIN_PASSWORD=admin
- KC_DB=postgres
- KC_DB_URL=jdbc:postgresql://postgres:5432/keycloak
- KC_DB_USERNAME=keycloak
- KC_DB_PASSWORD=keycloak
ports:
- "8090:8080"
networks:
- fhir-network
depends_on:
- postgres
command: ["start-dev"]
postgres:
image: postgres:15-alpine
container_name: fhir-postgres
environment:
- POSTGRES_DB=keycloak
- POSTGRES_USER=keycloak
- POSTGRES_PASSWORD=keycloak
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- fhir-network
networks:
fhir-network:
driver: bridge
volumes:
postgres_data:
8. Kubernetes Deployment
Tạo file YAML để triển khai trên Kubernetes:
gateway-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: fhir-api-gateway
namespace: fhir-system
spec:
replicas: 2
selector:
matchLabels:
app: fhir-api-gateway
template:
metadata:
labels:
app: fhir-api-gateway
spec:
containers:
- name: fhir-api-gateway
image: your-docker-registry/fhir-api-gateway:latest
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: "kubernetes"
- name: EUREKA_HOST
value: "eureka-server"
- name: REDIS_HOST
value: "redis"
- name: KEYCLOAK_URL
value: "http://keycloak:8080"
- name: FHIR_SERVER_URL
value: "http://hapi-fhir-server:8080"
- name: FRONTEND_URL
value: "http://frontend:3000"
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 60
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: fhir-api-gateway
namespace: fhir-system
spec:
selector:
app: fhir-api-gateway
ports:
- port: 80
targetPort: 8080
type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: fhir-api-gateway-ingress
namespace: fhir-system
annotations:
kubernetes.io/ingress.class: "nginx"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
tls:
- hosts:
- api.your-fhir-domain.com
secretName: fhir-api-tls
rules:
- host: api.your-fhir-domain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: fhir-api-gateway
port:
number: 80
9. Cấu hình Keycloak cho SMART on FHIR
- Tạo Realm mới có tên
fhir-realm - Cấu hình clients:
SMART on FHIR Client
- Client ID:
fhir-client - Client Protocol:
openid-connect - Access Type:
confidential - Valid Redirect URIs:
http://localhost:3000/*, https://your-production-domain.com/* - Web Origins:
+
- Tạo các roles:
fhir_adminpatient_readpatient_writeencounter_readencounter_writeappointment_readappointment_write
- Tạo các scopes (Client Scopes):
fhir_adminpatient_readpatient_writeencounter_readencounter_writeappointment_readappointment_write
- Map các roles vào scopes và clients
{
"realm": "fhir-realm",
"enabled": true,
"displayName": "FHIR Healthcare System",
"displayNameHtml": "<div class=\"kc-logo-text\"><span>FHIR Healthcare System</span></div>",
"roles": {
"realm": [
{
"name": "fhir_admin",
"description": "FHIR Administrator with full access"
},
{
"name": "patient_read",
"description": "Read access to patient resources"
},
{
"name": "patient_write",
"description": "Write access to patient resources"
},
{
"name": "encounter_read",
"description": "Read access to encounter resources"
},
{
"name": "encounter_write",
"description": "Write access to encounter resources"
},
{
"name": "appointment_read",
"description": "Read access to appointment resources"
},
{
"name": "appointment_write",
"description": "Write access to appointment resources"
}
]
},
"clients": [
{
"clientId": "fhir-client",
"name": "FHIR Client Application",
"description": "SMART on FHIR Client Application",
"rootUrl": "http://localhost:3000",
"adminUrl": "http://localhost:3000",
"baseUrl": "http://localhost:3000",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"secret": "change-me-in-production",
"redirectUris": [
"http://localhost:3000/*",
"https://your-production-domain.com/*"
],
"webOrigins": [
"+"
],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": true,
"publicClient": false,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"access.token.lifespan": 1800,
"saml.assertion.signature": "false",
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"saml.encrypt": "false",
"oauth2.device.authorization.grant.enabled": "false",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature": "false",
"saml.server.signature.keyinfo.ext": "false",
"use.refresh.tokens": "true",
"exclude.session.state.from.auth.response": "false",
"backchannel.logout.session.required": "true",
"client_credentials.use_refresh_token": "false",
"saml_force_name_id_format": "false",
"saml.client.signature": "false",
"tls.client.certificate.bound.access.tokens": "false",
"require.pushed.authorization.requests": "false",
"saml.authnstatement": "false",
"display.on.consent.screen": "false",
"saml.onetimeuse.condition": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": false,
"defaultClientScopes": [
"web-origins",
"profile",
"roles",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt",
"fhir_admin",
"patient_read",
"patient_write",
"encounter_read",
"encounter_write",
"appointment_read",
"appointment_write"
]
}
],
"clientScopes": [
{
"name": "fhir_admin",
"description": "FHIR Administrator Access",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "true",
"display.on.consent.screen": "true",
"consent.screen.text": "FHIR Administrator Access"
},
"protocolMappers": [
{
"name": "fhir_admin role",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-realm-role-mapper",
"consentRequired": false,
"config": {
"multivalued": "true",
"userinfo.token.claim": "true",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "roles",
"jsonType.label": "String",
"role": "fhir_admin"
}
}
]
},
{
"name": "patient_read",
"description": "Read access to patient resources",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "true",
"display.on.consent.screen": "true",
"consent.screen.text": "Read access to patient resources"
},
"protocolMappers": [
{
"name": "patient_read role",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-realm-role-mapper",
"consentRequired": false,
"config": {
"multivalued": "true",
"userinfo.token.claim": "true",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "roles",
"jsonType.label": "String",
"role": "patient_read"
}
}
]
},
{
"name": "patient_write",
"description": "Write access to patient resources",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "true",
"display.on.consent.screen": "true",
"consent.screen.text": "Write access to patient resources"
},
"protocolMappers": [
{
"name": "patient_write role",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-realm-role-mapper",
"consentRequired": false,
"config": {
"multivalued": "true",
"userinfo.token.claim": "true",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "roles",
"jsonType.label": "String",
"role": "patient_write"
}
}
]
},
{
"name": "encounter_read",
"description": "Read access to encounter resources",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "true",
"display.on.consent.screen": "true",
"consent.screen.text": "Read access to encounter resources"
},
"protocolMappers": [
{
"name": "encounter_read role",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-realm-role-mapper",
"consentRequired": false,
"config": {
"multivalued": "true",
"userinfo.token.claim": "true",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "roles",
"jsonType.label": "String",
"role": "encounter_read"
}
}
]
},
{
"name": "encounter_write",
"description": "Write access to encounter resources",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "true",
"display.on.consent.screen": "true",
"consent.screen.text": "Write access to encounter resources"
},
"protocolMappers": [
{
"name": "encounter_write role",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-realm-role-mapper",
"consentRequired": false,
"config": {
"multivalued": "true",
"userinfo.token.claim": "true",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "roles",
"jsonType.label": "String",
"role": "encounter_write"
}
}
]
},
{
"name": "appointment_read",
"description": "Read access to appointment resources",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "true",
"display.on.consent.screen": "true",
"consent.screen.text": "Read access to appointment resources"
},
"protocolMappers": [
{
"name": "appointment_read role",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-realm-role-mapper",
"consentRequired": false,
"config": {
"multivalued": "true",
"userinfo.token.claim": "true",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "roles",
"jsonType.label": "String",
"role": "appointment_read"
}
}
]
},
{
"name": "appointment_write",
"description": "Write access to appointment resources",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "true",
"display.on.consent.screen": "true",
"consent.screen.text": "Write access to appointment resources"
},
"protocolMappers": [
{
"name": "appointment_write role",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-realm-role-mapper",
"consentRequired": false,
"config": {
"multivalued": "true",
"userinfo.token.claim": "true",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "roles",
"jsonType.label": "String",
"role": "appointment_write"
}
}
]
}
],
"defaultDefaultClientScopes": [
"web-origins",
"profile",
"roles",
"email"
],
"defaultOptionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"browserSecurityHeaders": {
"contentSecurityPolicyReportOnly": "",
"xContentTypeOptions": "nosniff",
"xRobotsTag": "none",
"xFrameOptions": "SAMEORIGIN",
"contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';",
"xXSSProtection": "1; mode=block",
"strictTransportSecurity": "max-age=31536000; includeSubDomains"
},
"smtpServer": {},
"eventsEnabled": true,
"eventsListeners": [
"jboss-logging"
],
"enabledEventTypes": [
"LOGIN",
"LOGIN_ERROR",
"LOGOUT",
"LOGOUT_ERROR",
"CODE_TO_TOKEN",
"CODE_TO_TOKEN_ERROR",
"CLIENT_LOGIN",
"CLIENT_LOGIN_ERROR"
],
"adminEventsEnabled": true,
"adminEventsDetailsEnabled": true,
"internationalizationEnabled": false,
"supportedLocales": [],
"identityProviders": [],
"identityProviderMappers": [],
"components": {
"org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [
{
"name": "Allowed Protocol Mapper Types",
"providerId": "allowed-protocol-mappers",
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"oidc-usermodel-attribute-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-property-mapper",
"saml-user-property-mapper",
"oidc-full-name-mapper",
"saml-role-list-mapper",
"saml-user-attribute-mapper",
"oidc-address-mapper"
],
"consent-required-for-all-mappers": [
"true"
]
}
},
{
"name": "Allowed Client Scopes",
"providerId": "allowed-client-templates",
"subComponents": {},
"config": {
"allow-default-scopes": [
"true"
]
}
},
{
"name": "Consent Required",
"providerId": "consent-required",
"subComponents": {},
"config": {}
},
{
"name": "Full Scope Disabled",
"providerId": "scope",
"subComponents": {},
"config": {}
},
{
"name": "Max Clients Limit",
"providerId": "max-clients",
"subComponents": {},
"config": {
"max-clients": [
"200"
]
}
},
{
"name": "Trusted Hosts",
"providerId": "trusted-hosts",
"subComponents": {},
"config": {
"host-sending-registration-request-must-match": [
"true"
],
"client-uris-must-match": [
"true"
]
}
}
],
"org.keycloak.keys.KeyProvider": [
{
"name": "rsa-generated",
"providerId": "rsa-generated",
"subComponents": {},
"config": {
"priority": [
"100"
]
}
},
{
"name": "hmac-generated",
"providerId": "hmac-generated",
"subComponents": {},
"config": {
"priority": [
"100"
],
"algorithm": [
"HS256"
]
}
},
{
"name": "aes-generated",
"providerId": "aes-generated",
"subComponents": {},
"config": {
"priority": [
"100"
]
}
}
]
},
"userManagedAccessAllowed": false
}
10. Cấu hình Monitoring với Prometheus và Grafana
prometheus.yml
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'fhir-api-gateway'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['fhir-api-gateway:8080']
Grafana Dashboard:
- Tạo dashboard để giám sát các metrics chính:
- Request rate và latency
- Error rate
- Circuit breaker status
- JVM metrics (memory, threads)
- Rate limiter status
Tích hợp với Các Microservices FHIR
Để các microservices có thể giao tiếp qua API Gateway, cần đảm bảo:
- Mỗi service đều đăng ký với Eureka Service Discovery
- Xác thực token JWT từ gateway trong mỗi microservice
- Cung cấp OpenAPI/Swagger documentation cho mỗi service
Ví dụ cấu hình Eureka client trong microservice:
# application.yml của một microservice
spring:
application:
name: patient-service
security:
oauth2:
resourceserver:
jwt:
issuer-uri: ${KEYCLOAK_URL:http://localhost:8080}/realms/fhir-realm
jwk-set-uri: ${KEYCLOAK_URL:http://localhost:8080}/realms/fhir-realm/protocol/openid-connect/certs
eureka:
client:
service-url:
defaultZone: http://${EUREKA_HOST:localhost}:${EUREKA_PORT:8761}/eureka/
instance:
prefer-ip-address: true
Kết luận
API Gateway đóng vai trò quan trọng trong kiến trúc microservices của hệ thống y tế FHIR. Bằng cách sử dụng Spring Cloud Gateway kết hợp với các công nghệ như Keycloak, Redis, và các công cụ monitoring, chúng ta có thể tạo ra một gateway hiệu quả, an toàn và có khả năng mở rộng.
Hướng dẫn này đã trình bày các bước cơ bản để thiết lập một API Gateway hoạt động đầy đủ cho ứng dụng y tế dựa trên FHIR R5. Tùy vào quy mô và yêu cầu cụ thể của dự án, có thể cần thêm các tính năng và tùy chỉnh khác.