WooCommerce Webhook返回401错误 认证头应在PHP代码何处添加
WooCommerce Webhook接收端401错误修复方案
首先明确核心逻辑:你是Webhook推送的接收方,不需要主动给请求添加Authentication Headers。认证头是WooCommerce向你的地址发POST请求时主动附带的,你需要做的是在接收逻辑最开头校验这个签名,而非自己构造请求头。
你日志里看到的woocommerce_rest_cannot_view错误不是你写的PHP代码返回的,是请求被拦截后转到WordPress REST接口返回的内容,说明请求根本没走到你写的业务逻辑里。
401错误排查清单
按顺序排查以下问题,覆盖90%以上的同类场景:
- Webhook投递URL配置错误:不要填成WooCommerce站点自身的REST API路径,必须填你写的PHP接收文件对应的公网可访问HTTPS地址
- 接收地址被访问控制拦截:包括站点开启了HTTP Basic Auth目录密码保护、配置了IP白名单、CDN/WAF规则拦截POST请求、安装了REST API禁用类插件,都会导致请求被拦截返回401
- Webhook密钥配置异常:检查WooCommerce后台Webhook编辑页的「Secret」字段,确认已经生成有效值,不要留空
- 服务器过滤请求头:部分Web服务器配置会过滤非标准HTTP头,导致代码拿不到WooCommerce附带的签名头
- HTTPS证书异常:确保接收端的HTTPS证书是正规CA签发的有效证书,不要用自签名证书,否则WooCommerce的投递请求会被拦截
正确的签名验证实现
把签名验证逻辑放在所有业务代码最前面,校验通过后再处理订单数据,修复后的完整代码如下:
<?php session_start(); include "../db_conn.php"; include "../config.php"; include "../functions.php"; // ---------------- Webhook签名验证逻辑开始 ---------------- // 替换成你WooCommerce Webhook配置页生成的Secret密钥 $webhook_secret = '填写你自己的Webhook Secret值'; // 兼容不同服务器环境的请求头获取方法 if (!function_exists('getallheaders')) { function getallheaders() { $headers = []; foreach ($_SERVER as $name => $value) { if (substr($name, 0, 5) == 'HTTP_') { $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; } } return $headers; } } $all_headers = getallheaders(); $signature = $all_headers['X-WC-Webhook-Signature'] ?? ''; // 读取原始请求体,必须使用未经过任何过滤转义的原始内容计算签名 $raw_payload = file_get_contents('php://input'); // 用密钥计算预期签名 $expected_signature = base64_encode(hash_hmac('sha256', $raw_payload, $webhook_secret, true)); // 签名校验不通过直接拒绝请求 if (!hash_equals($expected_signature, $signature)) { http_response_code(403); exit('Invalid webhook signature'); } // ---------------- 签名验证逻辑结束 ---------------- // 校验通过后再解析数据处理业务 if($json = json_decode($raw_payload)) { $order = $json; file_put_contents("request_confirm.txt","request arrived"); if($order->status == "completed") { $purchased_hours=0; foreach ($order->line_items as $line_item) { if($line_item->sku != "") { $hours=$line_item->sku * $line_item->quantity; $purchased_hours+=$hours; } else { $purchased_hours+=$line_item->quantity; } } $purchased_at=$order->date_created; $user=$order->billing->first_name." ".$order->billing->last_name; $order_id=$order->id; $data="Purchased_hours: ".$purchased_hours." Cutomer name: ".$user." purchased_at: ".$purchased_at; $purchased_seconds=$purchased_hours*3600; try{ // 修复原代码SQL注入风险,所有参数改用预处理绑定 $sql = "INSERT INTO user_hours (purchased_seconds, name, purchased_at, order_id) SELECT ". "?,?,?,? WHERE NOT EXISTS (SELECT 1 FROM user_hours". " WHERE order_id = ?)"; $insert= $pdo->prepare($sql); $insert->execute([$purchased_seconds, $user, $purchased_at, $order_id, $order_id]); } catch (\Exception $exception) { file_put_contents("error.txt",$exception." order number: ".$order->id); } } $txt = "<pre>".print_r($order, true)."</pre>"; file_put_contents("test_webhook.txt",$txt); file_put_contents("test_webhook_collected_data.txt",$data); // 处理完成必须返回200状态码,否则WooCommerce会判定投递失败反复重试 http_response_code(200); exit; } else { http_response_code(400); exit('Invalid request payload'); } ?>
额外注意事项
- 业务逻辑处理完成后必须返回200状态码,不要输出无关内容,否则WooCommerce会启动重试机制,默认最多重试15次
- 原代码中直接把
$order_id拼接进SQL语句存在SQL注入风险,上述代码已经修正为全参数预处理绑定,直接替换即可 - 如果使用Nginx作为Web服务器,确认配置中没有过滤非标准HTTP头的规则,必要时可以添加
fastcgi_param HTTP_X_WC_WEBHOOK_SIGNATURE $http_x_wc_webhook_signature;配置透传签名头 - 测试时可以先把签名验证逻辑临时注释,确认请求能正常到达代码、能正确写入日志,再开启签名校验,方便定位问题
内容的提问来源于stack exchange,提问作者west
相关产品推荐
相关产品推荐

