3. 支付网关设计:网关职责、统一接入层、协议适配与签名验签

支付网关是支付中间件体系中最核心的流量入口与安全边界。它承担着将外部商户的支付请求转化为内部可路由、可处理、可审计的标准化消息的职责。本章从网关职责、统一接入层设计、协议适配机制以及签名验签体系四个维度,系统讲解企业级支付网关的落地实践。

3.1 支付网关核心职责

支付网关并非简单的请求转发器,它在支付链路中承担以下六大核心职责:

职责 说明 关键要求
安全防护 身份认证、签名验签、防重放、防篡改 支持多算法、密钥轮换、时间戳校验
协议适配 将外部多种协议转换为内部统一协议 HTTP/HTTPS、JSON/XML、自定义报文
路由分发 根据商户号、产品类型、渠道属性路由 动态路由、灰度发布、熔断降级
流量控制 限制商户/接口的并发与速率 令牌桶、漏桶、分布式限流
日志审计 全链路请求/响应记录 不可篡改、可追溯、支持离线分析
异常处理 超时、重试、幂等、降级 业务幂等、异步补偿、人工干预
设计原则:支付网关应保持无状态设计,所有会话状态下沉到分布式缓存或数据库;网关本身只做协议转换、安全校验与路由分发,不参与业务逻辑计算。

3.2 统一接入层架构

统一接入层是支付网关对外的唯一入口,所有商户请求必须经过该层才能进入内部网络。其核心设计目标包括:统一入口、协议归一、安全前置、可观测。

以下为统一接入层的典型架构图:

支付网关统一接入层架构 商户APP 商户Web 商户服务端 统一接入层 负载均衡 协议适配 签名验签 限流熔断 核心处理层 路由分发 参数校验 幂等处理 异步通知 ... 下游渠道:银行 / 第三方支付 / 清算机构
实践建议:统一接入层应独立部署为一组无状态网关节点,通过DNS轮询或SLB对外暴露唯一域名。所有商户只需对接这一个入口,内部路由对商户完全透明。

3.3 协议适配机制

支付网关需要支持多种外部协议,并将其统一转换为内部RPC或消息格式。协议适配层通常采用策略模式实现,每种协议对应一个适配器。

3.3.1 支持的协议类型

协议类型 传输层 数据格式 典型场景
HTTP TCP JSON / XML 商户服务端直连、开放平台API
HTTPS TLS 1.2/1.3 JSON / XML 敏感数据传输、合规要求
JSON HTTP/HTTPS JSON 移动端、轻量级对接
XML HTTP/HTTPS XML 银行、银联等传统渠道
自定义二进制 TCP TLV / 定长报文 高并发、低延迟内部通信

3.3.2 协议适配器设计

每个协议适配器负责完成以下三个步骤:

  1. 报文解析:将原始字节流解析为键值对或结构化对象。
  2. 格式转换:将外部字段映射为内部统一字段模型。
  3. 编码输出:将内部响应编码为外部协议格式返回。
// 协议适配器接口定义示例
public interface ProtocolAdapter {
    // 解析外部请求为内部统一请求对象
    UnifiedRequest parse(HttpServletRequest request);
    
    // 将内部统一响应对象编码为外部响应
    void encode(HttpServletResponse response, UnifiedResponse unifiedResponse);
    
    // 获取支持的协议类型
    String supportedProtocol();
}
字段映射避坑:不同渠道对金额、时间、状态的定义可能完全不同。例如金额单位有“分”和“元”之分,时间格式有“yyyyMMddHHmmss”和“ISO8601”之分。适配器必须显式处理这些差异,并在内部统一使用标准单位(金额:分,时间:UTC毫秒时间戳)。

3.4 签名验签机制

签名验签是支付网关安全的第一道防线。它确保请求在传输过程中未被篡改,并验证请求方的真实身份。

3.4.1 签名算法选型

算法 类型 密钥管理 性能 推荐场景
HMAC-SHA256 对称 共享密钥 高 内部系统、信任环境
RSA-SHA256 非对称 商户私钥签名,网关公钥验签 中 外部商户、开放平台
SM2-SM3 国密非对称 国密证书 中 金融合规、国产化要求

3.4.2 签名生成流程

以RSA-SHA256为例,商户端签名生成步骤:

  1. 将所有请求参数按字典序排序(key=value&key=value)。
  2. 拼接待签名字符串,末尾拼接商户密钥(或使用私钥签名)。
  3. 使用SHA256计算摘要,再用RSA私钥加密生成签名。
  4. 将签名放入请求头或请求体中的sign字段。
// 签名生成示例(Java)
public static String generateSign(Map<String, String> params, String privateKey) {
    // 1. 参数排序
    List<String> keys = new ArrayList<>(params.keySet());
    Collections.sort(keys);
    
    // 2. 拼接待签名字符串
    StringBuilder sb = new StringBuilder();
    for (String key : keys) {
        sb.append(key).append("=").append(params.get(key)).append("&");
    }
    String signStr = sb.substring(0, sb.length() - 1);
    
    // 3. 使用私钥签名
    PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey));
    PrivateKey key = KeyFactory.getInstance("RSA").generatePrivate(keySpec);
    Signature signature = Signature.getInstance("SHA256withRSA");
    signature.initSign(key);
    signature.update(signStr.getBytes(StandardCharsets.UTF_8));
    
    return Base64.getEncoder().encodeToString(signature.sign());
}

3.4.3 验签流程

网关收到请求后,执行以下验签步骤:

  1. 从请求中提取签名值(sign字段)。
  2. 使用与商户约定的排序规则,重新拼接待验签字符串。
  3. 根据商户号查询对应的公钥(或共享密钥)。
  4. 使用公钥验证签名是否匹配。
  5. 校验时间戳是否在允许的偏差范围内(通常±5分钟),防止重放攻击。
安全红线:
  • 禁止将私钥硬编码在代码中,必须使用密钥管理服务(KMS)或硬件安全模块(HSM)。
  • 验签失败必须记录完整请求日志,并触发告警,不能静默忽略。
  • 密钥轮换期间,需同时支持新旧两套密钥,避免轮换导致服务中断。

3.4.4 防重放攻击设计

仅靠签名无法防止重放攻击,必须引入nonce(一次性随机数)和时间戳机制:

字段 说明 校验规则
timestamp 请求发起时的Unix时间戳(秒) 当前时间 ± 300秒内有效
nonce 每次请求唯一的随机字符串 Redis中存储已使用的nonce,有效期300秒
sign 对timestamp+nonce+业务参数的签名 验签通过且nonce未使用

以下为签名验签与防重放的整体流程示意图:

签名验签与防重放流程 商户端 支付网关 Redis (nonce) 请求+签名 ① 验签 ② 时间戳校验 ③ nonce校验 查询/写入nonce 验签通过 验签失败 → 拒绝
性能优化:nonce校验使用Redis的SET NX命令,并设置TTL为300秒。对于极高并发场景,可使用本地布隆过滤器做第一层过滤,减少Redis压力。

3.5 网关高可用设计要点

支付网关作为关键链路节点,必须满足99.99%以上的可用性。以下是几个关键设计要点:

  • 无状态水平扩展:网关节点不存储任何会话数据,通过SLB均匀分发流量,支持秒级扩缩容。
  • 优雅降级:当依赖的下游服务(如Redis、数据库)不可用时,网关应自动降级为本地缓存或直接拒绝非关键请求,避免雪崩。
  • 全链路超时控制:每个环节设置独立的超时时间(如DNS解析100ms、连接建立200ms、读超时500ms),防止慢请求拖垮整个网关。
  • 灰度发布:新版本网关先接入1%的流量,观察无异常后再逐步放量至100%。
总结:支付网关的设计核心在于“统一”与“安全”。统一接入层屏蔽了外部协议的复杂性,签名验签机制保障了请求的合法性与完整性。在实际落地中,建议优先实现HTTPS+JSON+RSA-SHA256的组合,后续根据业务需求逐步扩展协议适配器与国密算法支持。