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,它解析的是 isslaunch 這兩個參數:

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(自己啟動)。

差別不在授權流程本身,兩者換 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 自己啟動
起始參數 isslaunch 沒有,你得自己知道要連哪台
使用者登入 已經在 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 留了解析 isslaunch 的能力,實際跑的卻是 standalone 那條。

這個系列要走哪一條

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

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

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

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

今天不寫程式。打開 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 參數幾乎一樣,只有第三個字元不同(WzAsWzMs)。它們是同一份設定的兩種放法: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 啟動,它會把 isslaunch 交到你手上,病人是誰對方直接告訴你;app 自己啟動,你得自己知道要連哪台伺服器,使用者也得自己登入一次。

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

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

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


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

尚未有邦友留言

立即登入留言