3. 支付网关设计:网关职责、统一接入层、协议适配与签名验签
支付网关是支付中间件体系中最核心的流量入口与安全边界。它承担着将外部商户的支付请求转化为内部可路由、可处理、可审计的标准化消息的职责。本章从网关职责、统一接入层设计、协议适配机制以及签名验签体系四个维度,系统讲解企业级支付网关的落地实践。
3.1 支付网关核心职责
支付网关并非简单的请求转发器,它在支付链路中承担以下六大核心职责:
| 职责 | 说明 | 关键要求 |
|---|---|---|
| 安全防护 | 身份认证、签名验签、防重放、防篡改 | 支持多算法、密钥轮换、时间戳校验 |
| 协议适配 | 将外部多种协议转换为内部统一协议 | HTTP/HTTPS、JSON/XML、自定义报文 |
| 路由分发 | 根据商户号、产品类型、渠道属性路由 | 动态路由、灰度发布、熔断降级 |
| 流量控制 | 限制商户/接口的并发与速率 | 令牌桶、漏桶、分布式限流 |
| 日志审计 | 全链路请求/响应记录 | 不可篡改、可追溯、支持离线分析 |
| 异常处理 | 超时、重试、幂等、降级 | 业务幂等、异步补偿、人工干预 |
设计原则:支付网关应保持无状态设计,所有会话状态下沉到分布式缓存或数据库;网关本身只做协议转换、安全校验与路由分发,不参与业务逻辑计算。
3.2 统一接入层架构
统一接入层是支付网关对外的唯一入口,所有商户请求必须经过该层才能进入内部网络。其核心设计目标包括:统一入口、协议归一、安全前置、可观测。
以下为统一接入层的典型架构图:
实践建议:统一接入层应独立部署为一组无状态网关节点,通过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 协议适配器设计
每个协议适配器负责完成以下三个步骤:
- 报文解析:将原始字节流解析为键值对或结构化对象。
- 格式转换:将外部字段映射为内部统一字段模型。
- 编码输出:将内部响应编码为外部协议格式返回。
// 协议适配器接口定义示例
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为例,商户端签名生成步骤:
- 将所有请求参数按字典序排序(key=value&key=value)。
- 拼接待签名字符串,末尾拼接商户密钥(或使用私钥签名)。
- 使用SHA256计算摘要,再用RSA私钥加密生成签名。
- 将签名放入请求头或请求体中的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 验签流程
网关收到请求后,执行以下验签步骤:
- 从请求中提取签名值(sign字段)。
- 使用与商户约定的排序规则,重新拼接待验签字符串。
- 根据商户号查询对应的公钥(或共享密钥)。
- 使用公钥验证签名是否匹配。
- 校验时间戳是否在允许的偏差范围内(通常±5分钟),防止重放攻击。
安全红线:
- 禁止将私钥硬编码在代码中,必须使用密钥管理服务(KMS)或硬件安全模块(HSM)。
- 验签失败必须记录完整请求日志,并触发告警,不能静默忽略。
- 密钥轮换期间,需同时支持新旧两套密钥,避免轮换导致服务中断。
3.4.4 防重放攻击设计
仅靠签名无法防止重放攻击,必须引入nonce(一次性随机数)和时间戳机制:
| 字段 | 说明 | 校验规则 |
|---|---|---|
| timestamp | 请求发起时的Unix时间戳(秒) | 当前时间 ± 300秒内有效 |
| nonce | 每次请求唯一的随机字符串 | Redis中存储已使用的nonce,有效期300秒 |
| sign | 对timestamp+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的组合,后续根据业务需求逐步扩展协议适配器与国密算法支持。