串接藍新金流踩的那些坑:SHA256、Webhook 三連坑、測試環境陷阱
把藍新金流串進刷題神器的過程,SHA256 簽章格式坑掉兩天,Webhook 解密連踩三個坑。把這些坑記下來,希望下次(或你)不用重踩一遍。
台灣的付費功能,繞不過金流。刷題神器選了藍新(NewebPay),串接過程踩了幾個坑,值得記錄下來。
架構選擇:為什麼是藍新
選擇藍新的過程記錄在另一篇金流選擇文章,這篇直接進技術整合。
整合架構:Next.js API Route 作為後端中介層,前端不直接碰金流——使用者點「購買」,前端呼叫 /api/checkout,後端組出藍新所需的參數,回傳 action(表單目標)和 fields(隱藏欄位),前端動態建 form 然後 submit 跳轉。
為什麼不用 fetch? 藍新只接受 form POST 跳轉,不是 fetch。這是第一個容易踩的坑。
兩張資料表:待確認訂單表 + 購買紀錄表
藍新付款流程有個時間差:使用者點「購買」→ 跳到藍新付款頁 → 完成付款 → 藍新 webhook 回呼你的伺服器。中間可能幾秒、可能幾分鐘,使用者也可能中途放棄。
所以設計了兩張表:
- 待確認訂單表:使用者觸發結帳時立即寫入,包含訂單 ref 和 user_id
- 購買紀錄表:藍新 webhook 回呼並驗證成功後才寫入
Webhook 靠 ref(訂單編號)把兩張表串起來,找到對應的待確認記錄,確認 user_id,寫進購買紀錄。中斷的訂單留在待確認表不動,不會被誤算為購買成功。
坑 1:SHA256 字串格式(卡了兩天)
這是整個過程最浪費時間的坑。
藍新 SHA256 簽章的字串組合,正確寫法是:
HashKey={key}&{AES加密後的TradeInfo}&HashIV={iv}
很多教學文(和 AI 生成的程式碼)寫的是:
HashKey={key}&TradeInfo={AES加密後的TradeInfo}&HashIV={iv}
差別在於:{tradeInfo} 前面沒有 TradeInfo= 前綴,只用 & 分隔。
兩種寫法看起來很像,但 SHA256 結果完全不同,藍新端驗簽必然失敗。症狀是:金鑰確認正確、AES 加解密自洽、線上解密器能解開 TradeInfo,但藍新仍回報「sha256 不符合」。這個誤導性的錯誤訊息讓你一直往金鑰方向排查,而不是往字串格式方向。
正確格式在藍新「線上交易—幕前支付技術串接手冊」4.1.2 SHA256 章節有範例,但要仔細對照,不能只看範例的字面格式。
坑 2:信用卡參數名
// ✅ 正確
CREDIT: '1'
// ❌ 錯誤
CREDITONETIME: '1'
藍新遇到不認識的參數會直接擋下,回報 sha256 失敗的誤導訊息。這讓你又會去排查簽章,而不是去排查參數名。
坑 3:AES 設定
- 演算法:AES-256-CBC
- Padding:PKCS7(Node.js crypto default 就是,不用特別設)
- HashKey 必須剛好 32 字元 ASCII
- HashIV 必須剛好 16 字元 ASCII
- AES 輸出:lowercase hex
- SHA256 輸出:uppercase hex(要
.toUpperCase())
坑 4:測試和正式環境完全分開
| 後台網址 | API 端點 | |
|---|---|---|
| 測試 | cwww.newebpay.com | ccore.newebpay.com/MPG/mpg_gateway |
| 正式 | www.newebpay.com | core.newebpay.com/MPG/mpg_gateway |
兩邊獨立帳號、獨立商店、獨立金鑰,憑證完全不互通。NEWEBPAY_ENV、NEWEBPAY_MERCHANT_ID、NEWEBPAY_HASH_KEY、NEWEBPAY_HASH_IV 必須同一組,不能混搭測試和正式的憑證。
Webhook 三連坑
Webhook 是付款完成後藍新主動 POST 過來的那支 API。串接時連踩三個坑:
坑 A:bad decrypt
Error: Provider routines::bad decrypt (code: ERR_OSSL_BAD_DECRYPT)
根因:藍新 AES-256-CBC padding 跟 Node.js 預設 PKCS#7 自動處理不相容。
修法:decipher.setAutoPadding(false),然後手動 PKCS#7 unpad,對齊藍新 PHP 範例的 OPENSSL_ZERO_PADDING + strippadding() 邏輯。
坑 B:解密成功但找不到 MerchantOrderNo
decrypted but missing MerchantOrderNo
根因:解密後的資料格式是 JSON,不是 URL-encoded form。程式碼用 URLSearchParams.get('MerchantOrderNo') 自然找不到。
修法:改用 JSON.parse,欄位在 Result.MerchantOrderNo 巢狀路徑裡。
坑 C:JSON parse 失敗
Unexpected non-whitespace character after JSON at position 482
根因:藍新 padding 不一定標準 PKCS#7,手動 unpad 後尾巴可能有 garbage 字元。
修法:parse 前先 str.slice(0, str.lastIndexOf('}') + 1) 把 JSON 後的雜訊砍掉,再 parse。
三個坑按順序出現,前一個解掉才會遇到下一個。
NotifyURL 和 ReturnURL 的差別
這個沒搞清楚會讓你永遠收不到付款結果:
| 欄位 | 用途 |
|---|---|
NotifyURL | 藍新 server → 你 server,背景 POST 付款結果 |
ReturnURL | 使用者瀏覽器跳轉回來 |
兩者都要設。NotifyURL 沒設好,使用者付了款,你的資料庫沒有紀錄,功能不會啟用。
本機開發時,藍新 webhook 打的是公開 URL,localhost 收不到。要測完整流程,用 Vercel Preview Deployment 最簡單——推一個 branch,自動有獨立 URL,把測試商店的 NotifyURL 設成那個 URL 就好。
一個實用的健診腳本
整合完成後,建了一個 npm run check-payment 腳本,每次部署前跑一次:
- 環境變數是否齊全
- HashKey / HashIV 長度是否正確
- Supabase 連線是否正常
- 待確認訂單表和購買紀錄表的結構是否正確
另外 npm run debug-newebpay 做 AES 的 self-roundtrip:把一段字串加密再解密,確認 key/iv 在本機是自洽的。
這兩個腳本在排查問題時省了不少時間——尤其是那種「金鑰到底有沒有設進去」的基礎確認。