iT邦幫忙

2026 iThome 鐵人賽

DAY 5
0

本文同步發表於個人部落格:SMART 的兩種啟動模式


火線超人的前端是 LINE。

這句話聽起來只是個技術選型。但它其實一開始就把一件事決定了:這個 app 沒有 EHR 可以待在裡面。使用者是在 LINE 的聊天室裡點開它的,不是在醫院診間的病歷系統裡點開它的。醫院的系統根本不知道有這個 app 存在。

翻開專案裡的 FhirOauthService,授權的進入點長這樣:

def initiate_authorization(fhir_server: nil, callback_url:, scope: nil)

initiate 這個字就是重點。授權是這個 app 自己發起的。先挑一台 FHIR 伺服器,自己組出授權網址,自己把使用者送過去。從頭到尾沒有第二個系統參與。

同一個專案裡還躺著另一支服務 Smart::LaunchContextService,它解析的是 iss 和 launch 這兩個參數:

def self.parse_launch_params(params)
  {
    iss: params[:iss],
    launch: params[:launch],
    patient: params[:patient],
    # ...
  }
end

這兩個參數,只有在「被別人啟動」的時候才會出現,LINE 這條路走不到它們。會有這一支,是因為我當初根本分不清楚這兩種模式差在哪,乾脆兩種都寫。結果就是兩條路在程式碼裡都留了位置,實際跑起來只用得到其中一條。

那時候要是有人先把差別講清楚,我可以少寫一半。今天就來把這兩條路攤開看。

兩種啟動模式

SMART App Launch 定義了兩種啟動方式。名字很直白:EHR Launch 是從 EHR 裡啟動。Standalone Launch 是 app 自己啟動。

差別不在授權流程本身。兩者換 token 的方式一模一樣,都是我們後面幾天要講的授權碼流程。差別在誰來啟動,以及app 一開始知道多少事。

EHR Launch:從病歷系統裡被點開

想像醫師正在看某位病人的病歷,畫面上有一排小工具按鈕,其中一個是你的 app。醫師點下去,你的 app 在 EHR 裡開了一個視窗。

EHR Launch 的時序圖。畫面有左右兩塊深藍色區域,都標示「醫院那一側」,左塊裡站著醫師與 EHR,右塊裡站著授權伺服器,旁邊還有一個淡化的 FHIR server,底下註明「iss 與 aud 指的是它」,它沒有生命線也不參與這段流程。兩塊中間夾著一段米色,上面只有一個角色,是明顯放大的白底方塊「你的 app」,上方標「你負責的」。參與流程的四個角色各配一個圖示與一條往下的虛線生命線。四趟訊息由上而下:醫師送出「在病歷裡點開 app」到 EHR;EHR 用一條粗珊瑚色箭頭跨出左邊的深色區送到你的 app,標籤寫「開啟你的網址,帶上」並附 iss 與 launch 兩個參數標籤,launch 是全圖唯一用珊瑚色標示的東西;你的 app 送出「帶著 launch 去要授權」到右塊的授權伺服器;授權伺服器以灰藍虛線回傳「token,病人在裡面」。圖下方灰底註記寫著整趟沒有登入畫面,也沒有同意畫面,醫師的身分與這個 app 的權限,醫院那一側早就確認過了

關鍵是 EHR 開啟你的網址時帶了兩個參數:

  • iss:這家醫院的 FHIR 伺服器位址。app 不用事先知道自己會被哪家醫院啟動
  • launch:一個不透明的識別碼。你看不懂它的內容,但把它原樣帶去授權伺服器,對方就知道「這次啟動是醫師在看某某病人時發起的」

launch 是這個模式的靈魂。它像是一張寄物櫃的號碼牌,牌子本身沒有資訊。但拿著它去櫃檯,就能領出「現在的病人是誰、在哪個就診事件」這些 context。app 什麼都不用問,token 回來時病人就在裡面了。

醫師也不會看到同意畫面,因為他在 EHR 裡已經登入過,app 也早就註冊在系統裡了。

Standalone Launch:使用者自己開 app

沒有 EHR 可以待的 app 走這條。使用者從桌面圖示、瀏覽器書籤,或者像火線超人一樣從 LINE 的選單點開它。

Standalone Launch 的時序圖,與前一張的配色語言相同但左右相反。左側米色區標「你負責的」,站著使用者與明顯放大的白底方塊「你的 app」;右側是一塊深藍色區域標「醫院那一側」,這次流程裡只剩授權伺服器一個角色,旁邊同樣有一個淡化、不參與流程的 FHIR server,底下註明「aud 指的是它」。七趟訊息由上而下:使用者送出「從 LINE 或書籤點開 app」到你的 app;你的 app 用一條深藍粗箭頭撞進深色區,標籤寫「自己組授權網址去要授權」並附兩個標籤,launch 被劃掉、aud 保留;接著是一塊珊瑚色虛線框住的橫向區域,左上角標「多出來的四趟」,這是全圖唯一用珊瑚色的地方,裡面依序是授權伺服器回「請先登入」、使用者送出「選好身分,送出登入」、授權伺服器回「這個 app 要這些權限,給不給」、使用者送出「按下同意」,這四趟都橫跨你的 app 的生命線而不經過它;最後授權伺服器以灰藍虛線回傳「token,病人在裡面」給你的 app。圖下方灰底註記寫著中間那四趟你的 app 完全沒參與,它只是等著被導回來,然後拿到跟 EHR Launch 一樣的 token

開場的是使用者自己,所以沒有 launch 參數,多了登入與同意。攤成訊息就是圖上那四趟。如果 app 需要知道「現在要看哪位病人」,得由使用者自己選。或者由授權伺服器依登入身分決定。

火線超人就是後者,使用者是病人本人。登入完成後 context 裡的病人自然就是他自己。

擺在一起看

多出來的那四趟不是缺點,是責任的移轉:醫院那端已經替你確認過使用者是誰了。

EHR Launch Standalone Launch
誰來啟動 EHR 開啟你的網址 app 自己啟動
起始參數 iss 加 launch 沒有,你得自己知道要連哪台
使用者登入 已經在 EHR 裡登入過了 要在授權伺服器登入一次
病人 context 哪來 launch 換回來的 登入身分或使用者自選
典型場景 醫師在診間用的臨床工具 病人自己的健康管理 app
需要先跟醫院註冊嗎 要,EHR 得知道你的啟動網址 要,但只需註冊 redirect 網址

aud:兩邊都要回答的問題

表格裡沒列的還有一個參數:aud(audience)。app 去要授權時不只要自報身分。還得說清楚「這張 token 要拿去存取哪一台 FHIR 伺服器」。兩種模式都要送,不是 standalone 專用。

差別只在值從哪裡來。EHR launch 把 iss 原封不動抄過去;standalone 沒人告訴你,只能填自己設定的那台。

為什麼要多這一道手續?因為 token 是不記名的票,誰拿到誰能用。規範對 aud 的說明很直白:防止把真的 token 洩漏給假的資源伺服器。

有人架一台假 FHIR server 騙你的 app 去授權。沒宣告目的地就會拿到一張到處能用的票;宣告了,授權伺服器就會擋掉不歸它管的目標。

同一個 app 可以兩種都支援

這兩種模式不是二選一的陣營。判斷方式很簡單,看啟動網址上有沒有 launch 參數:

const params = new URLSearchParams(location.search);
const iss = params.get('iss');
const launch = params.get('launch');

if (iss && launch) {
  // EHR launch:伺服器位址是對方給的,授權時要把 launch 原樣帶上
  startAuthorization({ fhirBaseUrl: iss, launch });
} else {
  // standalone:使用者自己開的,伺服器位址得自己知道
  startAuthorization({ fhirBaseUrl: MY_DEFAULT_FHIR_SERVER });
}

真正麻煩的不是這個 if,是兩條路的 scope 與 context 不一樣。這點後面幾天會展開。

表格最後一列的「註冊」值得多說一句。不管走哪條路,app 都得先在對方的授權伺服器登記。至少要留一組 client id 和 redirect URI。

redirect URI 是授權完成後使用者被送回的位址。伺服器只會把授權碼送到登記過的位址,一個字都不能差。本機開發改個埠號就得回去改註冊資料。EHR launch 還要多登記啟動網址,因為 EHR 得知道那排按鈕點下去要開哪一頁。上線後每家醫院還要各註冊一次,拿到各自的 client id。

只是對火線超人來說,支援 EHR launch 沒有意義。它的使用者永遠是從 LINE 進來的,不會有哪家醫院的病歷系統去啟動一個聊天機器人。所以 LaunchContextService 留了解析 iss 和 launch 的能力,實際跑的卻是 standalone 那條。

這個系列要走哪一條

接下來九篇的可跟著做程式碼,一律走 standalone launch。

理由很現實:standalone 授權完成後,使用者會被送回你自己的 localhost 頁面。本機開發環境直接就能跑。EHR launch 得先讓 Launcher 內嵌開啟你的 app、承接 iss 和 launch。還要應付 iframe 的各種狀況,時間會花在跟授權原理無關的除錯上。

EHR launch 不會被跳過。等到 day12 講 launch context,我們會實際跑一次。跑的是 Launcher 的 EHR 模式,看 launch 參數怎麼把病人帶進來。今天先知道差別在哪就夠了。

跟著做:在 Launcher 上把兩種模式各跑一次

起點:day04 建好的 smart-app 原封不動。今天不寫程式,一個檔案都不會動到。

產出:兩種模式的網址列各看過一次。你也會找到 Server's FHIR Base URL 那個欄位,明天要把它複製走。

打開 SMART Health IT Launcher,兩種模式各跑一次,重點全部在網址列。

第一次,EHR Launch:

  1. 進入 Launcher,Launch Type 保持預設的 Provider EHR Launch
  2. 捲到最下面,有一個標題是 App's Launch URL 的欄位。Launcher 在這裡問的是「你的 app 要從哪一頁開始」
  3. 填入 https://example.org/launch。這個網域是保留給示範用的,會回一個乾淨的靜態頁面,正好當一個「什麼都不做的 app」
  4. 按旁邊的 Launch,然後看網址列

你會看到這樣一串(launch 的值每次不一定相同):

https://example.org/launch
  ?iss=https%3A%2F%2Flaunch.smarthealthit.org%2Fv%2Fr4%2Ffhir
  &launch=WzAsIiIsIiIsIkFVVE8iLDAsMCwwLCIiLCIiLCIiLCIiLCIiLCIiLCIiLDAsMSwiIl0

這就是 EHR 交棒的那一刻。頁面上只寫著 Example Domain,什麼事都沒發生,但兩個參數已經送到了。真正的 app 會在這裡接手:把 iss 記下來、把 launch 原樣帶去授權伺服器。

想看完整流程的話,把 Launch URL 清空。改按下面的 Launch Sample App。這次會先要你挑一位醫師登入、再挑一位病人,Sample App 才開起來。

停在登入畫面時看一下網址列,那一長串就是 app 送出的授權請求。裡面有 launch,也有 aud。而 aud 的值跟剛才的 iss 一模一樣。全程不會出現同意畫面。

等 app 開起來,網址列又變回乾淨的 /sample-app。參數被 app 自己清掉了。因為它們留在網址列上會進瀏覽器歷史,也會跟著複製貼上跑到別的地方。

第二次,Standalone Launch:

  1. 回到 Launcher,Launch Type 改選 Patient Standalone Launch
  2. 先別急著按,看剛才那個欄位

欄位標題變了。原本的 App's Launch URL 變成 Server's FHIR Base URL。內容變成唯讀,旁邊多了一顆 Copy 按鈕。

這一個改變就把差別講完了。EHR launch 模式下,Launcher 問的是「你的 app 在哪裡」。因為等一下要由它去開你的 app。standalone 模式下換成它給你一串位址,因為這次是你要來找它。

  1. 按 Launch Sample App,走完全程

這次會先看到 Patient Login。登入後還有一張列出權限的 Authorize App Launch。按 Approve 才進得去。這兩步就是沒有 EHR 代勞之後多出來的成本。一樣在登入畫面看網址列,launch 不見了,aud 換成那串含 /sim/ 的長位址。

兩串編碼其實是同一份設定

把剛才兩個值擺在一起:

兩個瀏覽器視窗的網址列對照。上面是 EHR Launch,標題列標著 Launcher 的欄位名稱 App's Launch URL,網址是 https://example.org/launch 加上兩個參數,iss 用綠色標示為 https%3A%2F%2Flaunch.smarthealthit.org%2Fv%2Fr4%2Ffhir,launch 用珊瑚色標示為 WzAsIiIsIiIsIkFVVE8i 開頭的編碼,下方兩個標籤寫著 iss 是乾淨的 /v/r4/fhir、launch 由 EHR 交棒。下面是 Standalone Launch,標題列的欄位名稱換成 Server's FHIR Base URL,位址是 https://launch.smarthealthit.org/v/r4/ 之後接珊瑚色反白的 sim/WzMsIiIsIiIsIkFVVE8i 再接 /fhir,標籤寫著沒有 launch 可用、設定編進路徑裡位址因此變長。最下方深藍色橫條把兩串編碼上下對齊,只有第三個字元 A 與 M 用黃色標出,右側寫著只差第三個字元,base64 解開是同一個陣列,第一個數字 0 是 EHR、3 是 standalone

/sim/ 後面那一段跟 launch 參數幾乎一樣,只有第三個字元不同(WzAs 對 WzMs)。它們是同一份設定的兩種放法:base64 編碼過的 JSON 陣列。第一個數字就是啟動類型,0 是 Provider EHR Launch。3 是 Patient Standalone Launch。

這也解釋了 standalone 的位址為什麼那麼長:模擬設定總得有地方放。EHR launch 塞在 launch 裡,standalone 只好編進路徑。

要強調的是,能解開是這個 Launcher 的實作細節,不是標準行為。SMART 規定 launch 對 app 而言是不透明的。你該做的只有原樣轉交,不要去解它,更不要依賴裡面的內容。

這個細節明天就會派上用場。如果你把那串乾淨的 /v/r4/fhir 拿去當 standalone 要連的伺服器位址呢?授權伺服器會直接把你退回來,錯誤訊息是 Invalid launch options。明天我們就會踩到這個坑,然後爬出來。

預期結果:兩種模式的網址列你都看過了。而且說得出「誰來啟動」怎麼影響 Launcher 的欄位標題、登入畫面與同意畫面。

小結

今天沒動到程式碼,但決定了後面九篇的路線:走 standalone launch。因為它的每一步都能在你自己的機器上完成。

兩種模式的分水嶺就一件事,誰來啟動。由 EHR 啟動,它會把 iss 和 launch 交到你手上,病人是誰對方直接告訴你;app 自己啟動,你得自己知道要連哪台伺服器,使用者也得自己登入一次。

火線超人選 standalone 不是評估比較後的結果,是住在 LINE 裡的必然。這也提醒我們一件事:啟動模式往往不是技術偏好決定的,是產品長在哪裡決定的。

那麼問題來了。standalone 是自己出發的。我們怎麼知道某台 FHIR 伺服器的授權網址在哪、token 要往哪裡換?總不能每家醫院都寫死在程式裡吧。

明天談 discovery,看看怎麼「問」出這些端點。


上一篇
Day04 - SMART on FHIR 開發環境準備
下一篇
Day06 - SMART Discovery 與能力探索
系列文
SMART on FHIR 開發之路:30 天做一個跨醫院的 app 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言