

本文為英文版的機器翻譯版本，如內容有任何歧義或不一致之處，概以英文版為準。

# 教學課程：使用 Lambda 函式 URL 建立 Webhook 端點
<a name="urls-webhook-tutorial"></a>

在本教學課程中，您將建立 Lambda 函式 URL 來實作 Webhook 端點。Webhook 是一種輕量型的事件驅動型通訊機制，能透過 HTTP 自動在應用程式間傳輸資料。您可以使用 Webhook 來接收有關其他系統中發生事件的即時更新，例如當新客戶在網站上註冊、處理付款或上傳檔案時。

使用 Lambda 時，可以透過 Lambda 函式 URL 或 API Gateway 來實作 Webhook。對於不需要進階授權或請求驗證等功能的簡單 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>

首先建立當 HTTP 請求傳送至 Webhook 端點時執行的 Lambda 函式。在此範例中，傳送端應用程式會在付款提交時傳送更新，並在 HTTP 請求內文中指出付款是否成功。Lambda 函式會剖析請求，再依據付款狀態採取動作。在此範例中，程式碼僅會列印付款的訂單 ID，但在實際應用程式中，您可以將訂單新增至資料庫或傳送通知。

函式還實作了 Webhook 最常用的身分驗證方法，即雜湊訊息驗證碼 (HMAC)。使用此方法時，傳送端與接收端應用程式皆會共用一個秘密金鑰。傳送端應用程式使用雜湊演算法，利用此金鑰和訊息內容產生唯一簽章，並將該簽章作為 HTTP 標頭包含在 Webhook 請求中。然後，接收端應用程式會重複此步驟，使用秘密金鑰產生簽章，並將結果值與請求標頭中傳送的簽章進行比較。如果結果相符，請求會視為合法。

使用 Lambda 主控台並選擇 Python 或 Node.js 執行時期來建立函式。

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

**建立 Lambda 函式**

1. 開啟 Lambda 主控台中的[函數頁面](https://console.aws.amazon.com/lambda/home#/functions)。

1. 執行下列動作，建立基本的 'Hello world' 函式：

   1. 選擇 **Create function (建立函數)**。

   1. 選取**從頭開始撰寫**。

   1. 針對**函數名稱**，請輸入 **myLambdaWebhook**。

   1. 針對**執行期**，選取 **python3.14**。

   1. 選擇 **Create function (建立函數)**。

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. 在 **DEPLOY** 區段中，選擇**部署**以更新函數的程式碼。

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

**建立 Lambda 函式**

1. 開啟 Lambda 主控台中的[函數頁面](https://console.aws.amazon.com/lambda/home#/functions)。

1. 執行下列動作，建立基本的 'Hello world' 函式：

   1. 選擇 **Create function (建立函數)**。

   1. 選取**從頭開始撰寫**。

   1. 針對**函數名稱**，請輸入 **myLambdaWebhook**。

   1. 針對**執行期**，選取 **nodejs24.x。**

   1. 選擇 **Create function (建立函數)**。

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. 在 **DEPLOY** 區段中，選擇**部署**以更新函數的程式碼。

------

## 建立秘密金鑰
<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. 選擇 **Add environment variable** (新增環境變數)。

   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. 在**身分驗證類型**欄位中，選取 **NONE**。

1. 選擇**儲存**。

剛剛建立的函式 URL 端點會顯示在**函式 URL** 窗格中。複製端點，在教學課程後續部分使用。

## 在主控台中測試函式
<a name="urls-webhook-tutorial-test-console"></a>

在透過 URL 端點使用 HTTP 請求調用函式之前，請先在主控台中測試，確認程式碼如預期運作。

若要在主控台中驗證函式，需先使用之前在教學課程中產生的秘密，搭配以下測試用 JSON 承載資料計算 Webhook 簽章：

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

利用下列其中一個以 Python 或 Node.js 編寫的程式碼範例，並使用您自己的秘密來計算 Webhook 簽章。

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

**計算 Webhook 簽章**

1. 將下列程式碼儲存為名為 `calculate_signature.py` 的檔案。將程式碼中的 Webhook 秘密取代為您自己的值。

   ```
   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` 的檔案。將程式碼中的 Webhook 秘密取代為您自己的值。

   ```
   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。將 Webhook 簽章取代為在上一個步驟中計算得出的值。

   ```
   {
     "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. 開啟 Lambda 主控台中的 [函數頁面](https://console.aws.amazon.com/lambda/home#/functions)。

1. 選擇您建立的函數。

1. 選擇 **Actions** (動作)、**Delete** (刪除)。

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. 在文字輸入欄位中輸入角色的名稱，然後選擇**刪除**。