Sermepa payment system 与 CodeIgniter 的 CSRF 问题
结论
不要全局关闭 CodeIgniter 的 CSRF 保护。应该为 Sermepa(在现有环境中也可能称为 Redsys)单独设置服务器回调地址,只将这个 URI 排除在 CSRF 校验之外,再使用支付平台提供的签名算法验证通知是否真实。
CSRF Token 用于保护浏览器表单和用户会话。Sermepa 的服务器拿不到网站会话中的 Token,因此它发送的后台 POST 无法通过常规的 CSRF 校验。支付回调的安全性应由签名验证、金额和订单核对、幂等处理以及 HTTPS 来保证。
CodeIgniter 3 的配置方式
假设回调地址是:
https://example.com/payment/sermepa-notification
在 application/config/config.php 中仅排除这个地址:
$config['csrf_protection'] = TRUE;
$config['csrf_exclude_uris'] = [
'payment/sermepa-notification',
];
对应的路由可以这样写:
$route['payment/sermepa-notification']['post'] =
'payment/sermepa_notification';
不同的 CodeIgniter 3 小版本可能对 HTTP Method 路由有不同支持。如果项目不支持这种写法,可以改用普通路由,并在控制器中明确限制请求方法:
$route['payment/sermepa-notification'] =
'payment/sermepa_notification';
控制器示例:
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class Payment extends CI_Controller
{
public function sermepa_notification()
{
if ($this->input->method(TRUE) !== 'POST') {
return $this->output
->set_status_header(405)
->set_header('Allow: POST')
->set_output('Method Not Allowed');
}
$signatureVersion = $this->input->post('Ds_SignatureVersion', TRUE);
$merchantParameters = $this->input->post('Ds_MerchantParameters', FALSE);
$receivedSignature = $this->input->post('Ds_Signature', FALSE);
if (
empty($signatureVersion) ||
empty($merchantParameters) ||
empty($receivedSignature)
) {
return $this->output
->set_status_header(400)
->set_output('Invalid notification');
}
try {
/*
* 使用 Sermepa/Redsys 针对当前协议版本提供的官方库:
*
* 1. 解码 Ds_MerchantParameters;
* 2. 根据商户密钥重新计算通知签名;
* 3. 以恒定时间方式比较签名;
* 4. 读取订单号、金额、币种和 Ds_Response。
*
* 不要仅因为 POST 中的 Ds_Response 表示成功,
* 就直接把订单标记为已支付。
*/
$notification = $this->verifySermepaNotification(
$signatureVersion,
$merchantParameters,
$receivedSignature
);
if (!$notification['signature_valid']) {
log_message('error', 'Invalid Sermepa notification signature');
return $this->output
->set_status_header(400)
->set_output('Invalid signature');
}
$order = $this->order_model->findByReference(
$notification['order']
);
if (
!$order ||
$order->amount_minor !== $notification['amount_minor'] ||
$order->currency !== $notification['currency']
) {
log_message(
'error',
'Sermepa notification does not match local order'
);
return $this->output
->set_status_header(400)
->set_output('Order mismatch');
}
if ($notification['payment_accepted']) {
/*
* 必须以订单号或交易标识做幂等更新。
* 同一通知可能被支付平台重复发送。
*/
$this->order_model->markPaidOnce(
$order->id,
$notification['transaction_id']
);
} else {
$this->order_model->recordRejectedPayment(
$order->id,
$notification
);
}
return $this->output
->set_content_type('text/plain')
->set_status_header(200)
->set_output('OK');
} catch (Throwable $e) {
log_message(
'error',
'Sermepa notification error: ' . $e->getMessage()
);
return $this->output
->set_status_header(500)
->set_output('Internal Server Error');
}
}
private function verifySermepaNotification(
$signatureVersion,
$merchantParameters,
$receivedSignature
) {
/*
* 此处应封装与当前 Sermepa/Redsys 接口版本匹配的官方签名库。
* 返回字段只是业务层示意,不代表官方库的固定 API。
*/
throw new RuntimeException('Signature verifier is not configured');
}
}
这里没有自行实现签名算法,因为 Sermepa/Redsys 的字段、编码和签名方式取决于实际接入的协议及版本。应该使用对应版本的官方 SDK 或接入文档,以免在 Base64、Base64 URL、安全填充或密钥派生环节引入不易发现的错误。
CodeIgniter 4 的配置方式
如果项目使用 CodeIgniter 4,可以在 app/Config/Filters.php 中为精确路径设置例外:
public array $globals = [
'before' => [
'csrf' => [
'except' => [
'payment/sermepa-notification',
],
],
],
];
然后将路由限制为 POST:
$routes->post(
'payment/sermepa-notification',
'Payment::sermepaNotification'
);
控制器仍然需要验证 Sermepa/Redsys 的签名。将路径排除在 CSRF 校验之外,只是允许请求进入应用,并不代表通知本身可信。
回调处理的正确顺序
收到通知后,按以下顺序处理:
- 只允许
POST,拒绝其他 HTTP Method。 - 检查签名版本、商户参数和签名字段是否齐全。
- 使用商户密钥验证签名。
- 签名通过后,再解读支付结果。
- 根据订单号查询本地订单。
- 核对金额、币种、商户号和终端号等关键数据。
- 使用订单号或平台交易号执行幂等更新。
- 处理成功后尽快返回 HTTP
200和简单的文本响应。
不要根据用户支付完成后的浏览器跳转确认付款。用户可能关闭浏览器,网络也可能中断,跳转参数还可能被篡改。订单状态应该以通过验证的服务器通知为准。
常见注意事项
CSRF 排除项要尽可能精确,不要排除整个控制器,也不要使用 payment/.* 这类宽泛路径。普通结账表单、登录表单和后台操作仍然需要 CSRF 保护。
回调地址通常不应该依赖登录状态、Session Cookie 或浏览器跳转逻辑。收到通知后,也不要返回登录页、HTML 错误页或 302 重定向。
支付平台可能会因为请求超时或没有收到预期响应而重试,所以数据库更新必须具备幂等性。例如,可以为平台交易号建立唯一索引,并在事务中检查订单是否已经支付。
IP 白名单可以作为额外限制,但不能替代签名验证。支付机构的出口 IP 可能发生变化,使用前应查看当前商户后台或官方接入资料。
调试时可以记录请求到达时间、订单号、签名验证结果和响应状态,但不要记录商户密钥、完整银行卡资料或其他敏感支付信息。
如果配置排除后仍然返回 500,需要继续检查 CodeIgniter 日志和 Web 服务器日志。CSRF 错误页、路由未命中、PHP 异常、WAF 拦截以及控制器依赖 Session,都可能让支付平台看到相同的 500 error。可以先确认回调路由能够接收 POST 并返回 200 OK,再接入真实的签名校验和订单更新逻辑。
备注:内容仅供参考。
