本文描述一個匿名化的企業實作。已刻意移除或泛化公司名稱、內部網域、存放庫識別碼、工單編號以及專有控制名稱。
多重驗證(Multi-factor authentication)通常被視為登入問題:輸入密碼、接收驗證碼、確認身分。
針對本案例所描述的系統,上述模式並不足以因應需求。
此產品運行於店內環境,同一台平板可能在單次交易中被多人使用:
- 員工啟動流程;
- 主管核准或協助;
- 顧客在自己的裝置上檢視並簽名。
挑戰不只是證明使用者知道六位數驗證碼。我們需要建立一個安全的、短暫的工作階段交接機制,讓共用的店內工作階段與顧客的個人手機連結,同時不洩露後續的簽名工作階段,也不允許多部裝置同時認領同一筆交易。
本文將說明該平台的架構、安全模型、取捨考量,以及實際上線的做法。
真正的問題:安全的工作階段交接
流程從共用的平板開始。在某個階段,顧客需要繼續在自己的手機上完成部分程序。
因此平台必須回答幾個問題:
- 手機如何證明自己就是站在員工面前的那位顧客?
- 共用平板如何知道正確的手機已認領正確的工作階段?
- QR code 被掃描兩次時會發生什麼事?
- 如何防止工作階段識別碼與權杖出現在網址、瀏覽器歷史、紀錄檔或 referrer header 中?
- 驗證成功時如何立即通知手機?
- 如何確保單次使用的簽名網址在驗證完成前不會被曝光?
這些限制把看似單純的 MFA 功能轉變為分散式系統問題,涉及身分識別、即時通訊、並行控制、邊緣傳遞、基礎架構以及營運控管。
高階架構
解決方案使用了兩個主要應用程式元件:
- 顧客手機端採用 React 與 TypeScript 開發的單頁應用程式;
- 後端使用 AWS AppSync、Lambda 與 DynamoDB 建置的無伺服器 GraphQL API。
基礎架構還包含 CloudFront、S3、API Gateway、Route 53、ACM、WAF、Terraform、集中式記錄、APM、合成監控,以及自動化部署流程。
┌─────────────────────────────┐
│ Shared in-store tablet │
│ Employee / manager workflow │
└──────────────┬──────────────┘
│ create handoff session
▼
┌─────────────────────────────┐
│ GraphQL API │
│ AppSync + Lambda │
└──────────────┬──────────────┘
│ persist state
▼
┌─────────────────────────────┐
│ DynamoDB │
│ session + short-link data │
└─────────────────────────────┘
Shared tablet displays QR
│
▼
┌─────────────────────────────┐
│ Customer phone │
│ React SPA via CloudFront │
└──────────────┬──────────────┘
│ claim + subscribe
▼
┌─────────────────────────────┐
│ GraphQL API │
│ verification + status push │
└─────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode
架構刻意採用無伺服器設計。工作負載為突發性,使用者流程短暫,且大多數操作都是小型狀態轉換,而非長時間運算。
端到端工作階段交接流程
1. 建立工作階段交接
店內應用程式建立一筆短暫的交接紀錄,包含交易內容、後續簽名網址、到期資訊,以及 PENDING 狀態。
簽名網址被視為敏感且單次使用的資料,會儲存在內部,但在交接工作階段仍處於待處理狀態時,不會回傳給顧客端應用程式。
2. 產生短網址 QR code
API 產生與交接紀錄關聯的短代碼。店內平板將此短網址以 QR code 形式呈現。
顧客使用手機掃描。
3. 安全地初始化手機工作階段
QR 請求會經過 CloudFront 以及小型重新導向端點。
重新導向端點不會把工作階段識別碼與權杖放在查詢參數,而是回傳具備以下設定的短效 cookie:
-
Secure; -
SameSite=Strict; - 極短的到期時間。
手機應用程式讀取這些值、設定 GraphQL 用戶端,然後繼續工作階段交接流程。
此做法避免了以下管道暴露憑證:
- 瀏覽器歷史;
- 存取記錄;
- 複製的網址;
- 分析工具;
- referrer header。
4. 認領工作階段交接
手機呼叫 claimSession mutation。
後端產生:
- 六位數安全碼;
- 唯一的工作階段識別碼。
安全碼會顯示在顧客手機上,並由員工在共用平板上讀取。
平台採用最後掃描者優先的模式。若另一支手機掃描同一 QR code,新的認領動作將取代先前的認領。
這可防止兩部裝置同時持有同一個有效的工作階段交接。
5. 驗證安全碼
員工在店內應用程式輸入六位數安全碼。
後端將驗證視為狀態轉換:
- 正確:
PENDING → VERIFIED; - 錯誤且尚有嘗試次數:遞減嘗試次數;
- 最後一次錯誤:
PENDING → CANCELED; - 逾時:
PENDING → EXPIRED。
這些轉換使用 DynamoDB 條件寫入,而非讀取-修改-寫入序列。資料庫本身成為並行控制的基礎元件。
這很重要,因為兩次驗證請求或兩個競爭的認領動作不可能同時成功。
6. 即時推送結果
手機透過 GraphQL subscriptions 訂閱交接狀態更新。
當驗證成功時,AppSync 會立即將新狀態推送至手機。
不需要輪詢迴圈。
手機接著執行最後一次已驗證的讀取。此時 API 才會暴露單次使用的簽名網址。
應用程式隨即將顧客重新導向至簽名體驗,並使交接紀錄到期失效。
為什麼使用 GraphQL subscriptions 而非輪詢?
輪詢比較容易說明,但會產生不必要的流量、較慢的回饋,以及更多生命週期邊緣案例。
產品需求本質上是事件驅動的:
當店內工作階段驗證此交接工作階段時,通知這支手機。
GraphQL subscriptions 提供了:
- 低延遲的狀態變更;
- 依交接內容內建篩選;
- 透過用戶端函式庫管理 WebSocket 重新連線;
- 不需要自訂輪詢間隔或重試排程器。
前端仍需處理行動裝置特有的失敗模式:作業系統在使用者鎖定手機或切換應用程式時,常會暫停瀏覽器分頁。
當分頁再次可見時,應用程式會重新擷取交接狀態。這可涵蓋 WebSocket 被暫停而錯過最終事件的情況。
六位數安全碼的雙重用途
最重要的設計決策之一,是將六位數值同時作為:
- 人類可讀的 MFA 挑戰;
- 存取交接狀態所需的伺服器端能力。
讀取與訂閱篩選都需要此安全碼。當裝置被取代時,不會再傳回安全碼,且在狀態到達 VERIFIED 之前,後續簽名網址會保持隱藏。
這消除了產生第二個不透明工作階段權杖的需求,同時保留欄位層級授權。
也減少了必須產生、儲存、同步與失效的憑證數量。
分層安全模型
平台採用縱深防禦,而非只依賴 MFA 安全碼。
邊緣保護
CloudFront 與區域端點受到 AWS WAF 保護。受管理的規則群組分階段導入,先啟用高風險規則,而容易產生誤判的群組則先維持在監控模式。
傳輸與身分
所有流量均使用 HTTPS。GraphQL API 使用自訂 Lambda authorizer,驗證 OAuth/JWT 權杖、發行者、簽章、到期時間、操作層級範圍,以及授權權重。
未知操作會失敗關閉。
合約強化
GraphQL 介面在部署環境中停用 introspection,並限制查詢深度與 resolver 數量。
欄位層級授權
Resolver 在回傳交接狀態前會檢查安全碼。
僅在交接工作階段已驗證時,才會包含簽名網址。
對於已被取代的訂閱,resolver 不會回傳資料,而非揭示工作階段變更的原因。
憑證衛生
敏感的交接憑證不會透過網址傳遞。記錄排除個人可識別資訊與驗證密碼。保留關聯識別碼以供除錯,但不洩漏工作階段資料。
刻意保持前端精簡
顧客端應用程式為單一用途流程,因此前端避免不必要的框架複雜度。
使用了:
- React;
- 嚴格模式的 TypeScript;
- Vite;
- React Context 與本地 hooks;
- 受管理的 GraphQL 用戶端;
- 共用的企業設計系統。
沒有 router,也沒有全域狀態函式庫。
應用程式只有幾個狀態:
- 載入或認領中;
- 顯示 MFA 安全碼;
- 顯示錯誤或終止狀態;
- 驗證後重新導向。
這種簡潔降低了 bundle 大小、攻擊面、升級負擔,以及認知負荷。
從第一天就使用基礎架構即程式碼
所有基礎架構皆以 Terraform 定義。
API 存放庫不僅佈建後端,也佈建前端所需的共用基礎架構:
- AppSync;
- Lambda functions;
- DynamoDB;
- S3;
- CloudFront;
- API Gateway;
- DNS 與憑證;
- WAF;
- 監控資源。
前端存放庫產生不可變的建置成品,但不擁有獨立的基礎架構堆疊。
這為平台拓撲建立了單一事實來源,同時允許應用程式與 API 獨立演進。
建置一次,部署多處
前端不會在建置時嵌入環境特定設定。
而是從部署的主機名稱判斷環境。因此同一個成品可不經修改地依序升級至開發、QA、預生產與生產環境。
此模式帶來兩項好處:
- 生產環境收到的是先前已測試過的相同成品;
- 設定成為部署層面的考量,而非建置層面的考量。
交付管線包含:
- linting 與自動化測試;
- 靜態分析;
- 基礎架構驗證;
- 安全性掃描;
- 語意版本控制;
- 不可變成品發佈;
- 環境核准;
- 生產環境變更管理整合。
可觀測性是產品的一部分
營運可見性不是在上線後才加入。
平台包含:
- 結構化 JSON 記錄;
- 關聯識別碼;
- 命名業務事件;
- 集中式記錄轉送;
- APM 儀器;
- Lambda、GraphQL 與交易健康狀態的儀表板;
- 延遲、錯誤率與限流警示;
- 合成健康檢查;
- 環境特定升級規則。
監控設定存放在獨立的 Terraform 狀態中,因此可在不修改主要應用程式基礎架構的情況下進行變更。
這種分離很有用,因為警示的演進速度往往與應用程式資源不同。
真實的取捨:合規優先於較便宜的 API 選項
其中一個較具啟發性的決策涉及 API Gateway。
最初實作採用較新的 HTTP API,因為它較簡單且成本較低。在合規作業中,團隊確認該 API 類型不支援所需的 WAF 關聯。
可用的選擇有:
- 保留較新的 API 並申請例外;
- 重新設計邊緣流程;
- 改用支援 WAF 附加的 REST API v1。
平台改用 REST API v1。
這增加了每筆請求的成本,但觀察到的流量足夠低,月度影響可忽略。安全與合規效益比微小的基礎架構節省更重要。
教訓不是 REST API v1 一定比較好,而是要根據完整的生產限制來評估雲端服務選擇,而非只看開發便利性或標價。
以垂直切片交付
產品從空白存放庫逐步推進至生產環境:
- 基礎 API、資料庫與 Terraform 模組;
- 邊緣傳遞、DNS、憑證與第一版行動 UI;
- 自訂授權與範圍存取;
- 到期、短網址、QR 重新導向與安全工作階段初始化;
- 獨佔認領與即時訂閱;
- 結構化記錄、儀表板、警示與合成監控;
- WAF 執行與合規上線;
- 生產自動化與生命週期維護。
每個階段都交付完整架構能力,而非一組不相干的檔案。
這讓整合風險較早顯現,並允許設計隨著發現的平台限制而演進。
值得重複使用的模式
使用條件寫入處理小型分散式狀態機
對於簡單轉換的工作流程,DynamoDB 條件表達式可取代應用程式鎖定與讀取後再寫入的邏輯。
將被否決的設計視為有價值的文件
多條實作路徑因平台限制而失敗。記錄失敗原因可避免未來工程師重複相同的實驗。
盡可能將授權推近資料
Resolver 中的欄位層級檢查確保查詢與訂閱遵循相同規則。
逐步推出安全控制
WAF 設定先以監控模式導入、分析,然後選擇性啟用,並搭配回滾旗標與生產金絲雀。
保持單一用途前端的單一用途特性
沒有 router 或複雜狀態函式庫是刻意的架構選擇,而不是缺少複雜度。
在初始架構中就納入生產就緒性
驗證、CI/CD、可觀測性、安全性掃描、健康檢查與回滾機制,不應等到功能完成後才加入。
最終結論
此平台最困難的部分不是產生六位數安全碼。
真正的工程挑戰是在處理並行、即時狀態、短效憑證、單次使用的後續工作階段、生產安全,以及企業交付控管的同時,建立共用裝置與顧客自有裝置之間可信賴的交接。
當自訂 MFA 流程被視為完整的分散式系統,而非小型驗證元件時,它才具有價值。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.