如何在PHP(CodeIgniter 4)中精准实现 "JSON.stringify" 的效果,以保持与Node/Express的 "JSON.stringify" 一致,用于SHA-256验证?
我在根据这份文档进行webhook验证时,无法正确比较一个 sha256。
API文档: docs.api.delyva.com
API指南: delyva博客
webhook验证(来自API provider/delyva 的示例):
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
app.post('/webhooks/delyva', (req, res) => {
const payload = JSON.stringify(req.body);
const signature = req.headers['x-delyvax-hmac-sha256'];
const expected = crypto.createHmac('sha256', process.env.DELYVA_API_SECRET)
.update(payload).digest('base64');
if (signature !== expected) return res.status(401).send('Invalid signature');
// handle event...
res.send('ok');
});
app.listen(3000, () => console.log('listening'));
我在CodeIgniter4中的代码(沙箱环境):
public function postTracking(): ResponseInterface
{
// Set your API secret
$apiSecret = 'dx6ed8365ad507451595c151b6b61e3dbdcfbb987f';
// Get the raw POST body
$parsedBody = $this->request->getJSON();
$payload = json_encode($parsedBody, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
// Get the signature from headers
$signature = $this->request->getHeaderLine('X-Delyvax-Hmac-Sha256');
// Calculate expected signature
$expected = base64_encode(
hash_hmac('sha256', $payload, $apiSecret, true)
);
// Handle event...
return $this->respond([
'status' => 'success',
'sign' => $signature,
'computed' => $expected,
]);
}
供你参考的变量:
$apiSecret = 'dx6ed8365ad507451595c151b6b61e3dbdcfbb987f';
X-Delyvax-Hmac-Sha256 = f8vncFdUbn+STSRVnS3oa6Rr+50tEGevZaYc7LrRncw=
JSON请求体(payload请求数据):
{
"id": "cfa3d6ff-3d37-4d8f-8d14-e7a064d520ab",
"companyId": "9e0aed8a-5c67-42a4-82b6-e01bf7687f31",
"userId": "06a8b310-fcfd-11f0-9353-1fee8250c30f",
"customerId": 126004,
"serviceId": 3234,
"serviceCode": "GDEXMY-WM",
"price": {
"amount": 0,
"currency": "MYR"
},
"revenue": {
"amount": 0,
"currency": "MYR"
},
"commission": {
"amount": 0,
"currency": "MYR"
},
"distance": {
"unit": "km",
"value": 1
},
"weight": {
"unit": "kg",
"value": 14.54
},
"cod": {
"amount": 0,
"currency": "MYR"
},
"consignmentNo": "DX0010241MY",
"paymentTerm": "debit",
"itemType": "PARCEL",
"itemTypeId": null,
"dimension": {
"unit": "cm",
"width": 0,
"height": 0,
"length": 0
},
"statusCode": 100,
"status": "created",
"createdAt": "2026-03-26T05:15:58.847Z",
"updatedAt": "2026-03-26T05:16:00.720Z",
"deletedAt": null,
"failedReason": "",
"promoCode": "",
"vehicleId": null,
"invoiceId": null,
"discountPrice": "0.00",
"discount": "0.00",
"personnelId": null,
"note": "",
"extId": "",
"extIdType": "",
"serviceAddon": null,
"rating": null,
"ratingNote": "",
"agentCommission": null,
"billing": {
"city": "",
"name": "gobisnes solutions enterprise",
"email": "[email protected]",
"phone": "60103213012",
"state": "",
"mobile": "",
"unitNo": "",
"country": "MY",
"address1": "",
"address2": "",
"postcode": ""
},
"paymentMethodId": null,
"cancelledReason": null,
"source": "",
"promoValue": "0.00",
"cron": 0,
"metadata": {
"taskId": 468160
},
"personnel": {},
"commodityId": "",
"pluginId": null,
"surcharge": "0.00",
"requestPickup": null,
"insurance": {
"amount": 0,
"currency": "MYR"
},
"extTrackUrl": "",
"smsRate": "0.00",
"smsCredit": 0,
"smsUsed": 0,
"nanoId": "0mSeliGu",
"extId2": "",
"tip": "0.00",
"ip": "43.205.48.39",
"platformFee": "0.00",
"branded": false,
"pickupId": null,
"codStatus": null,
"referenceNo": null,
"multiPcs": null,
"multiPcsMaxWeight": 0,
"codDueAt": null,
"waRate": "0.00",
"waCredit": 0,
"waUsed": 0,
"rtsReason": null,
"pickupManifestId": null,
"quoteId": null,
"subConsignmentNo": [],
"labelS3Key": null,
"claimStatus": null,
"claimNotes": null,
"claimCreatedAt": null,
"slaPickupMin": null,
"slaDeliveryMin": null,
"slaCodProcessingMin": null,
"slaCodSettlementMin": null,
"slaClaimsMin": null,
"deliveryDueAt": null,
"pickupDueAt": null,
"waypoint": [
{
"id": "wp_t44MJUwwaLhKVF6qVGDcMg",
"orderId": "cfa3d6ff-3d37-4d8f-8d14-e7a064d520ab",
"no": 1,
"inventory": [
{
"name": "Apparel",
"type": "PARCEL",
"price": {
"amount": 60,
"currency": "MYR"
},
"action": "P",
"weight": {
"unit": "kg",
"value": 14.54
},
"quantity": 1,
"dimension": {
"unit": "cm",
"width": 10,
"height": 33,
"length": 30
},
"description": "-"
}
],
"contact": {
"city": "Kuala Lumpur",
"name": "GoBisnes Solutions Enterprise",
"coord": {
"lat": "3.07591",
"lon": "101.70577"
},
"phone": "60103213012",
"state": "Kuala Lumpur",
"unitNo": "",
"country": "MY",
"address1": "Lot 33, Jln Pauh Punggah",
"address2": "Kg Malaysia Raya",
"postcode": "57100",
"sortCode": "MW01"
},
"type": "PICKUP",
"description": "-",
"status": "pending",
"note": "-",
"scheduledAt": "2026-03-26T05:15:59.023Z",
"createdAt": "2026-03-26T05:15:58.847Z",
"updatedAt": "2026-03-26T05:16:00.720Z",
"deletedAt": null,
"cash": {
"amount": 0,
"currency": "MYR"
},
"placeId": null,
"actualScheduledAt": null,
"startAt": "2026-03-27T02:00:59.023Z",
"actualStartAt": null,
"extId": ""
},
{
"id": "wp_kdJYATv24G6rk7RLHWZrfB",
"orderId": "cfa3d6ff-3d37-4d8f-8d14-e7a064d520ab",
"no": 2,
"inventory": [
{
"name": "Apparel",
"type": "PARCEL",
"price": {
"amount": 60,
"currency": "MYR"
},
"action": "D",
"weight": {
"unit": "kg",
"value": 14.54
},
"quantity": 1,
"dimension": {
"unit": "cm",
"width": 10,
"height": 33,
"length": 30
},
"description": "-"
}
],
"contact": {
"city": "Sungai Besi",
"name": "Nik",
"phone": "60132017736",
"state": "Kuala Lumpur",
"unitNo": "",
"country": "MY",
"address1": "Jalan kenangan",
"address2": "",
"postcode": "12345",
"sortCode": "MW01"
},
"type": "DROPOFF",
"description": "-",
"status": "pending",
"note": "-",
"scheduledAt": "2026-03-26T14:00:59.023Z",
"createdAt": "2026-03-26T05:15:58.847Z",
"updatedAt": "2026-03-26T05:16:00.720Z",
"deletedAt": null,
"cash": {
"amount": 0,
"currency": "MYR"
},
"placeId": null,
"actualScheduledAt": null,
"startAt": "2026-03-26T05:17:59.969Z",
"actualStartAt": null,
"extId": ""
}
],
"orderId": "cfa3d6ff-3d37-4d8f-8d14-e7a064d520ab",
"consignmentNos": [
"DX0010241MY"
]
}
我的目标是,确保我的PHP代码的逻辑与那里Node示例完全一致。
我正在使用PHP。 webhook的计算示例基于Node。
这个变量示例来自webhook服务器发送的请求。
现在,我还在摸索如何解决。哈希/签名仍然不匹配。
更新:解决方案(显然在Delyva集成中,Secret Key不是API Key。刚从他们的开发团队那里收到说明):
// Get raw request body (for signature verification)
$rawBody = $this->request->getBody();
// Get signature from header
$signature = $this->request->getHeaderLine('X-Delyvax-Hmac-Sha256');
// Parse JSON data
$jsonData = json_decode($rawBody, true);
// Get API secret from merchant record
$apiSecret = 'YOUR SECRET KEY';
// Verify signature (if signature header is present)
if ($signature) {
$expected = hash_hmac('sha256', $rawBody, $apiSecret, true);
$expectedBase64 = base64_encode($expected);
if (!hash_equals($expectedBase64, $signature)) {
log_message('error', 'Invalid webhook signature for order: ' . $courierOrderId);
return $this->failUnauthorized('Invalid signature');
}
log_message('info', 'Webhook signature verified for order: ' . $courierOrderId);
} else {
log_message('warning', 'No signature header received for order: ' . $courierOrderId);
// You may choose to reject or accept based on your security needs
}
解决方案
你可以按如下方式调试你的 post请求 的内容
$this->request->getPost('example');
你也可以使用这个 方法 获取关于该请求的信息
$this->request->getServer(['SERVER_PROTOCOL', 'REQUEST_URI']);
你可以使用CI4内置的方法根据你的逻辑处理状态码,而不是手动添加它们。
$this->response->setStatusCode(200, 'success');
$this->response->setStatusCode(201, 'created');
在你的数组中更改它们
$data = array(
'status' => 'success',
'sign' => $signature,
'computed' => $expected,
);
return $this->response->setJSON($data);
备选方案
很高兴你找到了这个解决方案。
在这里你使用的方式,是错误的
$parsedBody = $this->request->getJSON();
$payload = json_encode($parsedBody, ...);
# 1. Decode JSON -> PHP object
# 2. Re-encode JSON
当你这样做时,键的顺序、空格、Unicode等都会改变。所以 $payload !== original payload sent by server
要解决这个问题,你需要使用类似下面这样的方式
$rawBody = $this->request->getBody();
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。