التحقق من التواقيع
كل ويب هوك ترسله TakeTheme موقّع بـمفتاح التوقيع الخاص بالعنوان (whsec_…). والتحقق من التوقيع يثبت أن الطلب صادر فعلًا عن TakeTheme وأن الحمولة لم تُعبَث بها أثناء النقل. تحقّق دائمًا قبل التصرف بناءً على الحمولة — وإلا استطاع كل من يكتشف رابطك تزوير الأحداث.
ترويسة التوقيع
يحمل كل تسليم ترويسة X-TakeTheme-Signature:
X-TakeTheme-Signature: t=1751371200,v1=5257a869e7b...
وتتكوّن من حقلين مفصولين بفاصلة:
| الحقل | الوصف |
|---|---|
t | طابع زمني Unix (بالثواني) للحظة توقيع التسليم. |
v1 | توقيع HMAC-SHA256 بترميز ست عشري للحمولة الموقّعة (الإصدار 1 من المخطط). |
كيف يُحتسب التوقيع
-
خذ الطابع الزمني
tمن الترويسة. -
خذ الحمولة الخام للطلب كما وصلت تمامًا (البايتات الخام — لا تعِد تسلسل JSON بعد تحليله).
-
كوّن الحمولة الموقّعة بدمجهما بنقطة:
signed_payload = "{t}" + "." + raw_body -
احسب
HMAC-SHA256(signing_secret, signed_payload)ورمّزه ست عشريًا. -
يجب أن تساوي النتيجة قيمة الحقل
v1.
يغطي التوقيع البايتات التي أرسلتها TakeTheme بالضبط. فإذا حلّل إطار العمل لديك الـ JSON ثم أعدت تسلسله، فقد يختلف ترتيب المفاتيح أو المسافات ويفشل التحقق. اقرأ الحمولة الخام قبل تحليل JSON (مثل express.raw وrequest.get_data() وfile_get_contents('php://input')).
خطوات التحقق
للتحقق من ويب هوك بأمان:
- استخرج
tوv1من ترويسةX-TakeTheme-Signature. - أعد حساب HMAC على
"{t}.{raw_body}"بمفتاح التوقيع لديك. - قارن قيمتك بـ
v1بمقارنة ثابتة الزمن لتجنّب هجمات التوقيت. - (موصى به) ارفض الطلب إذا تجاوز فارق
tعن الوقت الحالي نحو 5 دقائق، منعًا لإعادة تشغيل تسليمات مُلتقَطة.
أمثلة
- Node.js
- Python
- PHP
- Ruby
- Go
import crypto from "node:crypto";
const TOLERANCE_SECONDS = 5 * 60;
/**
* @param {Buffer|string} rawBody Raw request body (unparsed).
* @param {string} header Value of the X-TakeTheme-Signature header.
* @param {string} secret The endpoint signing secret (whsec_...).
*/
export function verifySignature(rawBody, header, secret) {
if (!header) return false;
const parts = Object.fromEntries(
header.split(",").map((kv) => kv.split("=").map((s) => s.trim()))
);
const timestamp = parts.t;
const signature = parts.v1;
if (!timestamp || !signature) return false;
// Optional but recommended: reject stale deliveries.
const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
if (age > TOLERANCE_SECONDS) return false;
const body = Buffer.isBuffer(rawBody) ? rawBody.toString("utf8") : rawBody;
const signedPayload = `${timestamp}.${body}`;
const expected = crypto
.createHmac("sha256", secret)
.update(signedPayload)
.digest("hex");
// Constant-time comparison.
const a = Buffer.from(expected, "hex");
const b = Buffer.from(signature, "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
import hashlib
import hmac
import time
TOLERANCE_SECONDS = 5 * 60
def verify_signature(raw_body: bytes, header: str, secret: str) -> bool:
if not header:
return False
parts = dict(
kv.strip().split("=", 1) for kv in header.split(",") if "=" in kv
)
timestamp = parts.get("t")
signature = parts.get("v1")
if not timestamp or not signature:
return False
# Optional but recommended: reject stale deliveries.
if abs(int(time.time()) - int(timestamp)) > TOLERANCE_SECONDS:
return False
signed_payload = f"{timestamp}.".encode() + raw_body
expected = hmac.new(
secret.encode(), signed_payload, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
<?php
function verify_signature(string $rawBody, ?string $header, string $secret): bool
{
if (!$header) {
return false;
}
$parts = [];
foreach (explode(',', $header) as $kv) {
[$k, $v] = array_pad(explode('=', trim($kv), 2), 2, null);
$parts[$k] = $v;
}
$timestamp = $parts['t'] ?? null;
$signature = $parts['v1'] ?? null;
if (!$timestamp || !$signature) {
return false;
}
// Optional but recommended: reject stale deliveries.
if (abs(time() - (int) $timestamp) > 5 * 60) {
return false;
}
$signedPayload = $timestamp . '.' . $rawBody;
$expected = hash_hmac('sha256', $signedPayload, $secret);
return hash_equals($expected, $signature);
}
// Usage:
// $raw = file_get_contents('php://input');
// $header = $_SERVER['HTTP_X_TAKETHEME_SIGNATURE'] ?? null;
// if (!verify_signature($raw, $header, getenv('TAKETHEME_WEBHOOK_SECRET'))) { http_response_code(400); exit; }
require "openssl"
TOLERANCE_SECONDS = 5 * 60
def verify_signature(raw_body, header, secret)
return false unless header
parts = header.split(",").each_with_object({}) do |kv, h|
k, v = kv.strip.split("=", 2)
h[k] = v
end
timestamp = parts["t"]
signature = parts["v1"]
return false unless timestamp && signature
# Optional but recommended: reject stale deliveries.
return false if (Time.now.to_i - timestamp.to_i).abs > TOLERANCE_SECONDS
signed_payload = "#{timestamp}.#{raw_body}"
expected = OpenSSL::HMAC.hexdigest("SHA256", secret, signed_payload)
# Constant-time comparison.
OpenSSL.secure_compare(expected, signature)
end
package webhooks
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"strconv"
"strings"
"time"
)
const toleranceSeconds = 5 * 60
// VerifySignature checks an X-TakeTheme-Signature header against the raw body.
func VerifySignature(rawBody []byte, header, secret string) bool {
if header == "" {
return false
}
var timestamp, signature string
for _, kv := range strings.Split(header, ",") {
pair := strings.SplitN(strings.TrimSpace(kv), "=", 2)
if len(pair) != 2 {
continue
}
switch pair[0] {
case "t":
timestamp = pair[1]
case "v1":
signature = pair[1]
}
}
if timestamp == "" || signature == "" {
return false
}
// Optional but recommended: reject stale deliveries.
ts, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
return false
}
if abs(time.Now().Unix()-ts) > toleranceSeconds {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(timestamp + "."))
mac.Write(rawBody)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signature))
}
func abs(n int64) int64 {
if n < 0 {
return -n
}
return n
}
تدوير مفتاح التوقيع
إذا انكشف مفتاح توقيع في أي وقت، فدوّره. ويُصدر التدوير مفتاحًا جديدًا ويُبطل القديم فورًا، لذا انشر المفتاح الجديد على عنوانك سريعًا.
curl -X POST "https://api.taketheme.com/api/v1/store/webhooks/endpoints/{id}/rotate-secret" \
-H "tt-api-key: tt_xxx"
{ "secret": "whsec_9f2c...41ab" }
وكما في استجابة الإنشاء، يُعاد المفتاح الجديد مرة واحدة فقط — احفظه في مكان آمن.
تحفظ TakeTheme مفاتيح التوقيع مشفّرة ولا يمكنها عرضها مجددًا بعد الإنشاء أو التدوير أبدًا. وإذا فقدت مفتاحًا، فدوّره للحصول على مفتاح جديد.
حل المشكلات
| العَرَض | السبب المرجّح |
|---|---|
| التوقيع لا يطابق أبدًا | تتحقق من JSON بعد تحليله وإعادة تسلسله. استخدم الحمولة الخام. |
| يعمل محليًا ويفشل في الإنتاج | وسيط أو برمجية وسيطة تعيد كتابة الحمولة. تحقّق قبل أي تحليل للحمولة. |
| إخفاقات متقطعة بعد تغيير المفتاح | المفتاح القديم ما زال منشورًا في مكان ما. أكمل نشر المفتاح المدوَّر. |
| كل التسليمات تُرفض كـ"قديمة" | انحراف ساعة الخادم. زامنها عبر NTP أو وسّع هامش الطابع الزمني لديك. |