iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0

本文同步發表於個人部落格:scope 解鎖哪些欄位


昨天那份火線超人的預設清單,我們只看了前面五行:

DEFAULT_SCOPES = %w[
  patient/Patient.read
  patient/Observation.read
  patient/Condition.read
  patient/MedicationStatement.read
  patient/Encounter.read
  launch/patient
  openid
  fhirUser
].join(' ').freeze

後面三行才是今天的主角。它們跟前面五行擺在同一個陣列裡,語法卻完全不一樣。都沒有帶「.」,也不對應任何一種 FHIR 資源。

還有一件事,從這份清單本身就看得出來:它沒有 offline_access

這代表火線超人拿不到 refresh token。access token 過期使用者就得再走一次授權。對一個住在 LINE 裡的 app 來說,這是在對話中間就會被打斷的體感。

我不知道當初是刻意還是漏掉,這份清單只告訴我它不在。但它剛好示範了一件事:這幾個 scope 少要一個,少掉的不是資料,是能力。

它們要的不是資料,是欄位

昨天那種分成三個部分的 scope 決定「這張 token 能碰什麼資料」。今天這幾個決定的是另一件事。token response 這份 JSON 上會多出哪些欄位。

少要一個 patient/Observation.rs,是拿不到檢驗資料;少要一個 launch/patient,是連「現在是哪位病人」都不知道。前者影響能力範圍,後者影響程式跑不跑得起來。

在看表之前先說清楚一件事。這些欄位不是你送了 scope 就一定拿得到。

規範把這件事定義成一場協商。app 提出想要哪些 launch context,伺服器決定給哪幾個。伺服器甚至可以回傳你沒有要求的 context。下面這張表是我在 SMART Health IT Launcher 上跑的。不是每一台都這樣。

我把十種組合實際跑了一遍,只換 scope,其他參數完全不動:

送出的 scope patient id_token refresh_token
patient/*.rs
launch/patient patient/*.rs
patient/*.rs openid fhirUser
patient/*.rs offline_access
以上全送

這台的對應很整齊。要哪個欄位,就送對應的那個 scope。

沒送的時候呢?那個 key 根本不存在,不是給你一個空值。 待會的跟著做會讓你親眼看到這件事怎麼安靜地爆掉。

但整齊的是這台,不是規範。換一台伺服器,要看它實際核發了哪些 scope。也要看 token response 回了哪些欄位。

scope 與 token response 欄位的對應圖,標題「要什麼欄位,送什麼 scope」,副標「這台 Launcher 的實測對應,換一台自己確認」。中間三列,左欄是深藍色底白字的 scope 方塊,右欄是白底的 token response 欄位方塊,各以一條灰藍色箭頭由左指向右:launch/patient 指向 patient、openid 與 fhirUser 兩個字並排的方塊指向 id_token、offline_access 指向 refresh_token。左欄上方標「送出去的 scope」,右欄上方標「token response」。最下方一整條深藍色區塊,左半邊三行文字寫著在這台沒送不是給你一個空值,是那個 key 根本不存在,程式不會報錯(不會報錯四字為珊瑚色),錯要等到你拿它去組網址;右半邊是一段深色程式碼,內容為 token.patient 箭頭 undefined,以及 Patient/undefined,兩處 undefined 皆為珊瑚色

launch/patient:病人是誰

launch/patient 長得像有斜線,但它不是昨天那種三個部分的寫法。斜線後面接的是 context 的種類,不是資源型別。

它跟另一個更短的 launch 是一對。差別在哪,day05 那兩種啟動模式的圖可以直接搬過來用:

launch launch/patient
用在 EHR Launch Standalone Launch
意思 請給我這次啟動帶進來的 context 請讓使用者選一位病人
context 哪來 launch 參數換回來的 授權過程中選的,或依登入身分決定

launch 用在 EHR Launch 那條路上。它是在說「我有 launch 參數,請把它代表的 context 給我」。context 在醫師點下去那一刻就決定了,app 沒得選。

standalone 沒有那個參數。所以 launch/patient 是在說「請在授權流程裡讓使用者選一位」。也就是我們前幾天看到的 Patient Login 畫面。

前面說的協商在這裡有個具體例子:launch/patient 在 EHR Launch 底下只是 hint。我們走 standalone,送就對了。

這台的第一列不符規範

回頭看表上第一列。只送 patient/*.rspatient 欄位是空的。

規範這裡說得很明確。app 要了限定單一病人的資源型 scope,而伺服器核發了它。這時候伺服器就該建立一個 patient context。 SHALL,不是建議。

規範給了伺服器兩條路。一條是拒絕這種沒帶 launch 的請求。另一條是自己推定 launch/patient,把病人選擇流程補上。

這台兩條都沒走。照收 patient/*.rsscope 欄位原樣回傳,然後不給 patient context。

你不太會真的這樣送,要病人資料本來就會配 launch/patient。但換一台伺服器,同一組 scope 可能被拒絕。

openid 與 fhirUser:使用者是誰

這兩個是 OpenID Connect 那一套進到 SMART 裡的部分。它們打開的是 id_token

access_tokenid_token 常被搞混,講清楚一次:

兩者對照圖,米色底,標題「access_token 給伺服器,id_token 給你」,副標「同一次 token response 回來的兩張,用途相反」。畫面左右各一張白色圓角卡片,兩張卡片上緣都是深藍色標頭。左卡標頭是等寬字白色的 access_token,底下一行淺色小字寫著給 FHIR 伺服器看的;右卡標頭是等寬字白色的 id_token,底下一行淺色小字寫著給你的 app 看的。兩張卡片內各有兩個區塊,區塊之間以一條淺色分隔線隔開。左卡第一個區塊的標籤是灰色小字「回答什麼」,值為粗體的這個請求准不准;第二個區塊標籤是「你要怎麼處理」,底下一個珊瑚色圓形叉號接珊瑚色粗體大字的不要解開,再下面一行灰字寫著對 app 而言是不透明字串。右卡第一個區塊標籤同樣是「回答什麼」,值為粗體的使用者是誰;第二個區塊標籤是「你要怎麼處理」,底下一個綠色圓形勾號接綠色粗體大字的先驗證再讀,再下面一行灰字寫著驗完簽章才讀裡面的 claims。圖片最下方一行灰字:id_token 來自 OpenID Connect 那一套,access_token 是 OAuth 2.0 本來就有的

day09 提過這台的 access_token 剛好也是 JWT,但那是實作細節,SMART 沒有要求它是。id_token 裡面有一個 fhirUser claim,指向操作者的 FHIR 資源:

patient-standalone   fhirUser = Patient/d48ac962-78c6-46cf-ba33-a24771bfa0e4
provider-standalone  fhirUser = Practitioner/4826865

同一個 claim,因為登入的人不同而指向不同的資源型別。這是「這個 app 現在是誰在用」的答案,day13 會整篇拆它。

這台要兩個一起送

實測到一個跟規範不一樣的地方,得說清楚。

規範的寫法是 openid 就會拿到 id_tokenfhirUser 的作用是讓你能從 claim 取得使用者的資源位址。也就是說這兩個是不同層次的東西。前者決定「發不發」,後者決定「裡面有沒有那個資訊」。

但這台 Launcher 不是這樣:

送出的 scope 有沒有 id_token
openid 沒有
patient/*.rs openid 沒有
patient/*.rs fhirUser 沒有
openid fhirUser

單獨送任一個都不發,兩個一起才發。

實務上這不太會咬到你,要使用者身分時你本來就會兩個一起送。你在別的伺服器上只送 openid 卻拿到 id_token,那不是它壞了,那才是規範寫的行為。

offline_access 與 online_access:token 過期之後

這兩個都在要同一樣東西,refresh_token。差別在它能活多久。

規範的定義是這樣:

  • offline_access:拿一個 refresh token,不管使用者在不在線上,只要授權伺服器和使用者允許,它就一直可用
  • online_access:拿一個 refresh token,只在使用者還在線上的期間可用

差別是語意上的,不是格式上的。我兩個都送了一次,這台都給了 refresh_token,從單次回應完全看不出差異。因為「使用者離線」這件事要等到 session 結束才觀察得到。

所以這裡我只能說規範怎麼定義,不能拿實測結果宣稱兩者等價。

該用哪一個,看你的 app 什麼時候需要資料:

  • 使用者開著頁面在用,關掉就算了 → online_access
  • 背景同步、排程通知、使用者早就離開了還要繼續跑 → offline_access

火線超人是後者的典型場景,它要在使用者沒有打開 LINE 的時候推播檢驗結果。但它的清單裡沒有 offline_access,這就是開頭說的那件事。

fhirContext:病人與就診事件以外的東西

patientencounter 是固定欄位,但 context 不會只有這兩種。醫師從一張影像報告點開你的 app,那張報告本身也是 context。

SMART v2 為此加了 fhirContext。這是一個陣列,放固定欄位以外的資源參照:

{
  "patient": "123",
  "fhirContext": [{ "reference": "ImagingStudy/123" }]
}

每個元素至少要有 referencecanonicalidentifier 其中一個,可以再帶 typerole

Patient 和 Encounter 原則上不放進來,它們有自己的欄位。例外在 role 上。role 沒填等同於 "launch",這種不准放。但 role 給的不是 "launch" 呢?那就可以放進陣列裡。

fhirContext 是選用的,這台 sandbox 沒有回。會用到的多半是 EHR Launch 底下的臨床工具。standalone 主線用不上,知道有這條路就好。

跟著做:一次拿掉一個,看欄位怎麼消失

起點:day10 之後的 smart-appauth.jsSCOPElaunch/patient patient/Observation.rs,能走完一次授權。

產出:你會親眼看到三個非資源型 scope 各自對應哪個欄位。那些欄位都在 token response 上。而且你會知道少送一個會讓程式在哪裡爆掉。app.js 只多一行觀察用的 console.log,六個檔案的組成沒有變。day12 會把 app.js 整份換掉。

第一步,先把欄位印出來

app.js 那幾行 console.log 底下加一行:

console.log('token response 的欄位:', Object.keys(token))

這一行是今天的觀察窗。前面幾天我們只印自己要的欄位,看不到「有哪些欄位不見了」。

第二步,跑一次基準

SCOPE 保持 day10 的值不動,走完授權,看 console:

token response 的欄位: ['access_token', 'expires_in', 'need_patient_banner',
  'patient', 'scope', 'smart_style_url', 'token_type']

七個欄位,patient 在裡面。

第三步,拿掉 launch/patient

export const SCOPE = 'patient/Observation.rs'

重跑。這次登入畫面之後,patient 那個欄位不見了,而且 app.js 最後那行畫面文字會變成:

授權完成,patient id 是 undefined

這就是前面說的那個 undefined。它不會拋錯,程式看起來跑完了,錯誤要到你拿它去組查詢網址才會出現。

第四步,一次加一個回來

依序改成這三個值,每次重跑一次,只看欄位清單怎麼變:

'patient/Observation.rs openid fhirUser'      // 多出 id_token
'patient/Observation.rs offline_access'       // 多出 refresh_token
'launch/patient patient/Observation.rs openid fhirUser offline_access'  // 九個全到

最後那一組就是 day09 給你的那串的 Observation 版本,兜了一圈回來。

預期結果:欄位清單隨 scope 增減,這台的三組各對一個欄位。而且你會發現少送 launch/patient 時程式不會報錯,只會安靜地拿到 undefined

執行結果圖,淺灰綠底色,標題「五次重跑,三個欄位怎麼進出」,副標「只換 scope,其他參數完全不動」。中央一個白色圓角面板,左上角三個灰色小圓點代表視窗,圓點右邊一行灰字寫著腳本直接打端點,沒有經過瀏覽器,所以這個版型沒有網址列。圓點下方一條淺灰橫帶寫著 token response 有沒有這個欄位。橫帶底下是一張五列的表,表頭左欄是灰字「送出去的 scope」,右邊三欄是等寬字的欄位名 patient、id_token、refresh_token。每一列左半上方是灰色小字的步驟標籤,下方是等寬字的 scope 字串,右邊三欄各放一個膠囊狀標記,有是淺綠底深綠字,無是淺灰底灰字。第一列標籤基準,scope 是 launch/patient patient/.rs,三欄依序為有、無、無。第二列整列淺珊瑚底、左緣一條珊瑚色直條,標籤是拿掉 launch/patient,scope 是 patient/.rs,patient 那一欄是珊瑚底紅字的無,另外兩欄是灰色的無。第三列標籤加回 openid fhirUser,scope 是 patient/.rs openid fhirUser,三欄依序為無、有、無。第四列標籤加回 offline_access,scope 是 patient/.rs offline_access,三欄依序為無、無、有。第五列標籤全送,scope 是 launch/patient patient/.rs openid fhirUser offline_access,三欄全部是有。圖片下方三條綠色圓形編號註記:一,珊瑚色那一格是第三步,patient 從有變成沒有;二,五組都順利換到 token,差別只在回應少了哪幾個欄位;三,實跑的資源型 scope 是 patient/.rs,跟著做把它換成 patient/Observation.rs

常用組合表

常見的幾種組合整理成一張表,照場景挑:

場景 scope 組合
病人自己的健康管理 app,只讀 launch/patient patient/Observation.rs patient/Patient.r
同上,要知道使用者是誰 再加 openid fhirUser
同上,要背景同步 再加 offline_access
醫師診間工具,從 EHR 啟動 launch user/Observation.rs openid fhirUser
夜間批次對接 system/Observation.rs,走 client_credentials,沒有使用者

挑的方法就兩句:資源型的照 day10 反推,不要用 *;非資源型的照欄位需求加,要哪個欄位就加哪個。其中 offline_access 最該想清楚。一張長期有效的 refresh token 等於一把長期有效的鑰匙。怎麼存是 day14 整篇的題目。

小結

今天這四個 scope 沒有一個是在要資料。它們要的是 token response 上的欄位:launch/patientpatientopenidfhirUserid_tokenoffline_accessrefresh_token

在這台是一對一,少送就沒有。而且沒有的時候程式不會報錯,只會給你 undefined

這台跟規範對不上的地方今天出現兩次。一是 openid 要配 fhirUser 才發 id_token。二是核發了 patient/*.rs 卻不給 patient context。處理方式一律是照規範寫、照實際行為測。

火線超人那份清單裡沒有 offline_access,代價是 token 過期就得重新授權一次。而這件事光看那份清單就讀得出來。

那麼,launch/patient 換回來的 patient 欄位,內容只是一串 id:

d48ac962-78c6-46cf-ba33-a24771bfa0e4

畫面上總不能顯示這個。明天就把這串 id 變成一個真正的病人。


上一篇
Day10 - 一個 scope 分成三個部分
下一篇
Day12 - 解析 Launch Context
系列文
SMART on FHIR 開發之路:30 天做一個跨醫院的 app13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言