我创建了一个 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.`);
}
};
要正确从 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 函数: