AWS SAM 请求映射以检索返回未定义的 Stripe Webhook 的原始请求正文

问题描述 投票:0回答:1

我创建了一个 Web 应用程序,并希望集成 Stripe 以订阅该 Web 应用程序的高级功能。 使用 AWS SAM,我在每个 Stripe 文档的 template.yaml 片段中尝试了以下正文映射模板,以从 Stripe 检索原始请求正文,下面的 Lambda 函数返回

rawBody
未定义,但
headers
有一个值。 我错过了什么或做错了什么?我已经在堆栈溢出和互联网上搜索了答案,但没有运气。我很感激任何建议/方向。

template.yaml 的片段

  ApiGatewayRestApi:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Name: xxxxx
      Description: xxxxx
      EndpointConfiguration:
        Types:
          - REGIONAL

  ApiGatewayDeployment:
    Type: AWS::ApiGateway::Deployment
    DependsOn:
      - StripeWebhookMethod
    Properties:
      RestApiId: !Ref ApiGatewayRestApi
      StageName: Prod

  StripeWebhookResource:
    Type: AWS::ApiGateway::Resource
    Properties:
      ParentId: !GetAtt ApiGatewayRestApi.RootResourceId
      PathPart: stripe-webhook
      RestApiId: !Ref ApiGatewayRestApi

  StripeWebhookMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      AuthorizationType: NONE
      HttpMethod: POST
      ResourceId: !Ref StripeWebhookResource
      RestApiId: !Ref ApiGatewayRestApi
      Integration:
        IntegrationHttpMethod: POST
        Type: AWS_PROXY
        Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${stripeWebhookFunction.Arn}/invocations
        RequestTemplates:
          application/json: |
            {
              "method": "$context.httpMethod",
              "body": $input.json('$'),
              "rawBody": "$util.escapeJavaScript($input.body).replaceAll("\\'", "'")",
              "headers": {
                #foreach($param in $input.params().header.keySet())
                "$param": "$util.escapeJavaScript($input.params().header.get($param))"
                #if($foreach.hasNext),#end
                #end
              }
            }
        IntegrationResponses:
          - StatusCode: 200
            ResponseTemplates:
              application/json: "{}"
      MethodResponses:
        - StatusCode: 200

Lambda 函数

import { PutCommand, QueryCommand } from "@aws-sdk/lib-dynamodb";
import { getClient, formatResponse } from "../utils.mjs";
import stripePackage from "stripe";

const stripe = stripePackage(process.env.STRIPE_API_KEY);
const ddbDocClient = getClient();
const tableName = process.env.APP_TABLE;
const appVersion = process.env.APP_VERSION;

export const stripeWebhookHandler = async (event) => {
  const { rawBody, headers } = event;
  const signature = headers["Stripe-Signature"];
  const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;

  console.log("rawBody: ", rawBody);
  console.log("headers: ", headers);
  console.log("signature: ", signature);

  let stripeEvent;

  // Verify Stripe event is legit
  try {
    stripeEvent = stripe.webhooks.constructEvent(
      rawBody,
      signature,
      webhookSecret,
    );
  } catch (err) {
    console.error(`Webhook signature verification failed. ${err.message}`);
    return formatResponse(400, { error: err.message });
  }

  const { data, type: eventType } = stripeEvent;

  try {
    switch (eventType) {
      case "checkout.session.completed":
        await handleCheckoutSessionCompleted(data);
        break;

      case "customer.subscription.deleted":
        await handleCustomerSubscriptionDeleted(data);
        break;

      default:
        console.warn(`Unhandled event type: ${eventType}`);
    }
  } catch (e) {
    console.error(`Stripe error: ${e.message} | EVENT TYPE: ${eventType}`);
  }

  return formatResponse(200, {});
};

const handleCheckoutSessionCompleted = async (data) => {
  const session = await stripe.checkout.sessions.retrieve(data.object.id, {
    expand: ["line_items"],
  });
  const customerId = session.customer;
  const customer = await stripe.customers.retrieve(customerId);
  const priceId = session.line_items.data[0].price.id;

  if (!customer.email) {
    console.error("No user found");
    throw new Error("No user found");
  }

  const userParams = {
    TableName: tableName,
    IndexName: "GSI1",
    KeyConditionExpression: "GSI1PK = :email",
    ExpressionAttributeValues: {
      ":email": `USER#${appVersion}#EMAIL#${customer.email}#`,
    },
  };

  const result = await ddbDocClient
    .send(new QueryCommand(userParams))
    .promise();

  const userItem =
    result.Items.length > 0
      ? result.Items[0]
      : {
          PK: `FU#${appVersion}#${customer.email}#`,
          SK: "PROFILE",
          GSI1PK: `USER#${appVersion}#EMAIL#${customer.email}`,
          GSI1SK: `USER#${appVersion}#${customer.email}`,
          email: customer.email,
          name: customer.name,
          customerId,
          priceId,
          hasAccess: true,
        };

  if (result.Items.length > 0) {
    userItem.customerId = customerId;
    userItem.priceId = priceId;
    userItem.hasAccess = true;
  }

  await ddbDocClient.send(
    new PutCommand({ TableName: tableName, Item: userItem }),
  );
};

const handleCustomerSubscriptionDeleted = async (data) => {
  const customerId = data.object.customer;
  const customer = await stripe.customers.retrieve(customerId);

  if (!customer.email) {
    console.error("No user found");
    throw new Error("No user found");
  }

  const userParams = {
    TableName: tableName,
    IndexName: "GSI1",
    KeyConditionExpression: "GSI1PK = :email",
    ExpressionAttributeValues: {
      ":email": `USER#${appVersion}#EMAIL#${customer.email}#`,
    },
  };

  const result = await ddbDocClient
    .send(new QueryCommand(userParams))
    .promise();

  if (result.Items.length > 0) {
    const userItem = result.Items[0];
    userItem.hasAccess = false;

    await ddbDocClient.send(
      new PutCommand({ TableName: tableName, Item: userItem }),
    );
  } else {
    console.warn(`User with email ${customer.email} not found.`);
  }
};

aws-lambda stripe-payments aws-sam
1个回答
0
投票

要正确从 Stripe 捕获原始请求正文并将其传递到您的 AWS Lambda 函数,您应该对您的 AWS SAM 模板和 Lambda 函数进行一些调整。

首先,确保 Lambda 集成类型设置为 AWS_PROXY,这允许将整个请求(包括原始正文)传递到 Lambda 函数。但是,您的 RequestTemplates 不适用于 AWS_PROXY。相反,您应该删除 RequestTemplates 并允许代理集成将原始事件数据直接传递到 Lambda 函数。

这是调整后的 template.yaml 片段:

yaml 复制代码 ApiGatewayRestApi: 类型:AWS::ApiGateway::RestApi 特性: 姓名:xxxxx 描述:xxxxx 端点配置: 类型: - 区域

ApiGateway部署: 类型:AWS::ApiGateway::部署 依赖于取决于: - StripeWebhook方法 特性: RestApiId:!Ref ApiGatewayRestApi 艺名:Prod

StripeWebhook资源: 类型:AWS::ApiGateway::资源 特性: ParentId:!GetAtt ApiGatewayRestApi.RootResourceId 路径部分:stripe-webhook RestApiId:!Ref ApiGatewayRestApi

StripeWebhook方法: 类型:AWS::ApiGateway::方法 特性: 授权类型:无 Http方法:POST 资源 ID:!Ref StripeWebhookResource RestApiId:!Ref ApiGatewayRestApi 一体化: 集成Http方法:POST 类型:AWS_PROXY Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${stripeWebhookFunction.Arn}/incalls 集成响应: - 状态代码:200 响应模板: 应用程序/json:“{}” 方法响应: - 状态代码:200 接下来,在 Lambda 函数中,确保您正确访问 rawBody。使用 AWS_PROXY 时,主体字段包含字符串形式的原始请求主体。您可以直接从事件对象访问它。

按如下方式更新您的 Lambda 函数:

© www.soinside.com 2019 - 2024. All rights reserved.