PAYATHON 2026

不向开发者披露商户信息时如何维护 E-Commerce 应用?

支付小周

明确结论

可以不向开发者提供生产环境的商户账号、API 密钥和客户数据,但需要重新划分生产权限:

  • 商户凭据由客户或其运维人员保管,并通过密钥管理系统注入运行环境。
  • 开发者只使用支付平台提供的 sandbox/test account。
  • 部署通过受控的 CI/CD 流程完成,开发者不能直接登录生产服务器。
  • 支付信息尽量交由支付服务商托管、收集和令牌化,应用不保存银行卡数据。
  • 客户负责审批上线、轮换密钥、查看交易后台和处理生产数据。

有一点必须说清楚:如果开发者可以任意修改生产站点文件,而且修改后的代码能够直接上线,那么仅仅”隐藏密钥”并不能阻止开发者获取密钥。应用在运行时必须使用商户凭据,有权修改运行代码的人可以添加日志或网络请求,导出凭据和客户数据。

所以,有效的办法不是把密钥藏进开发者不知道的配置文件,而是限制代码部署权限,审核所有生产变更,并保留审计记录。

推荐的职责与权限划分

可以按下面的方式划分权限:

角色可以访问不应访问
开发者源代码、测试环境、测试商户账号、脱敏日志正式商户密钥、支付后台、生产数据库
客户运维人员生产服务器、密钥管理系统、部署审批非必要的源代码修改权限
客户财务或业务人员支付后台、退款和对账功能服务器管理权限、应用密钥导出权限
CI/CD 服务账号部署指定构建产物支付后台、数据库交互式查询权限
生产应用运行所需的最小权限密钥管理权限、支付后台管理权限

小型团队未必能安排这么多独立岗位,但至少要满足以下要求:

  1. 正式商户账号归客户所有。
  2. 开发者不持有正式密钥。
  3. 生产部署需要客户批准。
  4. 部署内容和操作记录可以追溯。
  5. 开发者不能绕过发布流程直接修改线上代码。

具体实施步骤

1. 由客户创建并持有正式商户账号

客户应使用自己的公司资料、邮箱和双因素认证设备注册支付账户。开发者不应代替客户持有主账号,也不应与客户共用管理员密码。

如果支付平台支持团队成员、角色或细粒度权限,应为不同人员分别创建账号。例如,开发者最多获得测试环境权限,正式交易、退款和对账则由财务人员负责。

正式密钥由客户生成并保存。开发者只需知道应用需要哪些配置项,不需要知道具体的配置值。

2. 开发阶段只连接测试环境

测试环境和生产环境可以使用相同的配置接口,但必须采用不同的凭据:

# Development or staging only
PAYMENT_ENV=sandbox
PAYMENT_API_KEY=test_key_from_sandbox
PAYMENT_WEBHOOK_SECRET=test_webhook_secret

任何真实密钥都不能提交到 Git,也不能硬编码进应用:

// 错误示例:密钥会进入源代码和版本历史
const apiKey = "live_secret_key";

应用应从运行环境读取配置。如果缺少必要配置,应立即终止启动:

const requiredVariables = [
  "PAYMENT_API_KEY",
  "PAYMENT_WEBHOOK_SECRET"
];

for (const name of requiredVariables) {
  if (!process.env[name]) {
    throw new Error(`Missing required environment variable: ${name}`);
  }
}

const paymentConfig = {
  apiKey: process.env.PAYMENT_API_KEY,
  webhookSecret: process.env.PAYMENT_WEBHOOK_SECRET,
  environment: process.env.PAYMENT_ENV ?? "sandbox"
};

环境变量只是配置入口,本身并不构成完整的安全边界。能够控制应用代码、进程调试器或服务器管理员权限的人,仍可能读取进程使用的密钥。

3. 由客户将密钥注入生产环境

正式凭据可以保存在云平台的 secret manager、容器编排平台的 secrets 功能,或者受权限保护的部署系统中。具体使用哪种产品和配置方式,取决于客户的基础设施。

以容器部署为例,应用镜像只声明需要哪些变量,不包含实际值:

services:
  web:
    image: registry.example.com/shop/app:2026.09.01
    environment:
      PAYMENT_API_KEY: ${PAYMENT_API_KEY}
      PAYMENT_WEBHOOK_SECRET: ${PAYMENT_WEBHOOK_SECRET}
      PAYMENT_ENV: production

客户在生产系统中配置真实值。开发者交付的镜像、仓库和配置模板中都不应出现这些值。

不过,这种做法无法防住拥有生产部署权的开发者,因为恶意代码仍然可以读取 process.env.PAYMENT_API_KEY。因此,密钥注入必须与代码审核、部署审批和生产权限隔离配合使用。

4. 使用受控的 CI/CD 发布流程

推荐采用以下流程:

开发者提交代码
    ↓
自动测试与安全检查
    ↓
生成不可变构建产物
    ↓
客户审核变更及构建结果
    ↓
客户批准生产部署
    ↓
CI/CD 使用受限账号部署
    ↓
生产环境注入正式密钥

至少应设置这些控制措施:

  • 保护生产分支,禁止直接提交。
  • 所有生产变更都必须经过 pull request 或同等形式的审核。
  • 构建产物使用唯一版本号或 digest,防止审批后被替换。
  • 生产部署需要客户方人员批准。
  • CI/CD 服务账号只能部署,不能读取或导出密钥。
  • 禁止通过 SSH、FTP 或控制面板直接修改线上文件。
  • 保存代码审核、部署、登录和密钥访问日志。
  • 紧急修复也要经过单独审批,并在事后接受审计。

如果客户没有技术人员,可以委托独立运维方负责生产托管和上线审批。运维方不应与开发者共用同一个管理员账号。

5. 尽量采用托管支付页面或令牌化方案

如果支付服务商提供 hosted checkout、redirect checkout 或托管支付控件,可以优先采用。用户的银行卡信息直接提交给支付服务商,应用只接收:

  • 支付会话标识;
  • 交易状态;
  • token;
  • 支付平台发送的 webhook。

概念性流程如下:

浏览器 → 商户应用:创建订单
商户应用 → 支付服务商:创建支付会话
浏览器 → 支付服务商:填写并提交支付信息
支付服务商 → 商户应用:发送 webhook
商户应用:验证签名并更新订单状态

这种方式可以大幅缩小应用接触支付卡数据的范围,但具体实现仍须遵守所选支付平台的正式集成文档和适用的 PCI DSS 要求。使用托管页面并不意味着所有安全和合规责任都会自动消失。

6. 正确处理 webhook

不能只依靠浏览器跳转参数判断支付结果。用户可能篡改请求,浏览器也可能在付款后关闭。服务器应验证 webhook 的签名,并通过验证后的 webhook 更新支付状态。

下面是一个不依赖具体支付平台的结构示例:

import crypto from "node:crypto";
import express from "express";

const app = express();

app.post(
  "/webhooks/payment",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const signature = req.header("X-Payment-Signature");
    const secret = process.env.PAYMENT_WEBHOOK_SECRET;

    if (!signature || !secret) {
      return res.sendStatus(400);
    }

    const expected = crypto
      .createHmac("sha256", secret)
      .update(req.body)
      .digest("hex");

    const receivedBuffer = Buffer.from(signature, "hex");
    const expectedBuffer = Buffer.from(expected, "hex");

    if (
      receivedBuffer.length !== expectedBuffer.length ||
      !crypto.timingSafeEqual(receivedBuffer, expectedBuffer)
    ) {
      return res.sendStatus(401);
    }

    const event = JSON.parse(req.body.toString("utf8"));

    // 根据 event.id 做幂等处理,再更新订单状态。
    // 不要记录银行卡号、密钥或完整敏感载荷。
    processPaymentEvent(event);

    return res.sendStatus(200);
  }
);

这只是通用示例。实际使用时,签名格式、时间戳校验、原始请求体要求和重放防护都必须按照具体支付服务商的官方文档实现,不能直接假定所有平台都采用 HMAC-SHA256。

进一步隔离支付能力

如果主站需要频繁维护,可以把支付功能拆分成一个权限控制更严格的独立服务:

E-Commerce Web Application
          ↓ 受限内部 API
Payment Service
          ↓
Payment Provider

主站只向支付服务发送订单编号、金额和币种。商户凭据由支付服务单独保存,同时限制调用来源和允许执行的操作。

支付服务对外提供的能力应尽量精简,例如只允许:

  • 为已有订单创建支付会话;
  • 查询指定订单的支付状态;
  • 接收并验证 webhook。

不要提供可以”发送任意支付平台请求”的通用代理接口,否则这种隔离就失去了意义。

还需要注意,支付服务能保护密钥,却不能自动解决主站代码遭到篡改的问题。攻击者仍可能修改商品价格、收款流程或页面跳转目标。代码审查和部署审批依然不可缺少。

限制客户数据访问

商户密钥和客户数据有关联,但不是同一个问题。即使开发者无法看到支付密钥,只要能够访问生产数据库、备份、日志或服务器管理员权限,仍可能接触客户信息。

需要分别采取以下措施:

  • 开发和测试使用合成数据,不复制真实客户数据。
  • 必须排障时,只提供脱敏日志或最小的数据样本。
  • 不向开发者开放生产数据库的交互式访问。
  • 按用途拆分数据库账号,并为每个账号授予最小权限。
  • 在日志中屏蔽 authorization header、cookie、token、密钥和支付信息。
  • 加密备份,并限制备份的下载和恢复权限。
  • 临时生产访问需要经过审批,并设置有效期。
  • 使用独立账号,禁止多人共用管理员凭据。
  • 对敏感数据的读取、导出和修改保留审计记录。

排障系统可以只向开发者展示错误类型、请求 ID、堆栈和脱敏后的业务上下文,不必提供完整的请求内容。

密钥管理注意事项

正式密钥应满足以下要求:

  • 测试密钥与生产密钥完全分离。
  • 不通过即时消息、普通邮件或工单正文传递。
  • 不写入源代码、镜像、前端 JavaScript、日志或错误页面。
  • 不同服务使用不同密钥,避免共用主密钥。
  • 如果平台支持权限限制,只授予应用必需的 API 权限。
  • 建立密钥轮换和吊销流程。
  • 人员离职、供应商变更或怀疑发生泄露时,立即轮换密钥。
  • 优先使用短期凭据、工作负载身份或不可导出的密钥机制,具体能否采用取决于支付平台和部署环境。

前端可以公开使用的 publishable key 必须与服务端 secret key 区分。某个支付平台允许 publishable key 出现在浏览器中,不代表其他凭据也能公开。

如何支持长期维护

日常维护不应默认要求开发者长期持有生产管理员权限。可以采用以下流程:

  1. 客户通过工单提供问题现象和请求 ID。
  2. 开发者先在测试环境复现问题。
  3. 客户提供经过脱敏的必要诊断信息。
  4. 开发者提交修复代码和测试结果。
  5. 客户或独立运维方审核并部署。
  6. 上线后通过监控验证,不直接查看客户业务数据。
  7. 只有极少数必须进入生产环境的情况,才授予限时、只读且全程审计的临时权限。

如果业务要求开发者承担 24 小时生产运维,就无法实现绝对的”零访问”。此时应该把目标改为可控访问,包括最小权限、即时授权、双人审批、会话记录、操作审计和事后自动撤权。

合同与治理同样重要

技术控制无法防范所有内部人员风险。维护合同或数据处理协议还应明确:

  • 商户账号和数据归谁所有;
  • 开发者可以接触哪些系统和数据;
  • 哪些操作必须经过客户批准;
  • 安全事件的通知时限和处理流程;
  • 密钥轮换、备份、日志和审计分别由谁负责;
  • 合同终止后如何撤销账号和删除数据;
  • 如何授予和收回紧急维护权限。

最终需要建立清晰的责任边界:开发者负责生产级代码和测试,客户控制生产身份、正式密钥和发布授权。如果开发者仍能跳过审核直接修改生产文件,就没有可靠的技术手段可以保证其无法接触商户凭据或客户数据。只有先收紧这项权限,其他密钥管理措施才有实际意义。

备注:内容仅供参考。