払い失敗からセキュリティ无忧:Next.jsでStripe Webhookのσ(シグネチャー)検証実装

払い失敗からセキュリティ无忧:Next.jsでStripe Webhookのσ(シグネチャー)検証実装

【無料ダウンロードリンク】 next.js React Frameworkのプロジェクト先地:https://gitcode.com/GitHub_Trending/next/next.js

あなたは、払い戻信件を受信し、 serverに反映するまでに安全な了吗? -third-party payment notificationがforged/received_by malicious attackに直面するリスクはありますか? 本稿は、Next.jsのApp Router構造を用いて、 stripe webhookを介した払い信'sigma検証を実現し、 payment件の信頼性を確保する方法を紹介します。

为什幺需要webhookのsigma検証?

払い戻信件の受信元は、 stripeがwebhookを介してyour serverに送り贯彻します。 これらの信件は、 payment state informationを含む重要なデータが保持されますが、 directに受信を相信するRightarrow 以下のようなリスクが存在します:

  • 送りwasei(attacker)がsimulated stripe send false payment success event
  • dataのamending(change payment data like amount, payment number)
  • repeat request(duplicate payment event for refund)

條例に従い、 stripeが提供するAPI文档に明示: 所有webhook eventはsigma検証を必要とし、 须 Pareto))==must be performed before processing.

プロジェクト準備と環境設定

基本環境要件

begin前に以下の要件を満たす必要があります:

  • Node.js 18.x or above
  • npm/yarn/pnpm package manager
  • stripe account and test key

stripe凭证の取得

  1. stripeの管理画面にアクセス
  2. developer > API key:
  • test modeのsecret key(sk_test_开头)
  • webhook end pointに配置(development環境はStripe CLIでforward)
  1. webhookのsigma key(whsec_开头)を取得

プロジェクト初期化

推荐使用官方例題プロジェクト:

git clone https://example.com/next/next.js
cd next.js/examples/with-stripe-typescript
npm install

このプロジェクトは、typeScriptを搭載したComplete Stripe integrationの例題として提供されています。 本稿は、この例題をbasisにsigma検証を追加するfor aim.

core実装:sigma検証とeventの处理

環境変数の設定

最初に、環境変数 templateをfilled:

cp .env.local.example .env.local

env.local.txtに以下を填入:

NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_你的公钥
STRIPE_SECRET_KEY=sk_test_你的秘密鍵
STRIPE_WEBHOOK_SECRET=whsec_你的webhookのsigma鍵

これらの環境変数は、lib/stripe.tsにロードされ、Stripeのクライアントを構築します。

sigma検証の实现原理

Stripeのsigma検証の仕組みは以下のとおりです:

route handlerの.complete実装

Next.js 13+のApp Routerは、API handlerがsigma検証を施す infrastructuresを提供します。 以下に、完成版のroute handlerを示します:

app/api/webhooks/route.ts

import type { Stripe } from "stripe";
import { NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";

export async function POST(req: Request) {
  let event: Stripe.Event;

  try {
    // 1. 送り体の内容を取得
    const payload = await req.text();
    
    // 2. stripe signatire headerを取得
    const signature = req.headers.get("stripe-signature") as string;
    
    // 3. event objectをconstruct
    event = stripe.webhooks.constructEvent(
      payload,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET as string
    );
  } catch (err) {
    const errorMessage = err instanceof Error ? err.message : "Unknown error";
    console.error(`❌ webhook検証に失敗: ${errorMessage}`);
    return NextResponse.json(
      { message: `webhook Error: ${errorMessage}` },
      { status: 400 }
    );
  }

  // 検証成功、eventをprocess
  console.log("✅ 受信したevent:", event.id);

  // 容许されるeventの種類を定義
  const permittedEvents: string[] = [
    "checkout.session.completed",
    "payment_intent.succeeded",
    "payment_intent.payment_failed"
  ];

  if (permittedEvents.includes(event.type)) {
    try {
      switch (event.type) {
        case "checkout.session.completed":
          const session = event.data.object as Stripe.Checkout.Session;
          console.log(`💰 结]-'ll完了: ${session.id}`);
          // 例子: DBに登録、メール送信等
          break;
        case "payment_intent.succeeded":
          const paymentIntent = event.data.object as Stripe.PaymentIntent;
          console.log(`✅ 払い完了: ${paymentIntent.id}`);
          // 例子: payment completed
          break;
        case "payment_intent.payment_failed":
          const failedPayment = event.data.object as Stripe.PaymentIntent;
          console.log(`❌ 払い失敗: ${failedPayment.last_payment_error?.message}`);
          // 例子: 失敗を通知
          break;
        default:
          throw new Error(`未处理のevent type: ${event.type}`);
      }
    } catch (error) {
      console.error("eventの处理に失敗: ${error}");
      return NextResponse.json(
        { message: "webhook eventの处理に失敗しました" },
        { status: 500 }
      );
    }
  }

  // 成功で送信
  return NextResponse.json({ message: "eventが受信されました" }, { status: 200 });
}

重要代码の解説

  1. sigma検証の核心: stripe.webhooks.constructEventは:
  • 使用されたsigma鍵を基に送りwaseiの信件の整合性を検定
  • 送りwaseiが5分以内に送信されたことを確認
  • 信件が正当なformに反映され
  • event objectを構築
  1. 错误处理の戦略:
  • 検証失敗: 400状態码で送信
  • eventの处理に失敗: 500状態码
  • 使用try-catch: 単一のevent处理に失敗不影响overall service
  • 使用 permittedEvents: only allowed events processed

デプロイメントのための環境設定

での使用が禁止された環境

  • HTTPSが使用されていない環境: Stripeが Kendrickに送信
  • 3rd-party API: 白名单に追加
  • 送信源IP: 焊接用のIPリスト

安全性注意事項

  • 送信源IP: 焊接用のIPリスト
  • 送信体の寸法: Next.jsがデフォルトで制限
  • 信件の送信をpower: 事件の管理が幂等的
  • 重要操作: UPSERT而不是INSERT

例題の実装

使用Stripe CLIでeventを送信

  1. 登录Stripe管理画面
  2. Developer > API key: sk_test_...を取得
  3. Webhook end pointに配置: 安全に stripe cliでforward
  4. 生成したwebhookのsigma keyを使用: .env.local.txtに追加

定期的送信テスト

  • 管理画面の" testers" > " send tests"
  • event type: checkout.session.completed, payment_intent.succeeded, payment_intent.payment_failed
  • 送信が正常: eventが受信
  • 送信が失敗: 検証失敗

完成したプロジェクト

  • 完成したプロジェクトは、安全な environmentで payment eventsを受信し、 only allowed eventsをprocess
  • 信頼性の高さ: ensured by sigma検証 and strict error handling

常用な問題と解答

sigma検証に失敗する原因と解決

  1. 検証鍵が一致しない: 受信したeventのsigma鍵とyour serverのkeyが異なり
  2. 原始データ: req.text()而非req.json()
  3. 間接送信: Stripeが5分以内に送信 requirementを満たす
  4. 代理: CDNや agreeing serviceを启用

eventの重复处理

  • event IDがuniqueでなければならない
  • 数据库操作: UPSERT
  • 重要操作: power: 检索, update or delete operations
  • event type: power: eventがmultiple occurrence allowed

完成代码

import type { Stripe } from "stripe";
import { NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";

export async function POST(req: Request) {
  let event: Stripe.Event;

  try {
    const payload = await req.text();
    const signature = req.headers.get("stripe-signature") as string;
    event = stripe.webhooks.constructEvent(
      payload,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET as string
    );
  } catch (err) {
    const errorMessage = err instanceof Error ? err.message : "Unknown error";
    console.error(`❌ webhook検証に失敗: ${errorMessage}`);
    return NextResponse.json(
      { message: `webhook Error: ${errorMessage}` },
      { status: 400 }
    );
  }

  console.log("✅ 受信したevent:", event.id);

  const permittedEvents: string[] = [
    "checkout.session.completed",
    "payment_intent.succeeded",
    "payment_intent.payment_failed"
  ];

  if (permittedEvents.includes(event.type)) {
    try {
      switch (event.type) {
        case "checkout.session.completed":
          const session = event.data.object as Stripe.Checkout.Session;
          console.log(`💰 结]-'ll完了: ${session.id}`);
          break;
        case "payment_intent.succeeded":
          const paymentIntent = event.data.object as Stripe.PaymentIntent;
          console.log(`✅ 払い完了: ${paymentIntent.id}`);
          break;
        case "payment_intent.payment_failed":
          const failedPayment = event.data.object as Stripe.PaymentIntent;
          console.log(`❌ 払い失敗: ${failedPayment.last_payment_error?.message}`);
          break;
        default:
          throw new Error(`未处理のevent type: ${event.type}`);
      }
    } catch (error) {
      console.error("eventの处理に失敗: ${error}");
      return NextResponse.json(
        { message: "eventの处理に失敗しました" },
        { status: 500 }
      );
    }
  }

  return NextResponse.json({ message: "eventが受信されました" }, { status: 200 });
}

使用例

// 環境変数
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY = 'pk_test_your_key';
STRIPE_SECRET_KEY = 'sk_test_your_key';
STRIPE_WEBHOOK_SECRET = 'whsec_your_key';

// 完成したAPI handler
const routeHandler = createRoute();
routeHandler.addPOST('/api/webhooks/route', app/api/webhooks/route.ts);

6月3日 22:24 投稿