CakePHP 1.3 如何实现 HTTPS 安全支付与 HTTP 跳转
结论
CakePHP 1.3 的 SecurityComponent 可以限制支付相关 action 只能通过 HTTPS 访问,但不会自动把页面中的普通链接改成 HTTP,也不能代替完整的协议切换逻辑。
比较合适的处理方式如下:
- 使用
SecurityComponent::requireSecure()保护支付和确认 action。 - 集中配置 HTTP 和 HTTPS 的基础地址,避免在模板中到处写域名。
- 支付完成后,如果确认页不需要使用 HTTPS,直接重定向到 HTTP 确认页。进入该页面后,相对链接会自动沿用 HTTP。
- 如果确认页必须使用 HTTPS,header/footer 中指向普通站点的链接需要生成绝对 HTTP URL。可以将这部分逻辑封装到自定义 Helper 中。
使用 SecurityComponent 限制支付页面
在 CakePHP 1.3 中,可以只对支付相关 action 调用 requireSecure():
<?php
class BookingsController extends AppController
{
public $components = array('Security', 'Session');
public function beforeFilter()
{
parent::beforeFilter();
$this->Security->blackHoleCallback = 'securityError';
$this->Security->requireSecure('payment', 'processPayment');
}
public function securityError($type)
{
if ($type === 'secure' && $this->RequestHandler->isGet()) {
return $this->redirect(
'https://secure.domain.com' . $this->here
);
}
$this->cakeError('error400');
}
}
这段代码的作用是拒绝通过非 HTTPS 请求访问 payment 和 processPayment。
CakePHP 1.3 的不同补丁版本以及项目自身的组件配置,可能会影响 black-hole 回调的具体行为。因此,不要默认 requireSecure() 会按业务需要自动重定向。最好显式设置 blackHoleCallback,并分别测试 GET 和 POST 请求。
如果没有加载 RequestHandlerComponent,可以直接检查请求方法:
public function securityError($type)
{
if ($type === 'secure' && env('REQUEST_METHOD') === 'GET') {
return $this->redirect(
'https://secure.domain.com' . $this->here
);
}
$this->cakeError('error400');
}
不要把已经收到的 HTTP 支付 POST 请求直接重定向到 HTTPS。部分客户端或中间设备可能在重定向过程中改变请求方法,或者丢失 POST 数据。支付表单应该从一开始就提交到 HTTPS 地址。
集中配置两个基础地址
可以在 app/config/bootstrap.php 中统一定义普通站点和安全站点的地址:
Configure::write('Site.httpBaseUrl', 'http://domain.com');
Configure::write('Site.httpsBaseUrl', 'https://secure.domain.com');
提交房态查询表单时使用安全地址:
<?php
echo $form->create('Booking', array(
'url' => Configure::read('Site.httpsBaseUrl') . '/bookings/payment'
));
?>
这里使用的仍然是绝对 URL,但域名只需维护一处,不会分散在 Controller、View、Element 和 Layout 中。
也可以在 Controller 中生成支付页面地址,再传给 View:
$this->set(
'paymentUrl',
Configure::read('Site.httpsBaseUrl') . '/bookings/payment'
);
<?php
echo $form->create('Booking', array('url' => $paymentUrl));
?>
首选方案:付款后重定向回 HTTP
如果确认页不再显示敏感的支付信息,最简单的办法是在支付成功后将用户重定向到普通域名:
public function processPayment()
{
// 验证订单、调用支付服务并确认支付结果。
if ($paymentSucceeded) {
$this->Session->write(
'Message.flash',
array('message' => '付款成功,预订已经确认。')
);
return $this->redirect(
Configure::read('Site.httpBaseUrl') .
'/bookings/confirmation/' .
urlencode($bookingReference)
);
}
}
浏览器进入 HTTP 确认页后,Layout 中原有的相对链接:
<?php echo $html->link('首页', '/'); ?>
<?php echo $html->link('客房介绍', array(
'controller' => 'rooms',
'action' => 'index'
)); ?>
会自动指向 http://domain.com,不需要逐个修改 header/footer。
HTTP URL 中不应包含支付状态、银行卡资料、支付网关令牌,或者能够直接操作订单的秘密值。确认页应使用不可预测的引用值或服务器端 Session 查找订单,并再次确认当前用户有权查看该订单。
如果整个站点都支持 HTTPS,更适合长期使用的方案通常是全站 HTTPS,而不是在支付完成后降级到 HTTP。继续使用 HTTP 会让页面内容、登录状态以及未设置 Secure 的 Cookie 面临网络窃听和篡改风险。
确认页保持 HTTPS 时生成 HTTP 链接
如果确认页继续使用:
https://secure.domain.com/bookings/confirmation
相对链接必然会继承当前页面的协议和主机名,不会自动转到:
http://domain.com
此时必须生成绝对 HTTP URL。为了避免在每个模板中拼接域名,可以建立一个简单的 Helper:
<?php
App::import('Helper', 'Html');
class SiteHtmlHelper extends HtmlHelper
{
public function publicUrl($url = '/')
{
if (is_array($url)) {
$url = Router::url($url);
}
return rtrim(
Configure::read('Site.httpBaseUrl'),
'/'
) . '/' . ltrim($url, '/');
}
public function publicLink($title, $url = '/', $options = array())
{
return $this->link(
$title,
$this->publicUrl($url),
$options
);
}
}
在 Controller 中加载该 Helper:
public $helpers = array('Html', 'Form', 'SiteHtml');
然后在 Layout 或 Element 中统一使用:
<?php echo $siteHtml->publicLink('首页', '/'); ?>
<?php echo $siteHtml->publicLink('查看客房', array(
'controller' => 'rooms',
'action' => 'index'
)); ?>
协议和域名的切换逻辑会集中在一个 Helper 中。以后更换域名或改成全站 HTTPS,只需调整配置或 Helper。
也可以在 Layout 中切换链接模式
如果只有少数页面使用安全域名,可以由 Controller 向 Layout 传入一个标志:
public function confirmation()
{
$this->set('usePublicLinks', true);
}
Layout 再根据这个标志决定链接地址:
<?php
$homeUrl = !empty($usePublicLinks)
? Configure::read('Site.httpBaseUrl') . '/'
: '/';
echo $html->link('首页', $homeUrl);
?>
这种方式适合链接较少的项目。如果链接很多,自定义 Helper 会更容易维护。
路由层或 Web 服务器强制协议
除应用层检查外,也可以在 Web 服务器中设置协议规则:
/bookings/payment只能通过 HTTPS 访问。- 普通内容统一使用 HTTP,或者统一使用更推荐的 HTTPS。
- HTTP 请求到达支付路径时,通过
301、302或307跳转到安全域名。
对于表单 POST,不应依赖普通的 301 或 302 完成协议升级。表单应该直接提交到 HTTPS。Web 服务器规则主要用于处理用户手工输入的地址、旧书签,以及搜索引擎中记录的错误地址。
Session 和 Cookie 要特别检查
支付流程跨越 domain.com 和 secure.domain.com 时,需要确认 Session Cookie 的作用域符合预期。
如果两个子域需要共享同一个 Session,Cookie 域可能需要配置为:
.domain.com
是否共享 Session,需要根据系统的身份认证设计决定。还要注意以下事项:
- 支付和登录 Cookie 应设置
HttpOnly。 - 仅供安全区域使用的 Cookie 应设置
Secure,防止它们通过 HTTP 发送。 - 设置了
Secure的 Cookie 无法在 HTTP 页面中使用。 - 不要为了让 HTTP 页面读取 Session,降低保存支付凭据的 Cookie 的安全等级。
- 支付结果必须由服务器向支付服务端校验,不能只信任浏览器返回的参数。
如果站点配置了 HSTS,特别是启用了 includeSubDomains,浏览器可能会自动把普通域名的 HTTP 请求升级为 HTTPS。此时,从 HTTPS 返回 HTTP 的方案无法可靠实现,需要改用全站 HTTPS。
推荐的最终结构
实际项目可以采用以下边界:
http://domain.com
普通浏览、房态查询
https://secure.domain.com/bookings/payment
支付信息提交
https://secure.domain.com/bookings/process_payment
支付处理
http://domain.com/bookings/confirmation/{reference}
只显示非敏感的预订确认信息
SecurityComponent 用来确保支付 action 只能通过 HTTPS 访问。基础地址配置和自定义 Helper 用来生成跨协议链接。支付完成后的重定向则负责让页面回到普通站点。
因此,没有必要逐个硬编码所有链接,但也不能只依靠 SecurityComponent 切换协议。它负责约束请求的安全性,URL 的主机名和协议仍需由重定向逻辑、统一配置和视图 Helper 管理。
备注:内容仅供参考。
