Проверка на потписите на webhooks
Проверувајте го заглавието X-Bilify-Signature за вашата адреса да прифаќа само вистински и свежи испораки. Готов код за PHP, Node.js и Python.
Секој што ја знае вашата webhook адреса може да ѝ прати барање. Секоја вистинска испорака од Bilify е потпишана со тајниот клуч на адресата, па вашиот примач може да докаже дека барањето доаѓа од Bilify, дека не е изменето по пат и дека не е старо барање пратено повторно. Направете ја оваа проверка на секоја испорака пред да постапите по неа.
Заглавието со потпис
Секоја испорака носи едно заглавие:
X-Bilify-Signature: t=1791100800,v1=b5600c284432b9dfcb76fb21f58d9d0e044986b635576e2826d8ab096a0aacfa
| Дел | Значење |
|---|---|
t |
Кога Bilify ја потпишал испораката, во Unix секунди |
v1 |
HMAC-SHA256 од потпишаната содржина, како хексадецимален запис со мали букви |
Потпишаната содржина е временскиот печат, точка и суровата содржина на барањето, точно онаква каква што пристигнала:
<t>.<сурова содржина>
Клучот е тајниот клуч за потпишување на адресата, целата вредност заедно со префиксот whsec_. Го видовте еднаш кога ја создадовте адресата; ако веќе го немате, користете Генерирај нов клуч во менито на адресата (видете Webhooks).
Како се проверува
- Прочитајте ги суровите бајти од содржината пред какво било парсирање на JSON. Ако парсираниот JSON повторно го претворите во текст, се менуваат празните места и знаците и потписот не се совпаѓа.
- Поделете го заглавието по
,, а секој дел по првото=. Земете гиt(само цифри) и вредноста или вредноститеv1. - Одбијте го барањето ако
tе повеќе од 300 секунди (5 минути) оддалечено од часовникот на вашиот сервер, во која било насока. Часовникот на серверот нека биде синхронизиран (NTP). - Пресметајте
HMAC-SHA256(secret, t + "." + rawBody)како хексадецимален запис со мали букви. - Споредете го со
v1со споредба во константно време. Никогаш не користете обично==за ова. - Ако се совпаѓа, брзо одговорете со
2xxи обработете го настанот. Ако не, одговорете со400и игнорирајте ја содржината.
Пример за проверка
Со тајниот клуч whsec_example, t=1791100800 и содржината {"event":"client.created"} (без празни места и без нов ред), очекуваниот v1 е b5600c284432b9dfcb76fb21f58d9d0e044986b635576e2826d8ab096a0aacfa. Со него проверете го вашиот код, со исклучена проверка на времето.
PHP
<?php
function bilify_verify(string $payload, string $header, string $secret, int $tolerance = 300): bool
{
$timestamp = null;
$signatures = [];
foreach (explode(',', $header) as $part) {
[$key, $value] = array_pad(explode('=', trim($part), 2), 2, null);
if ($key === 't' && $value !== null && ctype_digit($value)) {
$timestamp = (int) $value;
} elseif ($key === 'v1' && $value !== null && $value !== '') {
$signatures[] = $value;
}
}
if ($timestamp === null || $signatures === []) {
return false;
}
if (abs(time() - $timestamp) > $tolerance) {
return false;
}
$expected = hash_hmac('sha256', $timestamp.'.'.$payload, $secret);
foreach ($signatures as $signature) {
if (hash_equals($expected, $signature)) {
return true;
}
}
return false;
}
// Обична PHP адреса
$payload = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_BILIFY_SIGNATURE'] ?? '';
if (! bilify_verify($payload, $header, getenv('BILIFY_WEBHOOK_SECRET'))) {
http_response_code(400);
exit;
}
$event = json_decode($payload, true);
http_response_code(200);
// Тука ставете ги $event['event'] / $event['data'] на ред за обработка.
Во Laravel, како содржина проследете $request->getContent(), а како заглавие $request->header('X-Bilify-Signature', ''), и исклучете ја рутата од CSRF заштитата.
Node.js (Express)
const crypto = require('crypto');
const express = require('express');
function verifyBilify(rawBody, header, secret, toleranceSeconds = 300) {
if (!header) return false;
let timestamp = null;
const signatures = [];
for (const part of header.split(',')) {
const index = part.indexOf('=');
if (index === -1) continue;
const key = part.slice(0, index).trim();
const value = part.slice(index + 1).trim();
if (key === 't' && /^\d+$/.test(value)) timestamp = parseInt(value, 10);
else if (key === 'v1' && value) signatures.push(value);
}
if (timestamp === null || signatures.length === 0) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > toleranceSeconds) return false;
const expected = Buffer.from(
crypto.createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest('hex'),
'utf8'
);
return signatures.some((signature) => {
const given = Buffer.from(signature, 'utf8');
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
});
}
const app = express();
// express.raw ја чува содржината како Buffer: не користете express.json() на оваа рута.
app.post('/webhooks/bilify', express.raw({ type: 'application/json' }), (req, res) => {
if (!verifyBilify(req.body, req.get('X-Bilify-Signature'), process.env.BILIFY_WEBHOOK_SECRET)) {
return res.sendStatus(400);
}
const event = JSON.parse(req.body.toString('utf8'));
res.sendStatus(200);
// Тука обработете ги event.event / event.data.
});
app.listen(3000);
Python (Flask)
import hashlib
import hmac
import os
import time
from flask import Flask, abort, request
def verify_bilify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
if not header:
return False
timestamp = None
signatures = []
for part in header.split(","):
key, sep, value = part.strip().partition("=")
if not sep:
continue
if key == "t" and value.isdigit():
timestamp = int(value)
elif key == "v1" and value:
signatures.append(value)
if timestamp is None or not signatures:
return False
if abs(int(time.time()) - timestamp) > tolerance:
return False
signed = str(timestamp).encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, s) for s in signatures)
app = Flask(__name__)
@app.post("/webhooks/bilify")
def bilify_webhook():
raw = request.get_data() # сурови бајти, прочитани пред парсирање на JSON
if not verify_bilify(raw, request.headers.get("X-Bilify-Signature", ""), os.environ["BILIFY_WEBHOOK_SECRET"]):
abort(400)
event = request.get_json()
# Тука обработете ги event["event"] / event["data"].
return "", 200
Чести грешки
| Симптом | Причина |
|---|---|
| Ниеден потпис не поминува | Содржината е парсирана и повторно серијализирана пред пресметката, или рамката додала нов ред. Пресметувајте врз суровите бајти. |
| Ниеден потпис не поминува откако генериравте нов клуч | Примачот сè уште го има стариот клуч. Новиот клуч важи веднаш. |
| Потписот не поминува само понекогаш | Часовникот на серверот отстапува повеќе од 5 минути, или прокси сервер ја менува содржината (на пример ги кодира знаците поинаку). |
| Не поминуваат само повторно испорачаните настани | Повторните испораки се потпишуваат одново, со нов t и со тековниот клуч, па проверете го часовникот и клучот, а не оригиналниот временски печат. |
| Клучот изгледа точен, но HMAC се разликува | Користете го целиот клуч заедно со whsec_, како текст, без base64 декодирање. |
Дали ви помогна оваа страница?
Поврзани статии
Сè уште имате проблем?
Пишете ни и ќе ви одговориме во рок од еден работен ден.