Strapi 忘記密碼的安靜回應:Anti-Enumeration、Phishing-as-a-Service 與撞庫經濟學

問題現場:一個「成功」卻收不到信的忘記密碼 某個週一下午,QA 回報:在一個 Flutter + Strapi 架構的 App 上,用測試帳號 [email protected] 點「忘記密碼」,畫面跳出 Success: Reset password link sent successfully on your email account.,但信箱(包含垃圾郵件匣)一封信都沒有。 直覺先排除幾個明顯可能:SMTP 設定壞掉、用戶被 block、信件被 Gmail 擋。但真正的答案比這些都有趣 — 這不是 bug,是 Strapi 一個刻意的安全設計,只是 App 前端把它翻譯錯了。 kubectl 診斷:20ms 與 800ms 的兩種人生 進 Strapi pod 撈 log,找 forgot-password 請求: kubectl logs -n default strapi-prod-xxxxxxxxx-xxxxx --since=6h \ | grep -E 'forgot-password|Reset password URL' 抽出的關鍵幾行: 04:04:52 info: Reset password URL generated: https://example.com/... 04:04:53 http: POST /api/auth/forgot-password (948 ms) 200 05:15:44 http: POST /api/auth/forgot-password (20 ms) 200 05:16:53 info: Reset password URL generated: https://example.com/... 05:16:54 http: POST /api/auth/forgot-password (664 ms) 200 05:17:30 http: POST /api/auth/forgot-password (26 ms) 200 一眼看出兩種節奏:800ms 級別的請求伴隨「Reset password URL generated」log,而 20-30ms 的請求則完全沒有。同一支 API,為什麼耗時差 30 倍? ...

April 21, 2026 · 4 分鐘 · Peter

Flutter 子母帳號切換踩了四個坑:GetX 狀態管理的隱藏地雷

一個「切換帳號」引爆四個 Bug 子母帳號功能很直覺:母帳號可以切換到子帳號視角,查看子帳號的健康日記、操作資料。技術上就是換一組 JWT,重新載入資料。 聽起來簡單,實際上踩了四個 GetX 狀態管理的地雷,每個都不是表面看得出來的問題。 Bug 連鎖反應全景圖 先看整體:四個 bug 環環相扣,每修一個就暴露下一個。 flowchart TD A[切換子帳號] --> B{JWT 更新了?} B -->|是| C{userId / documentId 更新了?} C -->|否| D[API 500 / 日記查錯帳號] C -->|修好| E{ViewModel 有重建?} E -->|刪除重建| F{UI widget 指向新 ViewModel?} F -->|否:持有舊參考| G[首頁資料不更新] F -->|改為原地刷新| H{全域變數也更新了?} H -->|否:top-level final| I[Dialog 功能全失效] H -->|改為 getter| J{App 重啟能恢復?} J -->|否:狀態在記憶體| K[母帳號控制權消失] J -->|持久化到 SharedPreferences| L[全部修好] classDef error fill:#f8d7da,stroke:#dc3545,color:#721c24 classDef success fill:#d4edda,stroke:#28a745,color:#155724 classDef warning fill:#fff3cd,stroke:#ffc107,color:#856404 class D,G,I,K error class L success class A,B,C,E,F,H,J warning 第一坑:JWT 換了,身份沒換 切換帳號的核心邏輯只做了一件事——替換 JWT: ...

April 16, 2026 · 2 分鐘 · Peter

廠商說不支援 URL Scheme:跨 App 綁定從自訂協議遷移到 Universal Link 的完整實錄

廠商一句話,整個回調機制要重寫 我們的健康管理 App 整合了一台第三方氣酮檢測裝置。綁定流程很單純:使用者從我們的 App 跳到廠商的 App 完成綁定,綁定完成後跳回來,顯示成功或失敗。 原本的回調方式是 URL Scheme: myapp://device-bindback?status=success iOS 上一直運作正常,直到廠商的 Android App 更新後回報:他們不支援 URL Scheme 回調,要求改用 Universal Link。 這不是改一行 URL 的事。Universal Link 牽涉到網域驗證、靜態檔案部署、平台設定、安全機制,幾乎是把整個回調架構重新設計。 在深入之前,先釐清幾個關鍵術語: 術語 說明 URL Scheme 自訂的 URL 前綴(如 myapp://),讓其他 App 可以透過這個 URL 開啟你的 App。缺點是任何 App 都可以註冊相同的 scheme,沒有驗證機制。 Universal Link(iOS) Apple 的機制,把 HTTPS URL 和特定 App 綁定。系統透過 AASA 檔案驗證網域和 App 的關聯,確保只有合法的 App 能攔截該 URL。 App Links(Android) Google 對應 Universal Link 的機制,透過 assetlinks.json 驗證網域和 App 的關聯,原理相同。 AASA Apple App Site Association,放在 https://你的網域/.well-known/apple-app-site-association 的 JSON 檔案,告訴 iOS「哪些路徑要交給哪個 App 開啟」。 assetlinks.json Android 版的 AASA,放在 https://你的網域/.well-known/assetlinks.json,功能相同。 Provisioning Profile Apple 的簽名設定檔,記錄 App 被允許使用哪些功能(如 Push Notifications、Associated Domains)。新增功能時必須重新產生。 Deep Link 泛指所有能從外部開啟 App 並導到特定頁面的連結,URL Scheme 和 Universal Link 都是 Deep Link 的實作方式。 SHA-256 指紋 App 簽名憑證的雜湊值(32 組 hex,如 AA:BB:CC:...),用來唯一識別一個 App 的簽名身份。assetlinks.json 靠它比對「宣稱的 App」和「裝置上的 App」是否同一個。 Upload Key 開發者本地 keystore 的私鑰,用來簽名上傳到 Play Console 的 AAB/APK。本地開發、CI、模擬器都用這把。 App Signing Key Google Play 持有的私鑰,用來對上架版 App 做最終簽名。使用者從 Play Store 安裝的 App 是這把 key 簽的,指紋跟 upload key 不同。 改動前後的流程差異,用一張圖看最清楚: ...

April 10, 2026 · 6 分鐘 · Peter

從 53 種食物到 13,152 種:五國食物資料庫 ETL 管線設計實錄

前言:手動維護 53 種食物的極限 我正在開發一款健康追蹤 App,核心功能之一是飲食記錄。一開始,食物資料庫是手動維護的 JSON — 53 種台灣常見小吃,從滷肉飯到珍珠奶茶。 53 種夠用嗎?使用者搜尋「鮭魚」找不到、搜尋「優格」找不到、搜尋「oatmeal」更不用說。手動新增不可能跟上需求,我需要一條自動化管線,把全世界的食物營養資料拉進來。 這就是 ETL 管線登場的時機。 什麼是 ETL?用食物資料庫解釋 ETL 是 Extract(擷取)、Transform(轉換)、Load(載入) 的縮寫,是資料工程最基礎的設計模式。與其抽象解釋,直接用這個專案的食物資料庫來看: flowchart LR subgraph Extract A1[🇹🇼 台灣食藥署 API] A2[🇯🇵 日本 GitHub] A3[🇨🇳 中國 GitHub] A4[🇺🇸 USDA API] end subgraph Transform B[統一格式化<br/>營養素映射<br/>分類標準化<br/>名稱清理] end subgraph Load C[合併去重<br/>驗證完整性] D[(foods_database.json<br/>13,152 種食物)] end A1 --> B A2 --> B A3 --> B A4 --> B B --> C --> D 四個來源的原始資料格式完全不同 — 台灣用中文欄位名(粗蛋白)、日本用 FAO 代碼(prot)、USDA 用數字編號(203)。Transform 階段把它們統一成 App 需要的 {id, name, emoji, category, nutrition} 結構。 ...

March 17, 2026 · 2 分鐘 · Peter

當授權資料不可信時,我選擇讓系統安靜地退後一步

延續上一篇的問題 在上一篇文章中,我們遇到了一個問題:Link Table 資料遺失導致使用者沒有角色,所有 API 都回傳 401。 文章最後我拋出了一個問題: 如果你的系統把角色資訊 cache 在 Redis、JWT claim、或 BFF 層,當 Link Table 資料不正確時,系統應該: 名詞解釋: Redis:一種高速的記憶體資料庫(In-Memory Database),常用於快取(Cache)熱門資料,避免每次都查詢主資料庫 JWT claim:JWT Token 內的資料欄位。例如把使用者角色直接寫在 Token 裡:{ "sub": "user_123", "role": "admin" },這樣就不用每次都查 DB BFF(Backend For Frontend):一種架構模式,在前端和後端 API 之間多一層「專為前端服務的後端」,常會在這層做權限快取 立即全站拒絕? 繼續相信 cache? 還是進入 degraded mode? 我的選擇是:進入 Degraded Mode。 這篇文章會解釋為什麼,以及如何實作。 一句話定義 Degraded Mode = 系統已知自己「部分不可信」,主動降級功能以維持安全與可用性。 不是壞了、不是裝沒事,而是: 我知道哪裡壞 我知道哪些功能不能給 我知道要保住什麼 為什麼不選另外兩個方案? ❌ 方案 A:立即全站拒絕 所有 API → 401/503 → 業務全掛 問題: 使用者完全無法使用系統 對業務衝擊太大 「寧可錯殺一百」的策略在商業系統中代價過高 適用場景: 金融交易、醫療處方等「錯了比沒有更糟」的場景 ...

January 20, 2026 · 4 分鐘 · Peter

資料庫同步的隱藏陷阱:Link Table 的重要性

問題現象:登入成功卻被拒於門外 最近在 Staging 環境遇到一個詭異的問題:使用者登入成功,拿到了有效的 JWT Token,但存取任何需要認證的 API 都回傳 401 Unauthorized。 # 登入成功,拿到 token POST /api/auth/local → 200 OK { "jwt": "eyJhbGc...xxxxx...your-jwt-token", "user": { "id": 1001, "email": "[email protected]" } } # 但存取個人資料失敗 GET /api/users/me → 401 Unauthorized Token 驗證通過、使用者存在、帳號未被封鎖。問題到底在哪? 根本原因:遺失的 Link Table 經過一番追查,發現問題出在資料庫同步時漏掉了關聯表(Link Table)。 什麼是 Link Table? 在關聯式資料庫中,多對多關係需要透過中間表來建立。這個中間表就是 Link Table(也稱為 Junction Table、Join Table、或 Pivot Table)。 erDiagram users ||--o{ users_roles_lnk : has roles ||--o{ users_roles_lnk : assigned_to users { int id PK string email string password boolean confirmed } roles { int id PK string name string type } users_roles_lnk { int user_id FK int role_id FK int user_ord } 使用者與角色的關係: ...

January 20, 2026 · 4 分鐘 · Peter

為什麼技術選型 CMS 我要選 Strapi?2024 年中的預算與系統分析決策

引言:一個技術選型的起點 2024 年 6 月,我坐在會議室裡,面對著老闆和行銷總監,準備報告我對公司新系統 CMS 的技術選型建議。這不是一個輕鬆的決定——選錯了,可能浪費數百萬的開發成本;選對了,能為公司省下可觀的人力支出。 經過數週的研究與分析,作為一個架構規劃師同時也是技術決策者,我最終選擇了 Strapi 作為我們的 Headless CMS 解決方案。這篇文章將分享我的決策過程、考量因素,以及最重要的——這個選擇如何為公司省下大筆預算。 什麼是 Headless CMS? 在深入 Strapi 之前,先理解 Headless CMS 的核心概念。 傳統 CMS vs Headless CMS graph TB subgraph Traditional["傳統 CMS(如 WordPress)"] A[後端 + 資料庫] --> B[模板引擎] B --> C[HTML 輸出] C --> D[瀏覽器] end subgraph Headless["Headless CMS(如 Strapi)"] E[後端 + 資料庫] --> F[REST/GraphQL API] F --> G[Web App] F --> H[Mobile App] F --> I[IoT Device] F --> J[其他服務] end 傳統 CMS 將前端與後端緊密耦合,網站的外觀和內容管理綁在一起。而 Headless CMS 則專注於內容管理和 API 提供,讓前端團隊可以使用任何技術框架來消費這些 API。 ...

December 23, 2025 · 4 分鐘 · Peter

Claude Code Token 不夠用?從 $20 升到 $100 還是燒光:我學到的教訓

前言:一個月燒掉 $100 的真實故事 如果你正在考慮升級 Claude Code 的訂閱方案,或者已經升級了卻發現 token 還是不夠用,那你來對地方了。 這不是一篇推銷文,而是一個真實的使用心得。 我從 Pro Plan($20/月) 開始使用 Claude Code,很快就發現 token 不夠用。心想:「升級到 Max Plan($100/月)應該就沒問題了吧?」 結果呢? Max Plan 的 token 也不夠用。 更尷尬的是,我花了大把的 token 去處理一些看似簡單的 bug,像是: 體脂率的小數點精度問題 排便次數顯示 null 和 0 的差異 這些問題聽起來簡單,實際上卻各花了大量 token 去「探索」程式碼。 直到我發現問題的根源不是 token 不夠 (Pro plan是真的不夠!),而是我沒有找到對的 Skill 來處理這類問題。 問題分析:Token 都燒去哪了? 讓我用一張圖來說明沒有使用正確 Skill 時的除錯流程: graph TD A[發現 Bug] --> B[讓 AI 搜尋程式碼] B --> C[讀取 file_a.dart] C --> D[沒找到,讀取 file_b.dart] D --> E[還是沒找到,讀取 file_c.dart] E --> F[發現可能相關,展開搜尋] F --> G[讀取更多檔案...] G --> H[終於找到問題] H --> I[提出修復方案] I --> J{修復成功?} J -->|否| B J -->|是| K[完成] style B fill:#ff6b6b,color:#000 style C fill:#ff6b6b,color:#000 style D fill:#ff6b6b,color:#000 style E fill:#ff6b6b,color:#000 style F fill:#ff6b6b,color:#000 style G fill:#ff6b6b,color:#000 style K fill:#51cf66,color:#000 看到問題了嗎? ...

December 20, 2025 · 3 分鐘 · Peter

Cursor Browser Visual Editor 深度解析:AI 驅動的視覺化前端開發革命

前言 傳統的前端開發流程往往是這樣的:你在程式碼編輯器中修改 CSS,儲存檔案,切換到瀏覽器查看效果,發現不滿意,再切回編輯器調整⋯⋯這個來回切換的過程不僅耗時,更打斷了創意的流動。 Cursor 的 Browser Visual Editor 正是為了解決這個問題而生。 這個全新功能將網頁應用程式、程式碼庫和視覺化編輯能力整合在單一介面中,讓開發者可以直觀地操作介面元素,同時保持與程式碼的同步。本文將深入解析這項創新功能的核心概念、運作原理和實際應用場景。 什麼是 Cursor Browser Visual Editor? 核心概念 Cursor Browser Visual Editor 是一個整合在 Cursor IDE 中的視覺化編輯工具,它讓開發者能夠: 直接拖拉介面元素來調整佈局 透過側邊欄即時修改樣式屬性 使用自然語言描述想要的變更 讓 AI 自動更新對應的程式碼 這不只是一個簡單的 CSS 預覽工具,而是一個完整的**設計到程式碼(Design-to-Code)**工作流程。 省 Token 的關鍵利器 這裡要特別強調一個重要的實用價值:Visual Editor 可以大幅減少你對 AI 下 prompt 的次數,進而節省 Token 用量! 想像一下傳統的 AI 輔助開發流程: ❌ 傳統 AI 輔助方式(消耗大量 Token): 1. "請把這個按鈕的 padding 加大一點" → 消耗 Token 2. 看效果,不滿意 3. "再大一點,然後加個圓角" → 消耗 Token 4. 看效果,顏色不對 5. "背景色換成藍色" → 消耗 Token 6. 還是不滿意... 7. 反覆對話 10 次 → 消耗大量 Token 💸 ✅ Visual Editor 方式(幾乎零 Token): 1. 直接在面板上拖動 padding 滑桿 → 0 Token 2. 調整 border-radius 數值 → 0 Token 3. 點選色盤換顏色 → 0 Token 4. 即時預覽,滿意為止 → 0 Token 5. 只有複雜變更才需要 AI → 少量 Token ✅ 對於 Cursor 的付費用戶來說,這意味著: ...

December 19, 2025 · 5 分鐘 · Peter

在 Kubernetes 上部署 OV SSL 證書:完整實戰指南

為什麼選擇 TWCA OV 證書 在生產環境中,使用自簽憑證會導致瀏覽器顯示不安全警告,影響使用者信任。雖然 Let’s Encrypt 提供免費的自動化證書,但某些企業或政府專案需要台灣在地的認證機構簽發證書以符合法規要求。 SSL 證書的三個等級 SSL 證書依據驗證強度分為三個等級: 證書類型 驗證內容 適用場景 信任標記 價格 DV (Domain Validation) 僅驗證網域所有權 個人網站、部落格 🔒 基本鎖頭 免費~低 OV (Organization Validation) 驗證組織身份 企業網站、SaaS 服務 🔒 組織名稱 中 EV (Extended Validation) 嚴格驗證企業 金融、電商、政府 🟢 綠色網址列 高 本文使用的是 TWCA OV SSL 證書,它提供: ✅ 組織身份驗證(瀏覽器可顯示公司名稱) ✅ 完整的證書鏈(受所有主流瀏覽器信任) ✅ 12 個月有效期 ✅ 台灣在地技術支援 HTTPS 與 SSL/TLS 運作原理 在深入部署之前,先理解 HTTPS 如何保護資料傳輸: sequenceDiagram participant Browser as 瀏覽器<br/>(內建 TWCA 根憑證) participant Server as Web Server Note over Browser,Server: 1. TLS 握手階段 Browser->>Server: ClientHello (支援的加密套件) Server->>Browser: ServerHello (選擇的加密套件) Server->>Browser: 傳送證書鏈<br/>(伺服器證書 + 中繼憑證) Note over Browser: 2. 證書驗證(完全離線) Browser->>Browser: 用中繼憑證驗證伺服器證書 Browser->>Browser: 用內建根憑證驗證中繼憑證 Browser->>Browser: 檢查證書有效期與網域 Note over Browser: ✓ 證書有效 Note over Browser,Server: 3. 金鑰交換 Browser->>Server: 用公鑰加密的 session key Server->>Server: 用私鑰解密 session key Note over Browser,Server: 4. 加密通訊 Browser->>Server: 加密的 HTTP 請求 Server->>Browser: 加密的 HTTP 回應 關鍵機制說明: ...

December 10, 2025 · 7 分鐘 · Peter