分類
Python

使用自定義 x-signature 在Cloudflare中來保護API

在 Cloudflare 中, 若是有一組自己的 API, 想要簡單地透過 Cloudflare 來保護, 有很多方法, 不過大多需要是付費的版本才能提供, 像是 API Shield.

若是想要快速而輕量地達成這個 API的保護, 可以透過 Worker 來檢查 x-signature 的 header 來實現.

在 Cloudflare 的 Workers & Pages 介面,  Create application, 然後給定一個名稱如 api-check, 然後在右上角的 Edit Code 進入程式編輯器.

輸入以下程式碼:

const EXPIRY_SECONDS = 300;

export default {
  async fetch(request, env, ctx) {
    // 1. 將您的秘密金鑰設定在環境變數 (安全起見建議後續設定為 Secret)
    const SECRET_KEY = env.API_SECRET_KEY;
    
    const url = new URL(request.url);
    const apiPath = url.pathname;
    const host = url.host;

    // 2. 擷取外部夥伴傳入的 X-Signature 標頭
    const xSignature = request.headers.get("X-Signature");
    if (!xSignature) {
      return new Response(JSON.stringify({ error: "Unauthorized: Missing X-Signature header" }), {
        status: 401,
        headers: { "Content-Type": "application/json" }
      });
    }

    // 3. 解析標頭格式 (預期格式: token-timestamp)
    const parts = xSignature.split("-");
    if (parts.length !== 2) {
      return new Response(JSON.stringify({ error: "Unauthorized: Invalid signature format" }), {
        status: 401,
        headers: { "Content-Type": "application/json" }
      });
    }
    const [clientToken, timestampStr] = parts;
    const clientTimestamp = parseInt(timestampStr, 10);

    // 4. 驗證時間戳記是否超時 (防止重放攻擊)
    const currentTimestamp = Math.floor(Date.now() / 1000);
    if (Math.abs(currentTimestamp - clientTimestamp) > EXPIRY_SECONDS) {
      return new Response(JSON.stringify({ error: "Unauthorized: Signature expired" }), {
        status: 401,
        headers: { "Content-Type": "application/json" }
      });
    }

    // 5. 使用 Web Crypto API 在本端重新計算 HMAC-SHA256
    // 簽章組合公式:路徑 + 網域 + 時間戳記 (符合原 WAF v0 規範)
    const messageStr = `${apiPath}${host}${clientTimestamp}`;
    
    try {
      const encoder = new TextEncoder();
      const keyData = encoder.encode(SECRET_KEY);
      const messageData = encoder.encode(messageStr);

      const cryptoKey = await crypto.subtle.importKey(
        "raw",
        keyData,
        { name: "HMAC", hash: "SHA-256" },
        false,
        ["sign"]
      );

      const signatureBuffer = await crypto.subtle.sign("HMAC", cryptoKey, messageData);
      
      // 將計算結果轉為十六進位字串 (Hex)
      const serverToken = Array.from(new Uint8Array(signatureBuffer))
        .map(b => b.toString(16).padStart(2, '0'))
        .join('');

      // 6. 比對客戶端與伺服器端的 Token 是否一致
      if (clientToken !== serverToken) {
        return new Response(JSON.stringify({ error: "Unauthorized: Signature mismatch" }), {
          status: 401,
          headers: { "Content-Type": "application/json" }
        });
      }
    } catch (err) {
      return new Response(JSON.stringify({ error: "Internal Server Error during verification" }), {
        status: 500,
        headers: { "Content-Type": "application/json" }
      });
    }

    // 7. 驗證通過,將請求原封不動轉發 (Proxy) 給後端的真實源站
    // 註:fetch(request) 會保留原始的 URL、Header 與 Body
    return fetch(request);
  }
};

其中的 EXPIRY_SECONDS 就是限制這個 x-signature 效期只有 300 秒, 你可以依實際的狀況增加或減少這個過期秒數設定, 完成後按下 deploy.

然後我們要將這個程式中用到的 API_SECRET_KEY 放在 Workers & Pages 的 Setting 中的 Variables, 所以按下左上角回到 Workers & Pages 頁面, 切換到 Setting 頁籤, 在最上面的 Variables & Secrets 中, add 一個 Secret 的變數, 內容請自訂, 如 x-123-456-789, 一樣要 deploy 即可, 到這裡完成 Workers 的程式準備.

接下來要透過 Domains 來綁定對應的 API 服務, 若你原本的 API 服務是在 myapi.example.com/api/v1/xxxx 這樣, 你可以在 Workers & Pages 下的 Domains 中, Add Domain, 設定對應的主域名, 然後選擇 Route Pattern, 指定 myapi.example.com/api/* 這樣的路徑, 按下 add route 完成設定, 到這裡就把 server 端在 Cloudflare 上的配置設定完成了.

接下來就是 Client 的部分了, 由於有自定義了 x-signature header, 所以我們就是利用自己的程式來實作這個部分, 把 x-signature header 做出來, 再進行訪問即可, Python 程式如下:

import hmac
import hashlib
import time
import requests
from urllib.parse import urlparse

# 1. 設定基本資料
SECRET_KEY = "x-123-456-789"

api_url = "https://myapi.example.com/api/v1/users"

parsed_url = urlparse(api_url)
api_path = parsed_url.path
host = parsed_url.hostname

# 2. 取得當前 Unix 時間戳記 (秒)
current_time = int(time.time())

# 3. 依照 Cloudflare 規範組合簽章訊息 (路徑 + 時間戳記)
# message = f"{api_path}{current_time}"
message = f"{api_path}{host}{current_time}"

# 4. 使用 HMAC-SHA256 進行雜湊,並轉為十六進位字串
token = hmac.new(
    SECRET_KEY.encode('utf-8'),
    message.encode('utf-8'),
    hashlib.sha256
).hexdigest()

# 5. 將「Token」與「時間戳記」用格式 `token-timestamp` 組合起來
# 這是 Cloudflare is_timed_hmac_valid_v1 函數要求的標準格式
x_signature = f"{token}-{current_time}"

# 6. 帶入 Header 發出請求
headers = {
    "X-Signature": x_signature
}

response = requests.get(api_url, headers=headers)
print(response.json())

其中的 SECRET_KEY 與前面 Cloudflare 中設的 API_SECRET_KEY 設定為一樣的就可以了, 這樣就可以在不傳遞 shared key, 又可以有效地透過這個簡易的 x-signature header 來訪問 API了.

 

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *