表单提交后如何将数据传递给 CGI script
明确结论
可以。应由 WordPress 服务器向 CCBill 的 CGI 接口发送 HTTP 请求,不要让浏览器通过 window.open() 直接访问接口。
请求链路应改为:
用户提交捐款表单
→ WordPress 在服务器端验证请求
→ WordPress 服务器调用 billingApi.cgi
→ WordPress 根据接口响应显示结果
这种方式不会触发浏览器的弹窗拦截,也不需要让用户离开网站。CCBill 通常会将请求识别为来自网站服务器的公网出口 IP,因此可以把这个 IP 加入白名单。
API 的 username、password、subscriptionId 等敏感信息不能出现在 JavaScript 或页面 URL 中。原来的做法会将这些信息暴露给访问者,还可能被浏览器历史记录、代理服务器和访问日志保存。
为什么弹窗方式会使用用户的 IP
window.open(url) 在用户的浏览器中执行。即使 URL 是由 WordPress 页面生成的,真正连接 bill.ccbill.com 的仍然是用户的浏览器:
用户浏览器 → CCBill
所以,支付处理方看到的是用户的公网 IP,而不是 WordPress 服务器的 IP。
需要将其改成服务器间通信:
用户浏览器 → WordPress → CCBill
WordPress 收到表单后,通过 PHP 中的 WordPress HTTP API 发起请求。此时,CCBill 通常看到的是服务器或其网络出口的公网 IP。
如果网站位于代理、NAT、云平台或动态出口网络之后,实际出口 IP 可能不同于服务器网卡上的 IP。需要向主机商确认应该将哪个公网 IP 加入白名单。
推荐的实现方式
可以将表单提交到 WordPress 自带的 admin-post.php,再由处理函数调用 CCBill。这样无需单独创建 PHP 接口,还能直接使用 WordPress 的登录状态、nonce 和重定向功能。
1. 输出捐款表单
function donation_shortcode()
{
if (!is_user_logged_in()) {
return '<p>Please log in before making a donation.</p>';
}
ob_start();
?>
<form method="post"
action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
<input type="hidden" name="action" value="process_ccbill_donation">
<?php wp_nonce_field(
'process_ccbill_donation',
'ccbill_donation_nonce'
); ?>
<label for="donation_amount">Amount ($)</label>
<input
type="number"
id="donation_amount"
name="donation_amount"
min="1.00"
step="0.01"
required
>
<button type="submit">Make Donation</button>
</form>
<?php
return ob_get_clean();
}
add_shortcode('ccbill_donation', 'donation_shortcode');
这里的 action 字段不是远程接口的操作名称,而是用于告诉 WordPress 应该调用哪个表单处理函数。
2. 在服务器端处理提交
add_action(
'admin_post_process_ccbill_donation',
'process_ccbill_donation'
);
function process_ccbill_donation()
{
if (!is_user_logged_in()) {
wp_die('You must be logged in.', '', ['response' => 403]);
}
check_admin_referer(
'process_ccbill_donation',
'ccbill_donation_nonce'
);
$user_id = get_current_user_id();
$subscription_id = get_user_meta(
$user_id,
'get_donate_details_id',
true
);
if (empty($subscription_id)) {
wp_die('No previous transaction was found.');
}
$raw_amount = isset($_POST['donation_amount'])
? wp_unslash($_POST['donation_amount'])
: '';
if (!is_numeric($raw_amount)) {
wp_die('Invalid donation amount.');
}
$amount = round((float) $raw_amount, 2);
if ($amount <= 0) {
wp_die('The donation amount must be greater than zero.');
}
/*
* 示例上限。实际值应按业务规则和支付接口限制设置。
*/
if ($amount > 10000) {
wp_die('The donation amount is too large.');
}
$request_data = [
'clientAccnum' => '900000',
'username' => CCBILL_API_USERNAME,
'password' => CCBILL_API_PASSWORD,
'action' => 'chargeByPreviousTransactionId',
'newClientAccnum' => '900000',
'newClientSubacc' => '0000',
'sharedAuthentication' => '1',
'initialPrice' => number_format($amount, 2, '.', ''),
'initialPeriod' => '365',
'recurringPrice' => '0',
'recurringPeriod' => '0',
'rebills' => '0',
'subscriptionId' => $subscription_id,
'currencyCode' => '840',
];
$response = wp_remote_post(
'https://bill.ccbill.com/jpost/billingApi.cgi',
[
'timeout' => 30,
'body' => $request_data,
]
);
if (is_wp_error($response)) {
error_log(
'CCBill request failed: ' .
$response->get_error_message()
);
wp_die(
'The payment service could not be reached. Please try again later.'
);
}
$status_code = wp_remote_retrieve_response_code($response);
$body = wp_remote_retrieve_body($response);
/*
* 必须按照 CCBill 对该接口返回格式的定义判断是否成功。
* HTTP 200 只说明收到了 HTTP 响应,不一定代表扣款成功。
*/
$payment_succeeded = ccbill_response_indicates_success(
$status_code,
$body
);
if (!$payment_succeeded) {
error_log(
'CCBill rejected donation. HTTP status: ' .
(int) $status_code
);
wp_die(
'The donation could not be processed. No success was confirmed.'
);
}
$redirect_url = add_query_arg(
'donation',
'success',
home_url('/donation/')
);
wp_safe_redirect($redirect_url);
exit;
}
ccbill_response_indicates_success() 应根据 CCBill 当前接口文档规定的响应格式来实现。接口可能返回键值对、特定文本或其他结构,不能只凭 HTTP 状态码 200 判断扣款成功。
可以先保守地定义这个函数,但之后必须加入真实的成功判断条件:
function ccbill_response_indicates_success($status_code, $body)
{
if ((int) $status_code !== 200) {
return false;
}
/*
* 不要在不知道正式响应格式的情况下直接返回 true。
* 请在这里解析 CCBill 文档规定的成功字段。
*/
return false;
}
如果接口只接受 URL 查询参数
首先需要确认 billingApi.cgi 是否接受针对该操作的 HTTP POST 请求。如果接口明确要求使用查询字符串,可以改用 wp_remote_get()。由于请求仍由服务器发出,来源 IP 依然是服务器的出口 IP:
$url = add_query_arg(
$request_data,
'https://bill.ccbill.com/jpost/billingApi.cgi'
);
$response = wp_remote_get(
$url,
[
'timeout' => 30,
]
);
不过,GET 请求会把密码、订阅编号和金额放进完整 URL。这些信息可能被 Web 服务器、代理或监控系统记录。只要接口支持 POST,就应优先使用 wp_remote_post()。
不要凭猜测修改请求方法或参数名称,应以当前使用的 CCBill API 文档和账户配置为准。
API 凭据应放在哪里
不要将 API 用户名和密码直接写入短代码输出,也不能放进 JavaScript。至少应把它们放在 wp-config.php 中:
define('CCBILL_API_USERNAME', 'your-api-username');
define('CCBILL_API_PASSWORD', 'your-api-password');
然后在插件中引用这些常量:
'username' => CCBILL_API_USERNAME,
'password' => CCBILL_API_PASSWORD,
如果示例中的凭据曾出现在真实网站的前端,即使后来已经删除,也应立即更换。它们可能已经被浏览器历史记录、缓存、日志或第三方分析系统保存。
需要特别注意的安全问题
不要相信浏览器提交的交易编号
subscriptionId、客户账号和子账号应根据当前登录用户,从服务器端数据库中读取。不要通过隐藏表单字段接收后直接使用,否则用户可以修改这些字段,尝试对其他交易发起操作。
金额必须在服务器端验证
HTML 中的 min、step 和 required 只能改善用户体验,不能提供安全保障。攻击者可以绕过表单直接提交请求,所以 PHP 端仍需检查:
- 输入是否为合法数字;
- 金额是否大于零;
- 小数位和货币格式是否正确;
- 是否超过业务允许的上限;
- CCBill 是否另有最低或最高金额限制。
金额最好使用十进制字符串或最小货币单位处理,避免在复杂的财务计算中直接依赖浮点数。
防止重复扣款
用户可能刷新页面、双击按钮,或在请求超时后再次提交。因此,支付操作需要加入幂等或重复请求保护,例如:
- 提交前生成唯一的本地请求编号;
- 将请求状态记录为
pending; - 成功后记录远程交易编号;
- 不得使用相同请求编号再次扣款;
- 请求超时时先查询原交易状态,不要立即重试。
如果 CCBill 提供幂等键、交易查询或回调机制,应优先使用官方机制。
不要把完整接口响应写入公开日志
支付响应可能包含订阅编号、交易编号或其他敏感信息。日志只应保留排错所需的信息,并遮盖敏感字段。任何情况下都不能把 API 密码写入日志。
超时不等于失败
服务器请求超时只表示 WordPress 没有及时收到结果,不能证明 CCBill 没有完成扣款。遇到超时时,不要立即重新发送同一笔扣款请求,否则可能造成重复收费。
使用回调确认最终状态
如果 CCBill 为该流程提供 webhook、postback 或交易状态查询接口,应通过这些机制确认最终支付状态。用户浏览器显示的页面只能用于提示,不能作为财务记账的唯一依据。
AJAX 是否必要
不使用弹窗并不等于必须使用 AJAX。让普通表单提交到 WordPress,再由服务器请求 CCBill,就可以解决 IP 白名单和弹窗问题。
如果希望提交后页面完全不刷新,可以在此基础上使用 WordPress AJAX 或 REST API。无论前端采用普通表单、fetch() 还是其他方式,原则都一样:浏览器只能把捐款请求发送到本站,包含 CCBill 凭据的请求必须由 WordPress 服务器发出。
备注:内容仅供参考。
