JavaScript >> Javascript 文檔 >  >> Tags >> web

如何安全地處理 Stripe Webhook

如何接收和解析 Stripe webhook、驗證它們的內容以及在你的應用程序中使用它們的數據。

開始使用

對於本教程,我們將使用 CheatCode Node.js Boilerplate 作為我們工作的起點。首先,讓我們從 Github 克隆一個副本:

終端

git clone https://github.com/cheatcode/nodejs-server-boilerplate

接下來,cd 進入項目並安裝其依賴項:

終端

cd nodejs-server-boilerplate && npm install

接下來,我們需要再添加一個依賴stripe 這將幫助我們解析和驗證我們從 Stripe 收到的 webhook:

終端

npm i stripe

最後,繼續啟動開發服務器:

終端

npm run dev

有了這個,我們就可以開始了。

獲取密鑰和 webhook 簽名密鑰

在深入研究代碼之前,我們需要做的第一件事是訪問兩件事:Stripe Secret Key 和 Webhook Signing Secret。

要獲得這些,您需要有一個現有的 Stripe 帳戶。如果您還沒有,可以在這裡註冊。訪問 Stripe 儀表板後,您可以繼續執行以下步驟。

登錄後,找到您的密鑰:

  1. 首先,在右上角,確保您已切換“測試模式”開關,使其亮起(在撰寫本文時,它會在激活時變為橙色)。
  2. 在該切換按鈕的左側,點擊“開發者”按鈕。
  3. 在下一頁的左側導航菜單中,選擇“API 密鑰”標籤。
  4. 在此頁面的“標準密鑰”塊下,找到您的“密鑰”並點擊“顯示測試密鑰”按鈕。
  5. 複製此密鑰(妥善保管,因為它用於與您的 Stripe 帳戶執行交易)。

接下來,一旦我們有了密鑰,我們需要打開我們剛剛克隆的項目並導航到 /settings-development.json 文件:

/settings-development.json

const settings = {
  "authentication": { ... },
  "databases": { ... },
  "smtp": { ... },
  "stripe": {
    "secretKey": "<Paste your secret key here>"
  },
  "support": { ... },
  "urls": { ... }
};

export default settings;

在此文件中,按字母順序排列在導出的 settings 底部附近 對象,我們要添加一個新屬性 stripe 並將其設置為具有單個屬性的對象:secretKey .對於此屬性的值,我們要粘貼您從上面的 Stripe 儀表板複製的密鑰。粘貼進去,然後保存這個文件。

接下來,我們需要再獲取一個值:我們的 webhook 簽名密鑰。為此,我們需要創建一個新端點。在 Stripe 儀表板的同一個“開發人員”選項卡中,從左側導航(您單擊“API 密鑰”的位置)找到“Webhooks”選項。

在此頁面上,您將看到創建第一個 webhook 端點的提示,或者添加 另一個 的選項 端點。單擊“添加端點”選項以顯示 webhook 配置屏幕。

在顯示自身的窗口中,我們要自定義“Endpoint URL”字段,然後從 Stripe 中選擇我們想要監聽的事件。

在 URL 字段中,我們希望使用運行我們的應用程序的域名。例如,如果我們在生產中,我們可能會執行類似 https://cheatcode.co/webhooks/stripe 的操作 .對於我們的示例,因為我們預計我們的應用程序會在 localhost 上運行,所以我們需要一個指向我們機器的 URL。

為此,強烈推薦使用 Ngrok 工具。這是一項免費服務(帶有附加功能的付費選項),可讓您創建通過互聯網返回計算機的隧道。對於我們的演示,https://tunnel.cheatcode.co/webhooks/stripe 我們正在使用的端點通過 Ngrok 指向我們的本地主機(免費計劃在 <randomId>.ngrok.io 獲得一個域 ,但付費計劃可以使用自定義域,如 tunnel.cheatcode.co 我們在這裡使用的)。

這裡重要的部分是域之後的部分:/webhooks/stripe .這是在定義的路由 我們希望將 webhook 發送到的應用程序。

接下來,在此下方,我們要單擊“選擇要收聽的事件”標題下的“選擇事件”按鈕。在下一個窗口中,Stripe 為我們提供了自定義它將發送到我們的端點的事件的選項。 默認情況下,他們會發送 all 的事件 類型,但建議您根據應用程序的需要對其進行自定義 .

對於我們的演示,我們將添加兩種事件類型:invoice.payment_succeeded (每當我們成功收到客戶付款時發送)和 invoice.payment_failed (每當客戶付款失敗時發送 )。

一旦你添加了這些——或者你喜歡的任何事件——點擊“添加端點”按鈕。

最後,要獲取您的 Webhook Signing Secret,請在創建端點後顯示的頁面中,在 URL 下方的行中,找到“Signing secret”框並單擊其中的“Reveal”鏈接。複製洩露的秘密。

/settings-development.json

...
  "stripe": {
    "secretKey": "",
    "webhookSecret": "<Paste your secret here>"
  },
  ...
}

回到你的 /settings-development.json 文件,在 stripe 下 我們之前添加的對象,添加一個附加屬性 webhookSecret 並將值設置為您剛剛從 Stripe 儀表板複製的密鑰。

添加中間件解析 webhook 請求

現在我們準備好進入代碼了。首先,為了確保我們正確地接收來自 Stripe 的 webhook,我們需要確保我們正在正確地處理我們將從 Stripe 收到的請求正文。

在我們上面克隆的項目中,我們想要導航到 /middleware/bodyParser.js 文件:

/middleware/bodyParser.js

import bodyParser from "body-parser";

export default (req, res, next) => {
  const contentType = req.headers["content-type"];

  if (req.headers["stripe-signature"]) {
    return bodyParser.raw({ type: "*/*", limit: "50mb" })(req, res, next);
  }
  
  if (contentType && contentType === "application/x-www-form-urlencoded") {
    return bodyParser.urlencoded({ extended: true })(req, res, next);
  }

  return bodyParser.json()(req, res, next);
};

在這個文件中,我們將找到樣板文件的現有正文解析器中間件。在這裡,您會發現一系列改變方式的條件語句 應根據請求的來源及其指定的 Content-Type 解析請求正文 標頭(這是 HTTP 請求中用於指定請求正文字段中數據格式的機制)。

一般來說,請求正文通常會以 JSON 數據或 URL 格式編碼數據的形式發送。這兩種類型已經在我們的中間件中處理了。

為了正確處理來自 Stripe 的請求,我們需要支持 raw HTTP 正文(這是 未解析的 HTTP 請求正文,通常是純文本或二進制數據)。對於 Stripe,我們需要這個,因為這是他們對自己的 webhook 驗證器函數的期望(我們稍後會看到)。

在上面的代碼中,我們添加了一個額外的 if 檢查 HTTP 標頭 stripe-signature 的語句 對我們應用程序的所有入站請求。上面導出的函數是通過/middleware/index.js調用的 在/index.js中將入站請求傳遞給我們的路由之前調用的文件 解決問題。

如果我們看到 HTTP 頭 stripe-signature ,我們知道我們正在接收來自 Stripe(一個 webhook)的入站請求,並且我們希望確保該請求的主體保持其原始狀態。為此,我們調用 .raw() bodyParser 上的方法 在我們文件頂部導入的對象(一個庫,它提供了一組特定於格式的函數,用於格式化 HTTP 請求正文數據)。

給它,我們傳遞一個選項對像說我們想要允許任何 */* 數據類型並將請求正文大小限制設置為 50mb .這樣可以確保任何大小的有效載荷都可以通過而不會觸發任何錯誤(請根據自己的需要隨意使用)。

最後,因為我們期望 .raw() 方法返回一個函數,我們立即調用該函數,傳入 req , res , 和 next 當 Express 調用我們的中間件時,參數通過 Express 傳遞給我們。

有了這個,我們準備好深入研究我們的 webhook 的實際處理程序。首先,我們需要添加 /webhooks/stripe 我們之前在 Stripe 儀表板上添加端點時提到的端點。

添加用於接收 webhook 的 Express 端點

這個很快。回想一下,之前在 Stripe 儀表板中,我們將端點分配給 http://tunnel.cheatcode.co/webhooks/stripe .現在,我們需要添加 /webhooks/stripe 在我們的應用程序中路由並將其連接到將解析和接收我們的 webhook 的處理程序代碼。

/api/index.js

import graphql from "./graphql/server";
import webhooks from "./webhooks";

export default (app) => {
  graphql(app);
  app.post("/webhooks/:service", webhooks);
};

上面,我們導出的函數是通過我們的 /index.js 調用的 middleware() 之後的文件 功能。此函數旨在為我們的應用程序設置 API 或路由。默認情況下,在這個樣板中,我們的 API 基於 GraphQL。 graphql() 我們在這裡看到的函數調用是無關緊要的,但是 app 它收到的論點很重要。

這是快遞 app 在我們的 /index.js 中創建的實例 文件。在這裡,我們要調用 .post() 該應用程序實例上的方法告訴 Express 我們想定義一個接收 HTTP POST 請求(我們期望從 Stripe 獲得的)的路由。在這裡,為了保持我們的代碼開放並適用於 Stripe 以及其他服務,我們將路由的 URL 定義為 /webhooks/:service 其中 :service 是一個可以與任何服務的名稱交換的參數(例如,/webhooks/stripe/webhooks/facebook )。

接下來我們要看看webhooks中存儲的函數 我們在文件頂部導入的變量並作為第二個參數傳遞給我們的路由。

添加 webhook 處理程序

我們實現的真正內容將是我們現在要編寫的處理函數。這是我們將完成兩件事的地方:

  1. 驗證我們從 Stripe 收到的 webhook 有效負載(以確保我們收到的數據實際上是 來自 條紋)。
  2. 根據 webhook 的類型(例如,invoice.payment_succeededinvoice.payment_failed )。

首先,我們將使用 stripe 編寫驗證代碼 我們之前安裝的包:

/api/webhooks/index.js

import _ from "lodash";
import settings from "../../lib/settings";
import { stripe } from "./stripe";

const handlers = {
  stripe(request) {
    // We'll implement our validation here.
  },
};

export default async (req, res, next) => {
  const handler = handlers[req.params.service];

  if (handler) {
    res.status(200).send("[200] Webhook received.");
    handler(req);
  } else {
    res.status(200).send("[200] Webhook received.");
  }
};

在上一步中,我們設置了一個 Express 路由,將變量 webhooks 傳遞給它 , 一個函數,作為對您定義的 URL 發出請求時調用的第二個參數,在本例中為 /webhooks/stripe .

在上面的代碼中,我們導出了一個接受三個參數的函數:req , res , 和 next .我們期待這些特定的參數,因為這些是 Express 將傳遞給路由回調函數的參數(在這種情況下,該回調函數是我們在此處導出並在 /api/index.js 中導入的函數 作為 webhooks )。

在該函數內部,我們需要確認我們正在接收對 stripe 的請求的服務 有相應的處理函數來支持它。這樣我們就不會收到來自互聯網的隨機請求(例如,有人向 /webhooks/hotdog 發送垃圾郵件 或 /webhooks/pizzahut )。

為了驗證我們 _have _ 一個處理函數,在我們導出的函數之上,我們定義了一個對象 handlers 並將 Stripe 定義為函數 on 該對象(在對像上定義的函數在 JavaScript 中稱為方法)。

對於該方法,我們希望接收傳遞給我們的路由的 HTTP 請求對象。回到我們導出的函數——路由回調——我們根據 req.params.service 確定調用哪個處理程序 價值。請記住,:service 在我們的 URL 中可以是任何東西,所以我們需要確保它存在 首先 在調用它之前。為此,我們使用 JavaScript 括號表示法來表示“在 handlers 對象,嘗試查找名稱等於 req.params.service 值的屬性 。”

對於我們的示例,我們期望 handlers.stripe 被定義為。如果那個 handler 存在,我們想向原始請求發出信號,表明已收到 webhook,然後 調用handler() 函數,傳入 req 我們要處理的。

/api/webhooks/index.js

import _ from "lodash";
import settings from "../../lib/settings";
import { webhooks as stripeWebhooks, stripe } from "./stripe";

const handlers = {
  stripe(request) {
    const data = stripe.webhooks.constructEvent(
      request.body,
      request.headers["stripe-signature"],
      settings.stripe.webhookSecret
    );

    if (!data) return null;

    const handler = stripeWebhooks[data.type];

    if (handler && typeof handler === "function") {
      return handler(data?.data?.object);
    }

    return `${data.type} is not supported.`;
  },
};

export default async (req, res, next) => {
  const handler = handlers[req.params.service];
  if (handler) {
    res.status(200).send("[200] Webhook received.");
    handler(req);
  } else {
    res.status(200).send("[200] Webhook received.");
  }
};

填寫我們的 stripe() 處理函數,在我們之前 我們從 Stripe 收到的任何與 webhook 相關的內容,我們都希望確保我們收到的 webhook 實際上是 來自 Stripe,而不是試圖向我們發送可疑數據的人。

為此,Stripe 在其 Node.js 庫中為我們提供了一個方便的函數——stripe 我們在教程開始時安裝的包——用於執行此任務:stripe.webhooks.constructEvent() .

在這裡,我們正在導入 stripe 的實例 從文件 /stripe/index.js 位於我們現有的 /api/webhooks 內 文件夾(我們將在下一節中設置它,所以現在我們假設它存在)。

我們希望該實例是一個包含 .webhooks.constructEvent() 的對象 我們在這裡調用的函數。該函數需要三個參數:

  1. request.body 我們在來自 Stripe 的 HTTP POST 請求中收到的。
  2. stripe-signature 我們從 Stripe 收到的 HTTP POST 請求的標頭。
  3. 我們的webhookSecret 我們設置並添加到我們的 /settings-development.json 早點存檔。

前兩個參數可以通過 HTTP request 立即提供給我們 (或 req 正如我們在其他地方引用的那樣)我們從 Stripe 收到的對象。對於 webhookSecret ,我們已將設置文件導入為 settings 在我們文件的頂部,利用 /lib/settings.js 中的內置設置加載器功能 根據我們當前的環境為我們挑選正確的設置(基於 process.env.NODE_ENV 的值 ,例如 developmentproduction )。

constructEvent() 內部 , Stripe 嘗試比較 stripe-signature 帶有接收到的 request.body 的散列副本的標頭 .這裡的想法是,如果此請求有效,則存儲在 stripe-signature 中的簽名 將等於 request.body 的散列版本 使用我們的 webhookSecret (僅當我們使用有效的 webhookSecret 時才有可能 並收到來自 Stripe 的合法請求)。

如果他們 匹配,我們期望 data 我們分配的變量 .constructEvent() 調用以包含我們從 Stripe 收到的 webhook。如果我們的驗證失敗,我們希望它為空。

如果它 為空,我們返回 null 來自我們的 stripe() 函數(這純粹是像徵性的,因為我們不期望函數有返回值)。

假設我們確實成功接收到了一些數據,接下來,我們想嘗試找到特定 type 的 webhook 處理程序 我們從 Stripe 收到的事件。在這裡,我們希望它可以在 type 中使用 data 上的屬性 對象。

在我們文件的頂部,我們還假設我們的 /stripe/index.js /api/webhooks 中的文件 將包含導出值 webhooks 我們將其重命名為 stripeWebhooks 將其導入頂部時(同樣,我們還沒有創建它——我們只是假設它存在)。

在該對像上,我們將在下一節中看到,我們期望一個與 webhook 名稱匹配的屬性 type 我們收到了(例如,invoice.payment_succeededinvoice.payment_failed )。

如果它確實 存在時,我們希望它向我們返回一個函數,該函數本身希望接收我們的 webhook 中包含的數據。假設確實如此,我們稱其為 handler() 函數,傳入data.data.object ——這裡,使用 JavaScript 可選鏈來確保 object 存在於 data 它上面的對象,它存在於 data 我們存儲了來自 Stripe 的已解析和驗證的請求正文。

總結一下,讓我們看看這個 /api/webhooks/stripe/index.js 我們一直在跳舞的文件。

添加函數來處理特定的 webhook 事件

現在,讓我們看看我們打算如何訪問我們上面提到的 Stripe 實例並處理我們的每個 webhook:

/api/webhooks/stripe/index.js

import Stripe from "stripe";
import settings from "../../../lib/settings";

import invoicePaymentSucceeded from "./invoice.payment_succeeded";
import invoicePaymentFailed from "./invoice.payment_failed";

export const webhooks = {
  "invoice.payment_succeeded": invoicePaymentSucceeded,
  "invoice.payment_failed": invoicePaymentFailed,
};

export const stripe = Stripe(settings.stripe.secretKey);

關注我們文件的底部,這裡我們可以看到 stripe 我們調用 stripe.webhooks.constructEvent() 的值 正在初始化。在這裡,我們取 Stripestripe 導入的函數 我們在教程開始時安裝的包被調用,傳入 secretKey 我們從 Stripe 儀表板中取出並添加到我們的 /settings-development.json 早點存檔。

在上面,我們可以看到 webhooks 我們導入並重命名為 stripeWebhooks 的對象 回到 /api/webhooks/index.js .在它上面,我們有兩種我們想要支持的事件類型 invoice.payment_succeededinvoice.payment_failed 定義,為每個傳遞一個函數,該函數的名稱對應於我們在接收到這些特定類型的事件時要運行的代碼。

目前,這些函數中的每一個都僅限於導出 console.log() 的函數 s 我們從 Stripe 收到的 webhook。這是我們想要使用 webhook 並更改我們的數據庫、創建我們收到的發票的副本或在我們的應用中觸發一些其他功能的地方。

/api/webhooks/stripe/invoice.payment_succeeded.js

export default (webhook) => {
  console.log(webhook);
};

而已!現在,讓我們通過前面提到的 Ngrok 工具啟動隧道,並接收來自 Stripe 的測試 webhook。

總結

在本教程中,我們學習瞭如何在 Stripe 上設置 webhook 端點,獲取 webhook 密鑰,然後使用 stripe.webhooks.constructEvent() 安全地驗證 webhook 功能。為此,我們在 Express 中設置了一個 HTTP POST 路由,並連接了一系列函數來幫助我們根據從 Stripe 接收到的事件類型來組織我們的 webhook 處理程序。


Tutorial JavaScript 教程
  1. 我用的是tinymce,是否可以只申請一個textarea

  2. Nodemon + Go 應用程序

  3. 什麼是渲染道具?

  4. 使用 TensorFlow.js 進行圖像識別

  5. 如何禁用 UWP 應用程序的高對比度模式

  6. 無限滾動或大量 dom 元素的性能?

  7. 您的 AWS 賬戶上的舊 AMI

  1. 從 Redux 遷移到 MobX - 減少樣板文件

  2. 如何從 JavaScript 字符串中刪除無效的 UTF-8 字符?

  3. 如何愛上順風

  4. 對賽普拉斯 E2E 測試充滿信心

  5. 我的 Hacktoberfest 個人挑戰:我在 7 天內編寫遊戲! (第 2 天)

  6. 如何在 JavaScript 中開始使用 Canvas 動畫

  7. JavaScript 代碼每日挑戰 #6

  1. React Js Google 面積和全堆積面積圖教程

  2. 10 個步驟學習 redux-toolkit

  3. 前端搭便車指南:JavaScript 框架

  4. 使用 CRUD 操作的簡單 JavaScipt 項目。