

# 教程：使用 Lambda 函数 URL 创建 Webhooook 端点
<a name="urls-webhook-tutorial"></a>

在本教程中，您将创建 Lambda 函数 URL 来实现 Webhooook 端点。Webhook 是一种轻量级、事件驱动的通信，其使用 HTTP 在应用程序之间自动发送数据。您可以使用 Webhook 接收有关在其他系统中发生的事件的即时更新，例如网站上有新客户注册、处理付款或上传文件时。

通过 Lambda，可以使用 Lambda 函数 URL 或 API Gateway 实现网络挂钩。对于不需要高级授权或请求验证等功能的简单 Webhook 来说，函数 URL 是一个不错的选择。

**提示**  
如果您不确定哪种解决方案最适合您的特定用例，请参阅[选择使用 HTTP 请求调用 Lambda 函数的方法](furls-http-invoke-decision.md)。

## 先决条件
<a name="urls-webhook-tutorial-prereqs"></a>

要完成本教程，您必须在本地计算机上安装 Python（版本 3.8 或更高版本）或 Node.js（版本 18 或更高版本）。

为使用 HTTP 请求测试端点，本教程使用了 [curl](https://curl.se/)，这是一种命令行工具，您可以使用它来通过各种网络协议传输数据。如果您还没有安装该工具，则请参阅 [curl 文档](https://curl.se/docs/install.html)以了解如何安装该工具。

## 创建 Lambda 函数
<a name="urls-webhook-tutorial-function"></a>

首先创建 Lambda 函数，在将 HTTP 请求发送到 Webhooook 端点时会运行该函数。在此示例中，每当提交付款时，发送应用程序都会发送更新，并在 HTTP 请求的正文中指示付款是否成功。Lambda 函数会解析该请求并根据付款状态采取行动。在此示例中，代码仅打印付款的订单 ID，但在实际应用程序中，您可以将订单添加到数据库或发送通知。

该函数还实现了用于 Webhook 的最常见的身份验证方法，即基于哈希的消息身份验证（HMAC）。使用这种方法，发送和接收应用程序共享一个密钥。发送应用程序使用哈希算法将此密钥与消息内容一起生成唯一签名，并将该签名作为 HTTP 标头包含在 Webhook 请求中。然后，接收应用程序会重复此步骤，使用密钥生成签名，并将结果值与请求标头中发送的签名进行比较。如果结果匹配，则该请求被视为合法。

将 Lambda 控制台与 Python 或 Node.js 运行时一起使用创建该函数。

------
#### [ Python ]

**创建 Lambda 函数**

1. 打开 Lamba 控制台的 [Functions](https://console.aws.amazon.com/lambda/home#/functions)（函数）页面。

1. 通过执行以下操作创建基本“Hello world”函数：

   1. 选择**创建函数**。

   1. 选择**从头开始编写**。

   1. 对于**函数名称**，请输入 **myLambdaWebhook**。

   1. 对于**运行时**，选择 **python3.14**。

   1. 选择**创建函数**。

1. 在**代码源**窗格中，通过复制并粘贴以下内容替换现有代码：

   ```
   import json
   import hmac
   import hashlib
   import os
   
   def lambda_handler(event, context):
       
       # Get the webhook secret from environment variables
       webhook_secret = os.environ['WEBHOOK_SECRET']
       
       # Verify the webhook signature
       if not verify_signature(event, webhook_secret):
           return {
               'statusCode': 401,
               'body': json.dumps({'error': 'Invalid signature'})
           }
       
       try:
           # Parse the webhook payload
           payload = json.loads(event['body'])
           
           # Handle different event types
           event_type = payload.get('type')
           
           if event_type == 'payment.success':
               # Handle successful payment
               order_id = payload.get('orderId')
               print(f"Processing successful payment for order {order_id}")
               
               # Add your business logic here
               # For example, update database, send notifications, etc.
               
           elif event_type == 'payment.failed':
               # Handle failed payment
               order_id = payload.get('orderId')
               print(f"Processing failed payment for order {order_id}")
               
               # Add your business logic here
               
           else:
               print(f"Received unhandled event type: {event_type}")
           
           # Return success response
           return {
               'statusCode': 200,
               'body': json.dumps({'received': True})
           }
           
       except json.JSONDecodeError:
           return {
               'statusCode': 400,
               'body': json.dumps({'error': 'Invalid JSON payload'})
           }
       except Exception as e:
           print(f"Error processing webhook: {e}")
           return {
               'statusCode': 500,
               'body': json.dumps({'error': 'Internal server error'})
           }
   
   def verify_signature(event, webhook_secret):
       """
       Verify the webhook signature using HMAC
       """
       try:
           # Get the signature from headers
           signature = event['headers'].get('x-webhook-signature')
   
           if not signature:
               print("Error: Missing webhook signature in headers")
               return False
           
           # Get the raw body (return an empty string if the body key doesn't exist)
           body = event.get('body', '')
           
           # Create HMAC using the secret key
           expected_signature = hmac.new(
               webhook_secret.encode('utf-8'),
               body.encode('utf-8'),
               hashlib.sha256
           ).hexdigest()
           
           # Compare the expected signature with the received signature to authenticate the message
           is_valid = hmac.compare_digest(signature, expected_signature)
           if not is_valid:
               print(f"Error: Invalid signature. Received: {signature}, Expected: {expected_signature}")
               return False
               
           return True
       except Exception as e:
           print(f"Error verifying signature: {e}")
           return False
   ```

1. 在**部署**部分，选择**部署**以更新函数的代码。

------
#### [ Node.js ]

**创建 Lambda 函数**

1. 打开 Lamba 控制台的[函数](https://console.aws.amazon.com/lambda/home#/functions)页面。

1. 通过执行以下操作创建基本“Hello world”函数：

   1. 选择**创建函数**。

   1. 选择**从头开始编写**。

   1. 对于**函数名称**，请输入 **myLambdaWebhook**。

   1. 对于**运行时**，选择 **nodejs24.x**。

   1. 选择**创建函数**。

1. 在**代码源**窗格中，通过复制并粘贴以下内容替换现有代码：

   ```
   import crypto from 'crypto';
   
   export const handler = async (event, context) => {
       // Get the webhook secret from environment variables
       const webhookSecret = process.env.WEBHOOK_SECRET;
   
       // Verify the webhook signature
       if (!verifySignature(event, webhookSecret)) {
           return {
               statusCode: 401,
               body: JSON.stringify({ error: 'Invalid signature' })
           };
       }
   
       try {
           // Parse the webhook payload
           const payload = JSON.parse(event.body);
   
           // Handle different event types
           const eventType = payload.type;
   
           switch (eventType) {
               case 'payment.success': {
                   // Handle successful payment
                   const orderId = payload.orderId;
                   console.log(`Processing successful payment for order ${orderId}`);
   
                   // Add your business logic here
                   // For example, update database, send notifications, etc.
                   break;
               }
   
               case 'payment.failed': {
                   // Handle failed payment
                   const orderId = payload.orderId;
                   console.log(`Processing failed payment for order ${orderId}`);
   
                   // Add your business logic here
                   break;
               }
   
               default:
                   console.log(`Received unhandled event type: ${eventType}`);
           }
   
           // Return success response
           return {
               statusCode: 200,
               body: JSON.stringify({ received: true })
           };
   
       } catch (error) {
           if (error instanceof SyntaxError) {
               // Handle JSON parsing errors
               return {
                   statusCode: 400,
                   body: JSON.stringify({ error: 'Invalid JSON payload' })
               };
           }
   
           // Handle all other errors
           console.error('Error processing webhook:', error);
           return {
               statusCode: 500,
               body: JSON.stringify({ error: 'Internal server error' })
           };
       }
   };
   
   // Verify the webhook signature using HMAC
   
   const verifySignature = (event, webhookSecret) => {
       try {
           // Get the signature from headers
           const signature = event.headers['x-webhook-signature'];
     
           if (!signature) {
               console.log('No signature found in headers:', event.headers);
               return false;
           }
     
           // Get the raw body (return an empty string if the body key doesn't exist)
           const body = event.body || '';
     
           // Create HMAC using the secret key
           const hmac = crypto.createHmac('sha256', webhookSecret);
           const expectedSignature = hmac.update(body).digest('hex');
     
           // Compare expected and received signatures
           const isValid = signature === expectedSignature;
           if (!isValid) {
               console.log(`Invalid signature. Received: ${signature}, Expected: ${expectedSignature}`);
               return false;
           }
           
           return true;
       } catch (error) {
           console.error('Error during signature verification:', error);
           return false;
       }
     };
   ```

1. 在**部署**部分，选择**部署**以更新函数的代码。

------

## 创建密钥
<a name="urls-webhook-tutorial-key"></a>

为了让 Lambda 函数对 Webhook 请求进行身份验证，其使用与调用应用程序共享的密钥。在此示例中，密钥存储在环境变量中。在生产应用程序中，切勿在函数代码中包含密码等敏感信息。相反，[创建一个 AWS Secrets Manager 密钥](https://docs.aws.amazon.com/secretsmanager/latest/userguide/create_secret.html)，然后[使用 AWS 参数和密钥 Lambda 扩展](with-secrets-manager.md)在 Lambda 函数中检索您的凭证。

**创建并存储 Webhook 密钥**

1. 使用加密安全随机数生成器生成一个随机的长字符串。您可以在 Python 或 Node.js 中使用以下代码片段来生成并打印一个 32 个字符的密钥，也可以使用自己的首选方法。

------
#### [ Python ]

**Example 生成密钥的代码**  

   ```
   import secrets
   webhook_secret = secrets.token_urlsafe(32)
   print(webhook_secret)
   ```

------
#### [ Node.js ]

**Example 生成密钥的代码（ES 模块格式）**  

   ```
   import crypto from 'crypto';
   let webhookSecret = crypto.randomBytes(32).toString('base64');
   console.log(webhookSecret)
   ```

------

1. 通过执行以下操作，将生成的字符串存储为函数的环境变量：

   1. 在函数的**配置**选项卡中，选择**环境变量**。

   1. 选择**编辑**。

   1. 选择**添加环境变量**。

   1. 在**键**中输入 **WEBHOOK\$1SECRET**，然后在**值**中输入您在上一步生成的密钥。

   1. 选择**保存**。

您将需要在本教程后面的部分中再次使用此密钥来测试函数，因此请现在记下该密钥。

## 创建函数 URL 端点
<a name="urls-webhook-tutorial-furl"></a>

使用 Lambda 函数 URL 为您的 Webhook 创建端点。由于您使用身份验证类型 `NONE` 来创建具有公共访问权限的端点，因此任何人都可以通过该 URL 调用您的函数。要了解有关控制函数 URL 访问权限的更多信息，请参阅[控制对 Lambda 函数 URL 的访问](urls-auth.md)。如果您的 Webhook 需要更多高级身份验证选项，请考虑使用 API Gateway。

**创建函数 URL 端点**

1. 在函数的**配置**选项卡中，选择**函数 URL**。

1. 选择 **Create function URL**（创建函数 URL）。

1. 对于**身份验证类型**，选择**无**。

1. 选择**保存**。

您刚刚创建的函数 URL 的端点将在**函数 URL** 窗格中显示。复制此端点以在本教程后面的部分中使用。

## 在控制台中测试函数
<a name="urls-webhook-tutorial-test-console"></a>

在使用 HTTP 请求通过 URL 端点调用您的函数之前，请在控制台中对其进行测试，以确认您的代码按预期工作。

要在控制台中验证该函数，请首先使用本教程前面的部分中生成的密钥计算一个 Webhook 签名，其中包含以下测试 JSON 有效载荷：

```
{
    "type": "payment.success", 
    "orderId": "1234",
    "amount": "99.99"
}
```

使用以下任一 Python 或 Node.js 代码示例，采用您自己的密钥计算 Webhook 签名。

------
#### [ Python ]

**计算 Webhook 签名**

1. 将以下代码另存为名为 `calculate_signature.py` 的文件。将该代码中的 Webhooook 密钥替换为您自己的值。

   ```
   import secrets
   import hmac
   import json
   import hashlib
   
   webhook_secret = "arlbSDCP86n_1H90s0fL_Qb2NAHBIBQOyGI0X4Zay4M"
   
   body = json.dumps({"type": "payment.success", "orderId": "1234", "amount": "99.99"})
   
   signature = hmac.new(
               webhook_secret.encode('utf-8'),
               body.encode('utf-8'),
               hashlib.sha256
           ).hexdigest()
   
   print(signature)
   ```

1. 通过从保存该代码的同一目录运行以下命令来计算签名。复制该代码输出的签名。

   ```
   python calculate_signature.py
   ```

------
#### [ Node.js ]

**计算 Webhook 签名**

1. 将以下代码另存为名为 `calculate_signature.mjs` 的文件。将该代码中的 Webhooook 密钥替换为您自己的值。

   ```
   import crypto from 'crypto';
   
   const webhookSecret = "arlbSDCP86n_1H90s0fL_Qb2NAHBIBQOyGI0X4Zay4M"
   const body = "{\"type\": \"payment.success\", \"orderId\": \"1234\", \"amount\": \"99.99\"}";
   
   let hmac = crypto.createHmac('sha256', webhookSecret);
   let signature = hmac.update(body).digest('hex');
   
   console.log(signature);
   ```

1. 通过从保存该代码的同一目录运行以下命令来计算签名。复制该代码输出的签名。

   ```
   node calculate_signature.mjs
   ```

------

现在，您可以在控制台中使用测试 HTTP 请求测试您的函数代码。

**在控制台中测试函数**

1. 为您的函数选择**代码**选项卡。

1. 在**测试事件**部分中，选择**创建新测试事件**。

1. 对于 **Event Name (事件名称)**，输入 **myEvent**。

1. 通过将以下内容复制并粘贴到**事件 JSON** 窗格中来替换现有 JSON。将 Webhooook 签名替换为您在上一步中计算得出的值。

   ```
   {
     "headers": {
       "Content-Type": "application/json",
       "x-webhook-signature": "2d672e7a0423fab740fbc040e801d1241f2df32d2ffd8989617a599486553e2a"
     },
     "body": "{\"type\": \"payment.success\", \"orderId\": \"1234\", \"amount\": \"99.99\"}"
   }
   ```

1. 选择**保存**。

1. 选择**调用**。

   您应该可以看到类似于如下所示的输出内容：

------
#### [ Python ]

   ```
   Status: Succeeded
   Test Event Name: myEvent
   
   Response:
   {
     "statusCode": 200,
     "body": "{\"received\": true}"
   }
   
   Function Logs:
   START RequestId: 50cc0788-d70e-453a-9a22-ceaa210e8ac6 Version: $LATEST
   Processing successful payment for order 1234
   END RequestId: 50cc0788-d70e-453a-9a22-ceaa210e8ac6
   REPORT RequestId: 50cc0788-d70e-453a-9a22-ceaa210e8ac6	Duration: 1.55 ms	Billed Duration: 2 ms	Memory Size: 128 MB	Max Memory Used: 36 MB	Init Duration: 136.32 ms
   ```

------
#### [ Node.js ]

   ```
   Status: Succeeded
   Test Event Name: myEvent
   
   Response:
   {
     "statusCode": 200,
     "body": "{\"received\":true}"
   }
   
   Function Logs:
   START RequestId: e54fe6c7-1df9-4f05-a4c4-0f71cacd64f4 Version: $LATEST
   2025-01-10T18:05:42.062Z	e54fe6c7-1df9-4f05-a4c4-0f71cacd64f4	INFO	Processing successful payment for order 1234
   END RequestId: e54fe6c7-1df9-4f05-a4c4-0f71cacd64f4
   REPORT RequestId: e54fe6c7-1df9-4f05-a4c4-0f71cacd64f4	Duration: 60.10 ms	Billed Duration: 61 ms	Memory Size: 128 MB	Max Memory Used: 72 MB	Init Duration: 174.46 ms
   
   Request ID: e54fe6c7-1df9-4f05-a4c4-0f71cacd64f4
   ```

------

## 使用 HTTP 请求测试函数
<a name="urls-webhook-tutorial-test-curl"></a>

使用 curl 命令行工具测试您的 Webhook 端点。

**使用 HTTP 请求测试函数**

1. 在终端或 shell 程序中，运行以下 curl 命令。将该 URL 替换为您自己函数 URL 端点的值，并将 Webhook 签名替换为您使用自己的密钥计算得出的签名。

   ```
   curl -X POST https://ryqgmbx5xjzxahif6frvzikpre0bpvpf.lambda-url.us-west-2.on.aws/ \
   -H "Content-Type: application/json" \
   -H "x-webhook-signature: d5f52b76ffba65ff60ea73da67bdf1fc5825d4db56b5d3ffa0b64b7cb85ef48b" \
   -d '{"type": "payment.success", "orderId": "1234", "amount": "99.99"}'
   ```

   您应看到以下输出：

   ```
   {"received": true}
   ```

1. 通过执行以下操作，检查函数的 CloudWatch 日志，确认其是否已正确解析有效载荷：

   1. 在 Amazon CloudWatch 控制台中，打开[日志组](https://console.aws.amazon.com/cloudwatch/home#logsV2:log-groups)页面。

   1. 选择函数的日志组（`/aws/lambda/myLambdaWebhook`）。

   1. 选择最新的日志流。

      您应该可以在函数的日志中看到类似于如下所示的输出内容：

------
#### [ Python ]

      ```
      Processing successful payment for order 1234
      ```

------
#### [ Node.js ]

      ```
      2025-01-10T18:05:42.062Z e54fe6c7-1df9-4f05-a4c4-0f71cacd64f4 INFO Processing successful payment for order 1234
      ```

------

1. 运行以下 curl 命令，确认您的代码检测到无效的签名。将该 URL 替换为您自己的函数 URL 端点。

   ```
   curl -X POST https://ryqgmbx5xjzxahif6frvzikpre0bpvpf.lambda-url.us-west-2.on.aws/ \
   -H "Content-Type: application/json" \
   -H "x-webhook-signature: abcdefg" \
   -d '{"type": "payment.success", "orderId": "1234", "amount": "99.99"}'
   ```

   您应看到以下输出：

   ```
   {"error": "Invalid signature"}
   ```

## 清除资源
<a name="urls-webhook-tutorial-cleanup"></a>

除非您想要保留为本教程创建的资源，否则可立即将其删除。通过删除您不再使用的 AWS 资源，可防止您的 AWS 账户 产生不必要的费用。

**删除 Lambda 函数**

1. 打开 Lamba 控制台的 [Functions（函数）页面](https://console.aws.amazon.com/lambda/home#/functions)。

1. 选择您创建的函数。

1. 依次选择**操作**和**删除**。

1. 在文本输入字段中键入 **confirm**，然后选择**删除**。

在控制台中创建 Lambda 函数时，Lambda 还会为您的函数创建一个[执行角色](lambda-intro-execution-role.md)。

**删除执行角色**

1. 打开 IAM 控制台的[角色页面](https://console.aws.amazon.com/iam/home#/roles)。

1. 选择 Lambda 创建的执行角色。该角色的名称格式为 `myLambdaWebhook-role-<random string>`。

1. 选择**删除**。

1. 在文本输入字段中输入角色名称，然后选择**删除**。