iT邦幫忙

2026 iThome 鐵人賽

DAY 29
0

本文同步發表於個人部落格:SMART 那一層改過四次


day12 那個 launch context,火線超人裡面是一支叫 Smart::LaunchContextService 的服務在處理。2025-11-16 寫完,到今天大約九個月,只有 commit 一次。

處理 OAuth 授權的是另外兩支服務。同樣這九個月,加起來卻改了十五次。

我從那十五次裡挑四次來看看。前三次改的都是我當初沒照規範做的地方,而第四次有點不大一樣。

第一次:端點寫死且寫錯

2026-01-24,commit 815fd78。原本 token 端點是這樣寫的:

token_endpoint = "#{@fhir_server_url}/auth/token"

看起來沒什麼問題對吧,但問題是 @fhir_server_url 後面本來就有帶 /fhir,再接上去就變成 /fhir/auth/token。那個位址上什麼都沒有。

commit 訊息寫得很清楚:Fix token endpoint path (was using /fhir/auth/token instead of /auth/token)

後來才改成不自己亂拼湊,先問 FHIR 伺服器再說。

day06 講過,問端點有兩條路。一條是 .well-known/smart-configuration,SMART App Launch 2.0 的作法。另一條更早,從 /metadata 的 CapabilityStatement 裡挖,那個擴充叫 oauth-uris。我當時走的是後面那條。

哪一條都行,反正都比自己亂拼字串強多了。壞就壞在我根本沒問。

同一個 commit 還補了另一樣東西。原本那個換 token 的方法簽章是 exchange_code_for_token(code, redirect_uri),改成了 exchange_code_for_token(code, redirect_uri, code_verifier: nil)

也就是說,在還沒換之前 PKCE 的 code_verifier 根本沒送給授權伺服器。day08 講過那組亂數怎麼用。授權時送的 code_challenge 是拿 code_verifier 算出來的,換 token 才送原本那組亂數。伺服器兩邊對得起來,才知道是同一個程式。而我當時只做了前面那一步。

第二次:EHR Launch 少帶兩個參數

2026-03-23,commit f6cf536,補的是 launchaud

commit 內文寫得很完整:The SMART on FHIR EHR Launch flow requires the launch token and aud (FHIR server URL) to be forwarded in the authorization request. Without these, the SMART Launcher returns "Invalid launch options".

Invalid launch options 這句話你在 day06 看過。當時那張圖講的是自己拼授權網址,會把 sim/ 那一段拼掉。拼出來的網址是合法的,但就是錯的,按下去就跳這句。

這次我拿到同一句話,成因卻不一樣。網址本身沒拼錯,是該帶的兩個參數沒帶。

aud 在 day05 有整整一節,重點是兩種模式都要送。它回答的是「這張 token 要拿去打哪一台 FHIR 伺服器」。launch 就是 EHR Launch 專屬的那根棒子。day12 用 EHR 模式跑的時候,那個值有 124 字元。

規範講過,我自己的系列也講過,程式碼還是漏了。

第三次:secret 送錯地方

2026-03-27,commit 7f7f587。我原本把 client secret 放在 POST 的表單內容裡送出去:

body[:client_secret] = @client_secret if @client_secret.present?

後來從表單內容裡刪掉,搬到 Authorization 標頭:

body.delete(:client_secret)
headers['Authorization'] = "Basic #{Base64.strict_encode64("#{@client_id}:#{@client_secret}")}"

理由 commit 訊息裡有:Use HTTP Basic Authentication header for confidential client token exchange per SMART on FHIR spec。同一句還註明它修掉的錯誤是 401 Basic authentication is required

這條你的 app 遇不到。day07 講過,瀏覽器裡的檔案人家打得開。所以純前端一律是 public client,手上根本沒有 secret 可送。

火線超人有後端,是另外那一種。手上有 secret 的話就多一個問題要回答:這東西送在哪裡。不是隨便找個欄位塞進去就算數。

第四次:授權網址太長

2026-03-27,跟上一次同一天,commit ce6af18

commit 內文是這樣寫的:LINE URI action has a 1000 character limit. SMART Launcher's authorization URLs exceed this due to long sim paths.

這次的狀況是這樣。火線超人把授權網址掛在 LINE 訊息的按鈕上,使用者點下去才開始授權。而 LINE 對那種按鈕的網址有硬性上限,一千個字元。sandbox 那串 sim/ 路徑本來就長,湊一湊就超過去,那則訊息根本送不出去。

後來改成把完整的授權網址存進資料表,讓使用者點一個短的位址,再由後端導過去。

這一次跟前三次不一樣。前三次是規範白紙黑字寫著,而我沒照做。這次規範根本沒寫,是通路自己的限制撞上了 sandbox 的網址長度。

分得出這兩種差別,遇到問題你才知道要去哪裡查。前三次翻 SMART App Launch 就有,第四次翻到爛也翻不到。

快取那件事不是設計出來的

第三次那個 commit 還順手做了一件事,跟 day22 有關。

它把抓回來的 metadata 快取起來,存活時間一小時。同時給那支請求設了 3 秒的 timeout,在那之前程式碼裡根本沒設過。

理由 commit 訊息裡寫著,這是為了避免重複抓太慢、把反向代理拖到逾時。火線超人是 LINE bot。使用者每傳一則訊息,LINE 就打一次 webhook 進來。那支請求裡要用到授權端點,於是每次都去抓一輪 metadata。抓到後來,反向代理等不下去。

所以那個快取根本不是設計階段想到的,是被逾時逼出來的。隔天的 494e0ad 才把端點從「每次抓」改成存進資料表欄位,那次 o_auth2_service.rb 一口氣少了 99 行。

day22 帶你做的那份快取設 24 小時。我這個一小時是當場止血訂的,兩個數字都不是標準規定的,是各自環境裡挑出來的。

四個步驟的時序流程圖,米色底,標題「快取是被逾時逼出來的」,副標「不是設計階段想到的,是出事之後才加的」。畫面由上而下是四塊圓角橫條,每塊左上角有一個圓形編號,塊與塊之間以一支向下的短箭頭相連。第一塊是淡粉底加淺珊瑚外框,編號 1 是珊瑚底白字,標題以暗紅色粗體寫「每次授權都重抓一次 metadata」,同一列最右側以淺褐色小字寫「沒有快取,也沒設 timeout」,其中後半句加粗;塊內一行灰褐色小字寫「要用授權端點時就去打一次」,後面接等寬字的 /metadata。第一塊與第二塊之間是一支淺珊瑚色的向下箭頭,沒有標籤。第二塊同樣是淡粉底加淺珊瑚外框,編號 2,標題暗紅色粗體「抓太慢,webhook 被拖到逾時」,最右側淺褐色小字「問題浮出來的地方」;塊內一行灰褐色小字寫「重複抓 metadata 太慢,把 webhook 那支請求拖過了反向代理的等待上限」,其中「webhook 那支請求」以暗紅色粗體標示。第二塊與第三塊之間是一支淺藍色向下箭頭,右邊標灰字「止血」。第三塊改成深藍色實心色塊,編號 3 是淺藍底深藍字,標題白色粗體「加快取,同時把等待時間縮短」,最右側以等寬白字寫 7f7f587,後接淺藍灰字的 2026-03-27;塊內兩個並排的深藍方塊,左邊方塊上行淺藍灰小字「metadata 快取存活時間」、下行白色粗體「一小時」,右邊方塊上行淺藍灰小字「同時給那支請求設了」、下行白色粗體「3 秒 timeout」。第三塊與第四塊之間是一支青綠色向下箭頭,右邊標灰字「隔天才變成設計」。第四塊是白底、左緣一條青綠色直條,編號 4 是青綠底深藍字,標題深藍粗體「端點不再每次抓,改成存進資料表欄位」,最右側等寬深藍字 494e0ad 後接灰字 2026-03-28;塊內兩個並排的淺灰方塊,左邊上行灰字「設定資料表新增的欄位」、下行深藍粗體「iss_host 等 3 個」,右邊上行灰字「那次 o_auth2_service.rb」、下行深藍粗體「少了 99 行」。圖片最下方一行灰字註記:兩個 commit 的日期與數值取自火線超人 repo,2026-08-29 查證

沒改過的那一邊

四次講完,來看另一邊。

開頭那支 Smart::LaunchContextService,89 行,九個月一個 commit。中間經過多租戶重構、跨院整合、兩次主機遷移,它一行都沒被動到。

再看開頭說的那兩支。它們不是並排的,是疊起來的。

裡面那層叫 Fhir::OAuth2Service,121 行,貼著協定走。它負責組授權網址跟換 token,總共 7 個 commit。最後一次改是 2026-03-28,到盤點日大概五個月沒動。

外面那層叫 FhirOauthService,348 行,處理綁定跟使用者狀態。它一路改到 2026-06-16 的多租戶隔離,總共 8 個 commit。

這兩支不是新舊版本的關係,是包覆關係。外層那支的第 297 行就在呼叫裡面那支。

三支服務的對照圖,米色底,標題「越貼著規範的改得越少」,副標「同一個 repo、同一段九個月,三支服務的行數與 commit 數」。畫面由上而下是三張圓角長條卡片,寬度相同,左側都有一條垂直色帶。第一張卡片色帶是青綠色,左側等寬深藍粗體寫 Smart::LaunchContextService,底下灰色小字「SMART launch context,照規範做」;卡片中段兩個並排的淺灰底方塊,左邊方塊上行灰字「行數」、下行深藍粗體 89,右邊方塊上行灰字「建立日」、下行深藍粗體 2025-11-16;卡片最右側是一個大字的深藍色 1,右邊接灰色小字「個 commit」,再下一行以青綠色小字寫「九個月沒改」。第二張卡片色帶是深藍色,左側等寬深藍粗體寫 Fhir::OAuth2Service,底下灰字「協定層,組授權網址與換 token」;中段兩個方塊分別是行數 121 與建立日 2025-11-16;最右側大字深藍色 7 接「個 commit」,下方兩行深藍灰小字,上行「最後一次改 2026-03-28」、下行「約五個月沒動」。第三張卡片色帶是珊瑚色,左側等寬深藍粗體寫 FhirOauthService,底下灰字「業務層,處理綁定與使用者狀態」;中段兩個方塊分別是行數 348 與建立日 2026-01-23;最右側大字珊瑚色 8 接「個 commit」,下方兩行珊瑚色小字,上行「一路改到 2026-06-16」、下行「的多租戶」。三張卡片右側之外有一條由上往下的灰色細箭頭,箭頭上方標灰字「離規範越遠」,下方標灰字「改得越多」。三張卡片下方是一塊佔滿寬度的深藍色圓角色塊,左上角淺藍灰小字寫「第二張與第三張不是新舊版本,是包覆關係」,底下一行白字寫「FhirOauthService 的第 297 行呼叫 Fhir::OAuth2Service」,右側一行淺藍灰小字寫「兩支同時存在,各自負責一層」。圖片最下方一行灰字註記:行數與 commit 數為 2026-08-24 盤點日的數字

規格文件那邊也對得起來。我這個專案的每一項功能都有一份規格檔,檔頭記著最後更新日期。SMART 授權相關的有四份,更新日期都停在 2026 年 3 月,最晚那份是 3 月 28 日。

那四份講的東西,剛好就是第二幕到第三幕教你的:

  • 每台伺服器各自的 credentials
  • 每台各自的 scope
  • fhirUser 身分
  • 端點快取

講白一點就是這樣。最貼著規範的那一支九個月沒動,包著它的業務層改到今年六月。而我挑出來的前三次,改的都是當初沒照規範做的地方。

這不是說規範比較高明。是說規範已經被很多人踩過坑了,而我的判斷只經過我一個人。

你在鐵人賽這幾天學到的,哪些會過期

照規範做的耐久,這件事也可以拿來看你自己。

你在鐵人賽這幾天學到的東西其實分兩部分。一部分是規範講死的,另一部分是我在這個系列裡幫你選的。

規範講死的那些,你都走過了:

  • 端點要跟伺服器問,不能自己拼
  • code_challengecode_verifier 要成對送出去
  • aud 要送,兩種 launch 模式都要
  • scope 怎麼組,fhirUser 是誰
  • 分頁要照著 next

這些東西不會變。換一台伺服器還在,換一個函式庫還在,換一種語言也還在。火線超人是 Rails,你的是瀏覽器原生 JS,兩邊要對的是同一份規範。

另一部分是我幫你選的,規範完全沒規定:

  • token 存 sessionStorage
  • discovery 快取 24 小時
  • 合併排序拿來源代號當次要鍵
  • 整個系列用 fhirclient

我選的那些你可以挑別的,而且你大概真的會挑別的。day22 那個 24 小時跟火線超人那個一小時,剛才講過了,兩個都不是標準規定的。

會過期的是我選的那些。真正要記住的是規範講死的,還有它們要去哪裡查。

跟著做:開著 Network 跑最後一次授權

最後一次動手。這次不用改程式碼,就是看它在做什麼。

打開你第四幕做出來的專案,開 DevTools 的 Network。記得勾 Preserve log,不然導轉之後那些紀錄會被清掉。然後按下連線,跑一次完整授權。

有四件事你會親眼看到,前三件正是我當初漏掉的。

第一,按下連線之後的第一支請求。 它會是 .well-known/smart-configuration。端點是問來的,不是拼出來的。我這次跑第一支還逾時了一次,重試才回 304。

第二,導去授權伺服器那一支的網址。 裡面有 audcode_challengecode_challenge_method=S256。但沒有 launch。你的 app 走的是 Standalone Launch,那個值只有 EHR 啟動時才有。

第三,導回來之後那支 POST。 打去 /auth/token,表單內容裡有 code_verifier

第二件跟第三件對著看。授權網址帶出去的是 code_challenge,換 token 才帶 code_verifier。前者是拿後者算出來的,伺服器兩個都收到才對得起來。我第一次修正補的就是後面那一步。

第四,同一支 POST 裡沒有的東西。 沒有 client_secret,Request Headers 裡也沒有 Authorization。因為你是 public client,手上根本沒有 secret 要送。

Network 面板的執行結果圖,米色底,標題「四件事,在你自己的 Network 裡」,副標「開 DevTools 的 Network,勾 Preserve log,跑一次完整授權」。畫面主體是一個白底圓角的視窗卡片,卡片頂端一列淺灰工具列,左側三個灰色圓點加深灰字「Network」,最右側灰字「Preserve log」後接藍色粗體「已勾選」。工具列下方是一列灰底的欄位標題,由左至右是 Method、Name、Status。底下四列請求,每列最左邊有一個圓形編號。第一列編號 1 是珊瑚底白字,Method 欄等寬字 GET,Name 欄等寬字 .well-known/smart-configuration,底下一行灰色小字「端點是問來的,不是拼出來的」後接珊瑚色粗體「我這次第一支逾時,重試才成功」,最右側 Status 欄以珊瑚色分兩行寫「逾時」與「重試 304」。第二列編號 2 是深藍底白字,Method 欄 GET,Name 欄 auth/authorize?…,底下灰色小字「網址裡有」後接三個深綠色粗體的等寬字 aud、code_challenge、code_challenge_method=S256,再接灰字「沒有 launch」,最右側 Status 欄深綠色 302。第三列編號 3,Method 欄 POST,Name 欄 auth/token,底下灰字「表單內容五個欄位,其中一個是」後接深綠色粗體 code_verifier,Status 欄 200。第四列編號 4,Method 欄 POST,Name 欄 auth/token 後接一段灰字「同一支,看它沒有什麼」,底下灰字「表單裡沒有」接珊瑚色粗體 client_secret、再接「標頭裡沒有」與珊瑚色粗體 Authorization,Status 欄 200。視窗卡片下方是一塊佔滿寬度的深藍色圓角色塊,最上一行淺藍灰小字寫「第二支與第三支對著看,那是 PKCE 的兩端」,底下左右兩個較淺的藍色方塊,左邊上行淺藍灰小字「授權網址帶出去的」、下行白色等寬粗體 code_challenge,右邊上行「換 token 才帶的」、下行白色等寬粗體 code_verifier,兩個方塊中間以淺藍色小字分兩行寫「算出來的」「是前者」。色塊最下一行淺藍灰字寫「我第一次修正補的就是右邊這一步」,後接珊瑚色粗體「在那之前只送了左邊」。圖片最下方一行灰字註記:2026-08-29 以 examples/day24-cross-server 對 A 醫院那組設定實跑,逾時與狀態碼為當次的值

四件事看完,你會發現一個共同點。前三件都不是 fhirclient 發明的,是規範規定的,它只是替你照做。第四件更單純,你是 public client,本來就沒有 secret。所以哪天你不用這個函式庫了,要對的還是同一份規範。

commit 訊息沒說的

誠實列一下。

四次修正裡,只有第二次跟第四次的 commit 內文提到 SMART Launcher。另外兩次是在哪一台伺服器上發現的,我當時沒寫。

修正的間隔為什麼是那樣,1 月一次、3 月三次,commit 訊息裡也看不出來。

還有第一次那個寫死的端點,在改掉之前流程到底走不走得完。commit 訊息只說路徑錯了,沒說在那之前換 token 成功過沒有。

這三件事我不補,因為補了就是編。

小結

我改過的四次,括號裡是系列講過它的那幾篇。

  1. 端點寫死而且寫錯(day06),同一個 commit 才把 code_verifier 送進 token 交換(day08)
  2. EHR Launch 漏帶 launchaud,拿到的錯誤跟 day06 那張圖同一句,成因不同(day05、day12)
  3. client secret 放在 POST 內容裡,而不是 Authorization 標頭(day07)
  4. 授權網址超過通路的一千字元上限,這條規範沒寫(day26)

前三條翻規範查得到,第四條查不到。而九個月沒被動過的那一支,做的正是規範講死了的事。

火線超人得過獎,但它的 SMART 那一層不是一開始就對的,是我改了四次才補齊的。你第四幕那個 app 一開始就沒踩到前三個坑。不是因為你比較厲害,是因為 fhirclient 照規範做了。

所以明天那篇不會教你新東西。它給的是規範的清單,也就是今天講的那些不會過期的要去哪裡查。


上一篇
Day28 - 上線檢查清單
下一篇
Day30 - 讓更多人進來
系列文
SMART on FHIR 開發之路:30 天做一個跨醫院的 app30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言