PAYATHON 2026

CakePHP 1.3 如何实现 HTTPS 安全支付与 HTTP 跳转

支付小周

结论

CakePHP 1.3 的 SecurityComponent 可以限制支付相关 action 只能通过 HTTPS 访问,但不会自动把页面中的普通链接改成 HTTP,也不能代替完整的协议切换逻辑。

比较合适的处理方式如下:

  1. 使用 SecurityComponent::requireSecure() 保护支付和确认 action。
  2. 集中配置 HTTP 和 HTTPS 的基础地址,避免在模板中到处写域名。
  3. 支付完成后,如果确认页不需要使用 HTTPS,直接重定向到 HTTP 确认页。进入该页面后,相对链接会自动沿用 HTTP。
  4. 如果确认页必须使用 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 服务器规则主要用于处理用户手工输入的地址、旧书签,以及搜索引擎中记录的错误地址。

支付流程跨越 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 管理。

备注:内容仅供参考。