如何在PHP(CodeIgniter 4)中精准实现 "JSON.stringify" 的效果,以保持与Node/Express的 "JSON.stringify" 一致,用于SHA-256验证?

前端开发 2026-07-11

我在根据这份文档进行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导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章