會員資料不見?這不是我沒要求,是外包商沒做完整的 OAuth 2.0

故事的開始:會員資料集體消失事件 最近收到許多會員的反映,問題驚人地一致: 📝 「我的日記資料不見了!」 👤 「帳號被自動登出,要重新登入」 ⏳「App 一直轉圈圈,什麼都看不到」 😰 「重新開啟 App 也一樣,資料都不見了」 更詭異的是,這些問題似乎沒有規律性,有的使用者正常,有的使用者卻深受其擾。身為技術負責人,這讓我立刻警覺:這不是資料遺失,而是認證機制出問題了。 調查過程:抽絲剝繭找出真相 問題時間軸 第一步:確認資料庫完整性 我的第一個懷疑是資料庫是否真的遺失資料。登入後台查詢,發現: ✅ 關鍵發現: 所有會員的日記資料都完好無缺 這代表問題不在資料層,而是在存取權限上。 第二步:檢查使用者行為模式 整理會員回報的時間點,我發現一個規律: 會員 註冊時間 問題發生時間 間隔 會員 A 30 天前 今天 30 天 會員 B 29 天前 今天 29 天 會員 C 3 天前 正常使用 - 關鍵字:30 天 ⚡ 第三步:檢查 JWT Token 配置 翻開 Strapi 後端的配置檔,真相大白: // Strapi 預設 JWT 設定 jwt: { expiresIn: '30d' // Token 有效期:30 天 } 問題根本原因 💡 關鍵洞察: 使用者以為資料不見了,實際上只是「看不到」而已。資料仍安全地存在資料庫中,只是 Token 過期導致無法存取。 ...

December 6, 2025 · 6 分鐘 · Peter

一次錯誤部署引發的 PostgreSQL Sequence 災難:為什麼使用者突然無法解鎖動畫?

「老闆,用戶的解鎖記錄全不見了!」「快把舊資料拉出來灌回去!」在緊急狀況下,我沒想太多就照做了。然後,我不小心埋下了一顆定時炸彈… 🔥 第一幕:災難降臨 2025 年 11 月某日,上午 10:30 Slack 突然炸開: 💬 同事:「完蛋了…我剛剛不小心部署到舊的 commit…」 💬 QA:「欸!為什麼使用者的動畫解鎖記錄都不見了?」 💬 使用者:「我昨天才花金幣解鎖的動畫怎麼不見了?」 💬 老闆:「@所有人 立刻確認影響範圍!」 我打開資料庫一看: SELECT COUNT(*) FROM user_unlocked_animations; -- 結果: 0 😱 所有用戶的解鎖記錄全部消失! 原因:同事不小心部署了一個舊的 Strapi commit,那個版本的 database migration 把 user_unlocked_animations 相關的表全部清空了。 ⚡ 第二幕:老闆的緊急命令 💬 老闆:「快!把之前的用戶解鎖記錄拉出來,灌回現在的資料庫!」 我心裡想:「舊資料插回去,新資料又同時在進來…會不會有問題?」 但老闆在等,使用者在抱怨,沒時間多想,先恢復資料再說。 緊急恢復資料 // 從備份拉出資料,直接插入(包含原始的 ID) blablablabla } // ⚠️ 直接指定了 id,但沒想到要更新 sequence... 執行完畢: ✅ 資料恢復完成! QA 測試:「使用者的解鎖記錄都回來了!」 眾人鬆了一口氣。 💣 第三幕:24 小時後,炸彈引爆 隔天下午 💬 客服:「有使用者回報說無法解鎖動畫!!!」 💬 使用者:「我有 3 個金幣,想解鎖動畫,但一直顯示錯誤!金幣被扣了但動畫沒解鎖!」 ...

November 9, 2025 · 5 分鐘 · Peter

解決 CKEditor 圖片水平排版在前端顯示為垂直排列的問題

前言:編輯器與前端的排版不一致之謎 在開發多平台醫療健康應用時,我們採用了現代化的技術棧: 後端 CMS:Strapi v5.15.1(Headless CMS) 前端框架:Vue.js 3 富文本編輯器:CKEditor 5 資料傳輸:GraphQL API 這個組合在大多數情況下運作良好,編輯者可以在 Strapi 後台使用 CKEditor 輕鬆編輯富文本內容,前端 Vue.js 應用透過 GraphQL 獲取並渲染這些內容。 然而,我們遇到了一個令人困惑的問題: 在 Strapi 後台使用 CKEditor 精心排版的水平並排圖片,到了前端網頁卻變成了垂直排列。 這個問題不僅影響了內容的視覺呈現,也破壞了編輯者的排版意圖。更重要的是,這讓非技術背景的內容編輯者感到困惑:「為什麼我在後台看到的排版,到了網站上就變了?」 這篇文章將深入探討這個問題的根本原因,並提供系統性的解決方案。 問題現象與環境說明 內容流程架構 我們的內容從編輯到展示的完整流程如下: 問題的具體表現 預期行為: 在 Strapi CKEditor 後台,編輯者將兩張圖片設定為水平並排: <!-- CKEditor 生成的 HTML 結構 --> <figure class="image" style="float:left"> <img src="/uploads/image1.webp" alt="圖片1"> <figcaption>圖片1說明</figcaption> </figure> <figure class="image" style="float:left"> <img src="/uploads/image2.webp" alt="圖片2"> <figcaption>圖片2說明</figcaption> </figure> 實際現象: 前端 Vue.js 渲染後,圖片變成垂直排列: 兩張圖片沒有並排顯示,而是一張接著一張垂直堆疊。 技術環境詳情 Strapi CKEditor 配置(config/schema.json): { "kind": "collectionType", "collectionName": "articles", "info": { "singularName": "article", "pluralName": "articles", "displayName": "文章" }, "attributes": { "title": { "type": "string" }, "content": { "type": "customField", "options": { "preset": "defaultHtml" }, "customField": "plugin::ckeditor5.CKEditor" } } } Vue.js 前端渲染組件(ServiceDetailView.vue): ...

August 1, 2025 · 9 分鐘 · Peter

前端登入失敗的真兇:深入理解 CORS 問題與實戰解法

前言:那個令人抓狂的錯誤訊息 在開發前後端分離的 Web 應用時,幾乎每位工程師都曾遇過這個令人頭痛的錯誤: Access to fetch at 'http://localhost:1337/api/auth/local' from origin 'http://localhost:5173' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. 這個錯誤通常發生在最關鍵的時刻: 前端登入功能即將完成,卻無法呼叫後端 API 串接第三方服務時,資料無法正常取得 部署到測試環境後,原本運作正常的功能突然失效 CORS(Cross-Origin Resource Sharing,跨來源資源共用)是現代 Web 開發中的核心安全機制,但也是許多開發者的痛點。這篇文章將從基礎原理到實戰應用,帶你完整理解 CORS 的運作方式,並提供實際可用的解決方案。 為什麼需要 CORS?從同源政策說起 同源政策的誕生 在理解 CORS 之前,我們需要先認識「同源政策」(Same-Origin Policy, SOP)。這是瀏覽器最基本的安全機制,在 1995 年 Netscape Navigator 2.0 引入 JavaScript 時就已經存在。 同源政策的目的:防止惡意網站讀取另一個網站的敏感資料。 想像一個情境:你登入了網路銀行(https://bank.com),此時你的瀏覽器保存了銀行的登入 Cookie。如果沒有同源政策,當你不小心訪問了一個惡意網站(https://evil.com),該網站的 JavaScript 就能透過你的瀏覽器向 https://bank.com 發送請求,並讀取你的帳戶資料。 同源政策阻止了這種攻擊:https://evil.com 的 JavaScript 無法讀取 https://bank.com 的回應內容。 什麼是「同源」? 兩個 URL 被視為同源,必須滿足以下三個條件: ...

July 1, 2025 · 9 分鐘 · Peter

整合 Google 登入至 Strapi:在 Kubernetes 上解決「Secure Cookie over Unencrypted Connection」的實戰紀錄

前言 在現代 Web 應用開發中,提供第三方登入(Social Login)已經成為標準配備。相比傳統的帳號密碼註冊流程,使用 Google、Facebook、GitHub 等服務登入不僅能降低使用者註冊門檻,還能提升安全性(由大廠處理密碼儲存與驗證)。 當我們決定為 Strapi CMS 後台加入 Google OAuth 登入時,原本以為只是個簡單的設定任務: 在 Google Cloud Console 建立 OAuth 2.0 憑證 在 Strapi 填入 Client ID 和 Client Secret 點擊「Login with Google」按鈕,完成! 但現實總是更複雜。當應用部署到 Kubernetes 叢集後,我們遇到了一個令人困惑的錯誤訊息: Error: Cannot send secure cookie over unencrypted connection 這個錯誤訊息背後,牽涉到 HTTP/HTTPS 協定、Proxy Trust 機制、Kubernetes Ingress 架構、以及瀏覽器 Cookie 安全策略等多層知識。這篇文章將完整記錄我如何一步步拆解問題、理解根本原因、並最終在生產環境中實現安全可靠的 Google 登入功能。 OAuth 2.0 授權碼流程基礎 在深入問題之前,我們先理解 Google OAuth 登入的完整流程。OAuth 2.0 提供了多種授權模式(Grant Types),而 Web 應用最常使用的是「授權碼模式(Authorization Code Flow)」。 這個流程中有幾個關鍵點: ...

May 16, 2025 · 13 分鐘 · Peter

解決 Strapi CMS 正式環境空白頁的踩坑經驗分享

前言:一個簡單的環境變數引發的災難 在部署 Strapi CMS 到 Kubernetes 正式環境時,只是加了一行看似無害的環境變數設定: env: - name: NODE_ENV value: production # 就是這一行! 結果卻導致整個管理後台變成一片空白,連登入頁面都看不到。更詭異的是: ✅ API 完全正常,GraphQL 和 REST 都能回應 ✅ Pod 狀態正常,沒有任何錯誤訊息 ✅ 日誌顯示 Strapi 成功啟動 ❌ 瀏覽器打開 /admin 卻是一片空白 這種「Schrodinger 的服務」(同時正常又不正常)讓人抓狂。經過一番排查,終於發現罪魁禍首是 CSP (Content Security Policy) 在作怪。 本文將深入探討: 為什麼正式環境會出現空白頁 CSP 的工作原理與安全機制 完整的問題排查步驟 如何正確配置 Strapi 的安全策略 生產環境的安全最佳實踐 問題背景:開發正常,正式環境空白 環境差異對比 問題現象詳細描述 Kubernetes Deployment 設定: apiVersion: apps/v1 kind: Deployment metadata: name: strapi-prod namespace: default spec: replicas: 1 selector: matchLabels: app: strapi template: metadata: labels: app: strapi spec: containers: - name: strapi image: myregistry.com/strapi:v5.0.0 env: - name: NODE_ENV value: production # 問題的起點 - name: DATABASE_HOST valueFrom: secretKeyRef: name: strapi-db-secret key: host - name: ADMIN_JWT_SECRET valueFrom: secretKeyRef: name: strapi-admin-secret key: jwt-secret ports: - containerPort: 1337 部署後的症狀: ...

May 7, 2025 · 8 分鐘 · Peter

解決 Kubernetes 多餘 Pod 問題與 CrashLoopBackOff 的實戰心得

前言:一次神秘的 Pod 複製事件 在一次例行的 Strapi CMS 更新部署到 AWS EKS 時,遇到了一個詭異的現象:明明 Deployment 設定檔中清楚寫著 replicas: 1,但實際運行的 Pod 卻有兩個!更奇怪的是,其中一個 Pod 持續處於 CrashLoopBackOff 狀態,而另一個則正常運行。 無論怎麼刪除多餘的 Pod,它總是會像打不死的蟑螂一樣再次出現。這種「靈異事件」讓我開始懷疑 Kubernetes 是不是有自己的想法… 本文將深入探討: 為什麼會出現多餘的 Pod CrashLoopBackOff 背後的機制 Kubernetes Deployment 和 ReplicaSet 的運作原理 實戰排查步驟與解決方案 Secret 編碼陷阱與預防措施 問題背景:Deployment 說一個,實際卻有兩個 問題現象 預期行為: # my-strapi-prod-deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: my-strapi-prod spec: replicas: 1 # 只要 1 個 Pod selector: matchLabels: app: my-strapi-prod 實際情況: $ kubectl get pods -n default | grep my-strapi-prod NAME READY STATUS RESTARTS AGE my-strapi-prod-7d4b5c8f9d-x2k4p 1/1 Running 0 10m my-strapi-prod-8c9a6d7e5f-w8n2q 0/1 CrashLoopBackOff 5 5m 兩個 Pod 同時存在,卻只有一個正常運行! ...

May 6, 2025 · 8 分鐘 · Peter

Strapi 5

客製化UI & Components Strapi 5 客製化UI & Components 發現: https://design-system.strapi.io/?path=/docs/getting-started-welcome–docs 在@strapi/design-system, 目前無法更改底層 https://docs.strapi.io/dev-docs/customization “Some parts of the admin panel can be customized.” 覆蓋樣式:使用自定義 CSS 或樣式覆蓋,而非直接改動源碼。(目前朝此方向研究)

January 15, 2025 · 1 分鐘 · Peter

Strapi 自訂搜尋功能:讓 Admin 面板支援多欄位搜尋

前言 在開發 Strapi 後台管理系統時,我遇到一個實際的使用者體驗問題:在「已購買課程」列表中,管理員可以用「真實姓名」搜尋到用戶的購課記錄,但用「電話號碼」卻搜尋不到。這個不一致的行為讓管理員感到困惑。 本文將分享如何在 Strapi v5 中實作自訂搜尋邏輯,讓 Admin 面板支援跨關聯的多欄位搜尋。 問題場景 資料結構: purchased-course(已購買課程):關聯到 user user(用戶):包含 email、realName、phoneNumber、username、nickname 等欄位 問題現象: ✅ 搜尋「真實姓名」→ 可以找到對應的購課記錄 ❌ 搜尋「電話號碼」→ 找不到任何結果 為什麼會這樣? 因為 Strapi 預設的搜尋功能只會搜尋當前 Collection 的直接欄位,不會自動搜尋關聯表(relation)的欄位。當我們在 purchased-course 列表搜尋時,Strapi 只會在 purchased-course 本身的欄位中搜尋,而不會去搜尋關聯的 user 資料。 解決方案架構 Strapi 提供了三個層級可以自訂搜尋邏輯: 1. Controller 層(API 端點) 位置: src/api/purchased-course/controllers/purchased-course.ts 用途: 處理前端透過 REST API 的搜尋請求 2. Service 層(業務邏輯) 位置: src/api/purchased-course/services/purchased-course.ts 用途: 封裝可重複使用的業務邏輯 3. Content Manager Plugin Extension(Admin 面板) 位置: src/extensions/content-manager/strapi-server.ts 用途: 最重要! 這是讓 Admin 面板搜尋生效的關鍵 ...

October 27, 2024 · 5 分鐘 · Peter

全端專案 AWS EKS 雲端架構深度解析

前言:從本地開發到雲端生產環境 本文將深入解析一個全端專案在 AWS 上的完整基礎設施架構,展示如何透過 Kubernetes (EKS) 實現高可用性、可擴展性和成本效益的生產環境。這個平台提供線上課程、預約服務、會員管理和金流整合等功能。 本文涵蓋內容: 完整的 AWS EKS 叢集架構 Strapi CMS 和 Vue.js 前端的容器化部署 Jenkins CI/CD 自動化部署流程 Ingress NGINX 負載均衡和 SSL 憑證管理 與 AWS RDS、S3、ECR 的整合 第三方服務整合 (Firebase FCM、台灣金流) 監控與日誌管理 架構概覽 整體架構圖 核心技術棧 基礎設施層: AWS EKS 1.32.9 (Kubernetes 託管服務) AWS EC2 (ARM64 架構 - t4g.medium) AWS RDS PostgreSQL (託管資料庫) AWS S3 (物件儲存) AWS ECR (容器映像倉儲) AWS ELB (負載均衡器) CI/CD 層: Jenkins (Mac mini 本地部署) Docker (容器建置) kubectl (Kubernetes 部署工具) 應用層: ...

June 5, 2024 · 10 分鐘 · Peter