本文描述一個匿名化的企業實作。已刻意移除或泛化公司名稱、內部網域、存放庫識別碼、工單編號以及專有控制名稱。

多重驗證(Multi-factor authentication)通常被視為登入問題:輸入密碼、接收驗證碼、確認身分。

針對本案例所描述的系統,上述模式並不足以因應需求。

此產品運行於店內環境,同一台平板可能在單次交易中被多人使用:

  • 員工啟動流程;
  • 主管核准或協助;
  • 顧客在自己的裝置上檢視並簽名。

挑戰不只是證明使用者知道六位數驗證碼。我們需要建立一個安全的、短暫的工作階段交接機制,讓共用的店內工作階段與顧客的個人手機連結,同時不洩露後續的簽名工作階段,也不允許多部裝置同時認領同一筆交易。

本文將說明該平台的架構、安全模型、取捨考量,以及實際上線的做法。

真正的問題:安全的工作階段交接

流程從共用的平板開始。在某個階段,顧客需要繼續在自己的手機上完成部分程序。

因此平台必須回答幾個問題:

  1. 手機如何證明自己就是站在員工面前的那位顧客?
  2. 共用平板如何知道正確的手機已認領正確的工作階段?
  3. QR code 被掃描兩次時會發生什麼事?
  4. 如何防止工作階段識別碼與權杖出現在網址、瀏覽器歷史、紀錄檔或 referrer header 中?
  5. 驗證成功時如何立即通知手機?
  6. 如何確保單次使用的簽名網址在驗證完成前不會被曝光?

這些限制把看似單純的 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 被暫停而錯過最終事件的情況。

六位數安全碼的雙重用途

最重要的設計決策之一,是將六位數值同時作為:

  1. 人類可讀的 MFA 挑戰;
  2. 存取交接狀態所需的伺服器端能力。

讀取與訂閱篩選都需要此安全碼。當裝置被取代時,不會再傳回安全碼,且在狀態到達 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、預生產與生產環境。

此模式帶來兩項好處:

  1. 生產環境收到的是先前已測試過的相同成品;
  2. 設定成為部署層面的考量,而非建置層面的考量。

交付管線包含:

  • linting 與自動化測試;
  • 靜態分析;
  • 基礎架構驗證;
  • 安全性掃描;
  • 語意版本控制;
  • 不可變成品發佈;
  • 環境核准;
  • 生產環境變更管理整合。

可觀測性是產品的一部分

營運可見性不是在上線後才加入。

平台包含:

  • 結構化 JSON 記錄;
  • 關聯識別碼;
  • 命名業務事件;
  • 集中式記錄轉送;
  • APM 儀器;
  • Lambda、GraphQL 與交易健康狀態的儀表板;
  • 延遲、錯誤率與限流警示;
  • 合成健康檢查;
  • 環境特定升級規則。

監控設定存放在獨立的 Terraform 狀態中,因此可在不修改主要應用程式基礎架構的情況下進行變更。

這種分離很有用,因為警示的演進速度往往與應用程式資源不同。

真實的取捨:合規優先於較便宜的 API 選項

其中一個較具啟發性的決策涉及 API Gateway。

最初實作採用較新的 HTTP API,因為它較簡單且成本較低。在合規作業中,團隊確認該 API 類型不支援所需的 WAF 關聯。

可用的選擇有:

  • 保留較新的 API 並申請例外;
  • 重新設計邊緣流程;
  • 改用支援 WAF 附加的 REST API v1。

平台改用 REST API v1。

這增加了每筆請求的成本,但觀察到的流量足夠低,月度影響可忽略。安全與合規效益比微小的基礎架構節省更重要。

教訓不是 REST API v1 一定比較好,而是要根據完整的生產限制來評估雲端服務選擇,而非只看開發便利性或標價。

以垂直切片交付

產品從空白存放庫逐步推進至生產環境:

  1. 基礎 API、資料庫與 Terraform 模組;
  2. 邊緣傳遞、DNS、憑證與第一版行動 UI;
  3. 自訂授權與範圍存取;
  4. 到期、短網址、QR 重新導向與安全工作階段初始化;
  5. 獨佔認領與即時訂閱;
  6. 結構化記錄、儀表板、警示與合成監控;
  7. WAF 執行與合規上線;
  8. 生產自動化與生命週期維護。

每個階段都交付完整架構能力,而非一組不相干的檔案。

這讓整合風險較早顯現,並允許設計隨著發現的平台限制而演進。

值得重複使用的模式

使用條件寫入處理小型分散式狀態機

對於簡單轉換的工作流程,DynamoDB 條件表達式可取代應用程式鎖定與讀取後再寫入的邏輯。

將被否決的設計視為有價值的文件

多條實作路徑因平台限制而失敗。記錄失敗原因可避免未來工程師重複相同的實驗。

盡可能將授權推近資料

Resolver 中的欄位層級檢查確保查詢與訂閱遵循相同規則。

逐步推出安全控制

WAF 設定先以監控模式導入、分析,然後選擇性啟用,並搭配回滾旗標與生產金絲雀。

保持單一用途前端的單一用途特性

沒有 router 或複雜狀態函式庫是刻意的架構選擇,而不是缺少複雜度。

在初始架構中就納入生產就緒性

驗證、CI/CD、可觀測性、安全性掃描、健康檢查與回滾機制,不應等到功能完成後才加入。

最終結論

此平台最困難的部分不是產生六位數安全碼。

真正的工程挑戰是在處理並行、即時狀態、短效憑證、單次使用的後續工作階段、生產安全,以及企業交付控管的同時,建立共用裝置與顧客自有裝置之間可信賴的交接。

當自訂 MFA 流程被視為完整的分散式系統,而非小型驗證元件時,它才具有價值。