在 Keycloak 和 HAPI FHIR 中間放一層 Rails(五):Keycloak 發證、OPA 判斷、Rails 執行、Kafka 傳事件
系列最後一篇。把前面拆開講的 constraint、CQL、function tooling 收起來,補上 OPA 跟 Kafka,看整張圖怎麼合。
最重要的原則:Rails 不能是唯一防線
先講結論裡最要緊的一條。這層 Rails 再厲害,我都不讓它變成唯一的安全防線。我採雙層防護:
Rails Gateway
第一層:業務政策、動態 constraint、CQL、tool control
↓
HAPI FHIR
第二層:禁止未授權 Patient、Resource、operation
就算有人繞過 Rails 直連 HAPI,HAPI 的 AuthorizationInterceptor 也不能讓他任意取資料。所以網路上 HAPI 不對外,放在 private network,只接受 Rails 服務帳號、mTLS、固定 audience 的 service token。
別做完全透明的 proxy
我也不建議做 /fhir/* → Rails → HAPI /fhir/* 這種完全透明代理。相容性是高,但很容易漏掉冷門功能:_history、_include、_revinclude、_has、chained search、conditional update、$export、$everything、$graphql、transaction Bundle、PATCH、bulk data。漏一個就是一個洞。
我改成兩條通道。標準 SMART FHIR 通道只放明確白名單的操作:
/api/fhir/Patient
/api/fhir/Observation
/api/fhir/MedicationRequest
臨床 function 通道給 LINE Bot、AI Agent、行動 App:
/api/functions/recent-labs
/api/functions/active-medications
/api/functions/visit-summary
補上 OPA 跟 Kafka
之前我還討論過一套更完整的架構,把 OPA(Open Policy Agent)跟 Kafka 也放進來。角色是這樣分的:Keycloak 發證、OPA 判斷、Rails 執行、HAPI 提供資料、Kafka 傳事件。整張圖合起來大概是這樣:

關鍵是 OPA 只回答「這次可不可以,如果可以要套哪些限制」,它不讀寫 FHIR、也不代理資料。真正執行 maximum_age_days = 7、patient_id = token.patient 的還是 Rails。用 PDP/PEP 的講法就是:OPA 是 Policy Decision Point,Rails 是 Policy Enforcement Point。
Kafka 則不放在每次即時讀取的主路徑上。即時讀取還是走 App → Gateway → OPA → HAPI。Kafka 適合 HIS 跟 Oracle 的資料同步、事件驅動、跨 DMZ 傳輸,還有把一筆資料更新分送給通知服務、CDS、AI Summary、稽核 pipeline 這些消費端。
各元件責任我列成一張表收尾:
| 元件 | 主要責任 |
|---|---|
| Kong | TLS、routing、rate limit、WAF、API 管理 |
| Keycloak | 登入、OAuth2/OIDC、Token、role、scope |
| OPA | 政策計算、Consent、purpose、constraint |
| Rails | 執行政策、改寫查詢、遮罩結果、CQL orchestration、AI tools |
| HAPI FHIR | FHIR REST、搜尋、驗證、版本、資料保存 |
| Kafka | HIS 同步、事件傳遞、跨區交換、非同步工作 |
所以我想到的那層 Rails API,不是另起爐灶,而是把先前的架構補完整。對現在要做的火線超人、患者端 SMART App 跟 AI 臨床工具,我會優先做 Rails 白名單 function API,其次才做有限制的 FHIR proxy,CQL 當 Rails 呼叫的獨立臨床邏輯引擎,不拿去取代 OAuth 授權。