1. 医療システムのマルチテナント戦略

###1.1. Keycloakを使用したマルチテナントモデル
多くの病院/診療所の医療システムを構築する場合、次の 3 つの戦略があります。

| 戦略 | モデル | 隔離 | 適切 |
|---|---|---|---|
| A: 病院ごとのレルム | 各病院には独自のレルムがあります。最上位 - 孤立したユーザー、ロール、クライアント | 独立系病院 | |
| B: 共有レルム + 組織 | 1 つの医療領域、各病院は 1 つの組織 | 中 - 共有 ID + 組織の分離 | 病院チェーン、保健省 |
| C: 共有レルム + グループ | 1 つのレルム、グループごとに分割 | 低 — シンプル、分離度が低い | 小さなクリニック |
###1.2.ベトナムの健康に関する推奨事項
| シナリオ | 推奨戦略 | 理由 |
|---|---|---|
| 私立病院チェーン | 戦略 B (組織) | 患者 ID の共有、組織レベルの分離 |
| 独立した公立病院 | 戦略 A (病院ごとのレルム) | 最大限の分離、独立した管理 |
| 小規模総合病院 | 戦略 C (グループ) | シンプルで管理が簡単 |
| 保健省は多くの施設を管理しています。戦略 B (組織) | 一元管理、組織の委任 |
2. ヘルスケア領域の設計
###2.1.レルム構成
{
"realm": "healthcare",
"displayName": "Healthcare Platform",
"enabled": true,
"sslRequired": "all",
"registrationAllowed": false,
"loginWithEmailAllowed": true,
"duplicateEmailsAllowed": false,
"resetPasswordAllowed": true,
"editUsernameAllowed": false,
"bruteForceProtected": true,
"permanentLockout": false,
"maxFailureWaitSeconds": 900,
"minimumQuickLoginWaitSeconds": 60,
"waitIncrementSeconds": 300,
"quickLoginCheckMilliSeconds": 1000,
"maxDeltaTimeSeconds": 43200,
"failureFactor": 5,
"passwordPolicy": "length(12) and upperCase(1) and lowerCase(1) and digits(1) and specialChars(1) and notUsername and passwordHistory(5) and maxLength(128)",
"ssoSessionIdleTimeout": 900,
"ssoSessionMaxLifespan": 28800,
"accessTokenLifespan": 300,
"accessTokenLifespanForImplicitFlow": 300,
"offlineSessionIdleTimeout": 2592000,
"actionTokenGeneratedByAdminLifespan": 43200,
"actionTokenGeneratedByUserLifespan": 300,
"organizationsEnabled": true,
"attributes": {
"cibaBackchannelTokenDeliveryMode": "poll",
"cibaExpiresIn": "120",
"cibaAuthRequestedUserHint": "login_hint",
"parRequestUriLifespan": "60"
}
}
###2.2.セキュリティ防御の構成
{
"browserSecurityHeaders": {
"contentSecurityPolicy": "default-src 'self'; frame-src 'self'; frame-ancestors 'self'; object-src 'none'; script-src 'self' 'unsafe-inline';",
"contentSecurityPolicyReportOnly": "",
"xContentTypeOptions": "nosniff",
"xRobotsTag": "none",
"xFrameOptions": "SAMEORIGIN",
"strictTransportSecurity": "max-age=31536000; includeSubDomains",
"xXSSProtection": "1; mode=block",
"referrerPolicy": "no-referrer"
}
}
3. ヘルスケア用のクライアント構成
###3.1.クライアントの概要
| クライアント | タイプ | 説明 |
|---|---|---|
| 彼のウェブアプリ | パブリック | HIS Web フロントエンド |
| emr-web-app | パブリック | EMR Web フロントエンド |
| 患者ポータル | パブリック | 患者ポータル |
| モバイルドクターアプリ | パブリック | ドクターモバイルアプリ |
| 患者サービス | 機密 | 患者API |
| 臨床サービス | 機密 | 臨床API |
| ラボサービス | 機密 | ラボ結果 API |
| 薬局サービス | 機密 | 薬局 API |
| スケジューリングサービス | 機密 | スケジューリング API |
| 請求サービス | 機密 | 課金 API |
| 通知サービス | 機密 | お知らせ |
| 監査サービス | 機密 | 監査/ロギング |
| APIゲートウェイ | 機密 | コング/APISIX |
| 管理者-cli | 機密 | 管理操作 |
| fhirサーバー | 機密 | HAPI FHIR サーバー |
###3.2.患者ポータル クライアント (パブリック)
{
"clientId": "patient-portal",
"name": "Patient Portal",
"description": "Patient self-service portal for viewing medical records",
"enabled": true,
"publicClient": true,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"implicitFlowEnabled": false,
"serviceAccountsEnabled": false,
"authorizationServicesEnabled": false,
"rootUrl": "https://portal.hospital.vn",
"baseUrl": "/",
"redirectUris": [
"https://portal.hospital.vn/*"
],
"webOrigins": [
"https://portal.hospital.vn"
],
"defaultClientScopes": [
"openid", "profile", "email", "patient-scope"
],
"optionalClientScopes": [
"offline_access"
],
"attributes": {
"pkce.code.challenge.method": "S256",
"post.logout.redirect.uris": "https://portal.hospital.vn/*",
"access.token.lifespan": "300",
"client.session.idle.timeout": "600"
}
}
###3.3.マイクロサービス クライアント (機密)
{
"clientId": "patient-service",
"name": "Patient Microservice",
"description": "Backend service managing patient demographics",
"enabled": true,
"publicClient": false,
"standardFlowEnabled": false,
"directAccessGrantsEnabled": false,
"serviceAccountsEnabled": true,
"authorizationServicesEnabled": true,
"secret": "${PATIENT_SERVICE_SECRET}",
"defaultClientScopes": [
"openid", "microprofile-jwt"
],
"attributes": {
"use.jwks.url": "true",
"token.endpoint.auth.signing.alg": "RS256"
},
"authorizationSettings": {
"policyEnforcementMode": "ENFORCING",
"decisionStrategy": "AFFIRMATIVE",
"resources": [
{
"name": "Patient Record",
"type": "urn:patient-service:resources:patient",
"uris": ["/api/v1/patients/*"],
"scopes": [
{"name": "read"},
{"name": "write"},
{"name": "delete"}
]
}
]
}
}
4. 医療用のユーザー プロファイル スキーマ
###4.1.医療ユーザーの属性
{
"attributes": [
{
"name": "employeeId",
"displayName": "Mã nhân viên",
"required": { "roles": ["user"] },
"permissions": { "edit": ["admin"], "view": ["admin", "user"] },
"validations": { "pattern": { "pattern": "^NV-\\d{6}$" } }
},
{
"name": "medicalLicenseNumber",
"displayName": "Số giấy phép hành nghề",
"required": { "roles": ["doctor"] },
"permissions": { "edit": ["admin"], "view": ["admin", "user"] },
"validations": { "length": { "min": 5, "max": 20 } }
},
{
"name": "department",
"displayName": "Khoa/Phòng",
"required": { "roles": ["user"] },
"permissions": { "edit": ["admin"], "view": ["admin", "user"] },
"validations": {
"options": {
"options": [
"KHOA_NOI", "KHOA_NGOAI", "KHOA_SAN", "KHOA_NHI",
"KHOA_UNG_BUOU", "KHOA_TIM_MACH", "KHOA_THAN_KINH",
"KHOA_XET_NGHIEM", "KHOA_CHAN_DOAN_HINH_ANH",
"KHOA_DUOC", "KHOA_CAP_CUU", "PHONG_HANH_CHINH"
]
}
}
},
{
"name": "hospitalCode",
"displayName": "Mã bệnh viện",
"required": { "roles": ["user"] },
"permissions": { "edit": ["admin"], "view": ["admin"] }
},
{
"name": "specialization",
"displayName": "Chuyên khoa",
"permissions": { "edit": ["admin"], "view": ["admin", "user"] }
}
]
}
###4.2.医療保険請求用のカスタム トークン マッパー
{
"name": "healthcare-claims-mapper",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"config": {
"user.attribute": "department",
"claim.name": "department",
"jsonType.label": "String",
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true"
}
}
JWT トークンには、医療固有のクレームが含まれます。
{
"sub": "019e1a40-user-0001-d001-f0a1b2c30001",
"name": "BS. Nguyễn Văn A",
"preferred_username": "bs.nguyen.a",
"email": "[email protected]",
"realm_access": {
"roles": ["doctor", "department_head"]
},
"resource_access": {
"patient-service": { "roles": ["patient_read", "patient_write"] },
"lab-service": { "roles": ["lab_read", "lab_order"] },
"pharmacy-service": { "roles": ["prescription_write"] }
},
"department": "KHOA_NOI",
"hospitalCode": "BV-CR-001",
"medicalLicenseNumber": "GPHN-12345",
"employeeId": "NV-001234",
"org_id": "bv-cho-ray"
}
5. 医療の役割階層
###5.1.レルムの役割
Realm Roles:
├── super_admin (Quản trị hệ thống toàn cục)
├── hospital_admin (Quản trị bệnh viện)
├── department_head (Trưởng khoa)
├── doctor (Bác sĩ)
├── senior_nurse (Điều dưỡng trưởng)
├── nurse (Điều dưỡng)
├── lab_technician (Kỹ thuật viên xét nghiệm)
├── pharmacist (Dược sĩ)
├── radiologist (Bác sĩ chẩn đoán hình ảnh)
├── receptionist (Lễ tân)
├── billing_staff (Nhân viên thanh toán)
├── patient (Bệnh nhân - Patient Portal)
└── auditor (Kiểm toán viên - read-only)
###5.2.複合役割
{
"name": "doctor",
"composite": true,
"composites": {
"realm": [],
"client": {
"patient-service": ["patient_read", "patient_write"],
"clinical-service": ["encounter_read", "encounter_write", "diagnosis_write"],
"lab-service": ["lab_read", "lab_order"],
"pharmacy-service": ["prescription_read", "prescription_write"],
"scheduling-service": ["appointment_read", "appointment_write"],
"imaging-service": ["imaging_read", "imaging_order"]
}
}
}
6. 病院向けのセッション管理
###6.1.セッションポリシー
| ユーザータイプ | セッションアイドル | セッション最大値 | 理由 |
|---|---|---|---|
| 医師 | 15分 | 8時間 | 共有ワークステーション、頻繁な自動ログオフ |
| 看護師 | 10分 | 8時間 | モバイル カート、素早いアクセスが必要 |
| 患者ポータル | 15分 | 2時間 | 公衆インターネットアクセス |
| 管理者 | 30分 | 8時間 | 管理タスク、PHI への曝露が減少 |
| サービスアカウント | 該当なし | 該当なし | トークンベース、対話型セッションなし |
###6.2.共有ワークステーションのサポート
病院には共有ワークステーションがあることが多く、多くの医師や看護師が 1 台のコンピュータを共有しています。解決策:

| オプション | メカニズム | 利点 | 制限事項 |
|---|---|---|---|
| 1.ユーザーの高速切り替え | Keycloak ショートセッション + バッジログイン | 早くて便利 | インフラストラクチャが必要 (バッジ リーダー) |
| 2.自動ログオフ + クイック再認証 | セッションタイムアウト 10 ~ 15 分 + PIN | シンプルでハードウェアは不要 | バッジよりも遅い |
| 3.仮想デスクトップ (VDI) | 各ユーザーは独自の仮想デスクトップを持っています。完全な隔離 | 高価で待ち時間が長い |
7. レルムのエクスポート/インポートの自動化
###7.1. Keycloakのコードとしてのインフラストラクチャ
#!/bin/bash
# export-realm.sh - Export healthcare realm for version control
KEYCLOAK_URL="https://keycloak.hospital.internal"
ADMIN_TOKEN=$(curl -s -X POST "${KEYCLOAK_URL}/realms/master/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=admin" \
-d "password=${KC_ADMIN_PASSWORD}" \
-d "grant_type=password" \
-d "client_id=admin-cli" | jq -r '.access_token')
# Export realm (excluding users for security)
curl -s -X GET "${KEYCLOAK_URL}/admin/realms/healthcare" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-H "Accept: application/json" | jq '.' > realm-healthcare-export.json
echo "Realm exported successfully"
###7.2. Keycloak 用の Terraform (オプション)
# keycloak.tf - Keycloak Realm as Code
resource "keycloak_realm" "healthcare" {
realm = "healthcare"
enabled = true
login_theme = "healthcare-theme"
security_defenses {
brute_force_detection {
permanent_lockout = false
max_login_failures = 5
wait_increment_seconds = 300
quick_login_check_milli_seconds = 1000
minimum_quick_login_wait_seconds = 60
max_failure_wait_seconds = 900
}
}
ssl_required = "all"
password_policy = "length(12) and upperCase(1) and lowerCase(1) and digits(1) and specialChars(1)"
sso_session_idle_timeout = "15m"
sso_session_max_lifespan = "8h"
access_token_lifespan = "5m"
}
resource "keycloak_role" "doctor" {
realm_id = keycloak_realm.healthcare.id
name = "doctor"
description = "Bác sĩ - Full clinical access"
composite_roles = [
keycloak_role.patient_read.id,
keycloak_role.patient_write.id,
keycloak_role.prescription_write.id,
]
}
8. まとめ
このレッスンでは次のことを行います。
- マルチ病院システム向けの 3 つのマルチテナンシー戦略を比較します
- セキュリティ強化を備えた ヘルスケア レルム 設計
- 患者ポータル、マイクロサービス、管理者の クライアント を構成する
- 医療固有の属性を使用して ユーザー プロファイル スキーマを構築する
- 病院組織に合わせて 役割階層 を設計する
- 共有ワークステーションの セッション管理 を構成する
- Infrastructure as Code を使用して レルムのエクスポート/インポート を自動化する
演習
- レッスンのすべてのセキュリティ構成を使用して「ヘルスケア」Keycloak レルムを作成します
- 5 つのクライアントを登録します: 患者ポータル、患者サービス、検査サービス、薬局サービス、API ゲートウェイ
- 役割階層を作成し、それをテスト ユーザーに割り当てます: 医師 1 人、看護師 1 人、患者 1 人
- レルム設定をエクスポートし、Git リポジトリにコミットします。
| ◀ 前の記事 | 次の記事 ▶ |
|---|---|
| レッスン 4: 医療情報システムの STRIDE/DREAD の脅威モデリング | レッスン 6: RBAC と ABAC - 医師、看護師、患者の分散化 |