如何使用 Google service 搭建 Payment API Gateway?
结论
可以实现。建议在 Google Cloud 上采用以下链路:
客户端
↓ HTTPS
API Gateway
↓ 身份验证后的请求
Cloud Run 支付服务
↓ VPC 出站流量
Cloud NAT
↓ 固定公网 IP
Payment API
解决 IP 许可名单问题的是 Cloud NAT 绑定的静态公网 IP,而不是 API Gateway。Payment API 只需允许这个固定 IP。
API Gateway 负责统一入口、身份验证和流量控制,Cloud Run 承载支付逻辑,Cloud NAT 提供稳定的出站 IP。不要将用户 IP 加入 Payment API 许可名单,也不要让客户端持有支付接口凭证或直接调用支付接口。
为什么不能只使用 API Gateway
Google Cloud API Gateway 主要处理入站请求,包括:
- 提供统一的 HTTPS 地址
- 验证 JWT 或 API Key
- 根据 OpenAPI 配置转发请求
- 控制后端服务的访问权限
- 记录调用日志
API Gateway 并不是提供固定出站 IP 的通用代理。即使请求经过 API Gateway,后端调用 Payment API 时,使用的仍是后端服务的出站网络。
因此,需要将 Cloud Run 的出站流量接入 VPC,再通过 Cloud NAT 使用预留的静态公网 IP。
实施步骤
1. 预留静态公网 IP
先在目标区域预留一个静态公网 IP:
gcloud compute addresses create payment-egress-ip \
--region=asia-east1
查看分配结果:
gcloud compute addresses describe payment-egress-ip \
--region=asia-east1 \
--format="get(address)"
将得到的 IP 提交给 Payment API 服务商,并加入访问许可名单。
Cloud NAT、Cloud Run 和相关网络资源应位于同一区域。示例中的 asia-east1 需要替换为实际部署区域。
2. 创建 Cloud Router 和 Cloud NAT
创建 Cloud Router:
gcloud compute routers create payment-router \
--network=default \
--region=asia-east1
创建 Cloud NAT,并指定刚刚预留的静态 IP:
gcloud compute routers nats create payment-nat \
--router=payment-router \
--region=asia-east1 \
--nat-external-ip-pool=payment-egress-ip \
--nat-all-subnet-ip-ranges \
--enable-logging
生产环境通常不适合直接使用 default 网络。更稳妥的方案是为支付服务创建独立的 VPC、子网和防火墙策略。
3. 将 Cloud Run 出站流量接入 VPC
Cloud Run 可以使用 Direct VPC egress,也可以通过 Serverless VPC Access connector 接入 VPC。新项目通常可以先评估 Direct VPC egress,具体能否使用取决于所选区域和 Google Cloud 当前的支持情况。
配置时,需要将所有出站流量都路由到 VPC,而不是只路由私网地址:
Egress setting: All traffic
如果只选择 private ranges,访问公网 Payment API 的请求可能绕过 Cloud NAT,因而无法使用预期的固定 IP。
Cloud Run 部署配置的主要关系如下:
Cloud Run
VPC network: payment-vpc
Subnet: payment-subnet
Egress: All traffic
控制台和 gcloud 中的具体字段可能随 Cloud Run 网络接入方式而变化,请以当前项目选择的 Direct VPC egress 或 VPC connector 配置为准。
4. 编写支付代理服务
下面是一个简化的 Node.js 示例。生产代码还需补充参数校验、身份验证、超时处理、幂等机制和日志脱敏。
import express from "express";
const app = express();
app.use(express.json());
app.post("/payments", async (req, res) => {
const { amount, currency, orderId } = req.body;
if (
!Number.isInteger(amount) ||
amount <= 0 ||
typeof currency !== "string" ||
typeof orderId !== "string"
) {
return res.status(400).json({
error: "Invalid payment request"
});
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10_000);
try {
const response = await fetch(
`${process.env.PAYMENT_API_BASE_URL}/payments`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.PAYMENT_API_TOKEN}`,
"Content-Type": "application/json",
"Idempotency-Key": orderId
},
body: JSON.stringify({
amount,
currency,
merchantReference: orderId
}),
signal: controller.signal
}
);
const responseBody = await response.text();
if (!response.ok) {
console.error("Payment API request failed", {
status: response.status,
orderId
});
return res.status(502).json({
error: "Payment provider rejected the request",
orderId
});
}
res
.status(response.status)
.type(response.headers.get("content-type") || "application/json")
.send(responseBody);
} catch (error) {
console.error("Payment API unavailable", {
name: error.name,
orderId
});
res.status(504).json({
error: "Payment provider timeout",
orderId
});
} finally {
clearTimeout(timeout);
}
});
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`Payment gateway listening on port ${port}`);
});
amount 最好使用最小货币单位的整数。例如,10.50 元应表示为 1050,以避免浮点数计算误差。
支付凭证不应直接写入代码或镜像。可以将其存放在 Secret Manager 中,并只向 Cloud Run 使用的服务账号授予指定密钥的读取权限。
5. 在 Cloud Run 前配置 API Gateway
API Gateway 可以通过 OpenAPI 文档定义公开接口。以下示例只展示基本结构:
swagger: "2.0"
info:
title: payment-gateway
version: "1.0.0"
schemes:
- https
paths:
/payments:
post:
operationId: createPayment
x-google-backend:
address: https://CLOUD_RUN_SERVICE_URL
path_translation: APPEND_PATH_TO_ADDRESS
responses:
"200":
description: Payment initialized
"400":
description: Invalid request
"401":
description: Unauthorized
"502":
description: Payment provider error
配置 API Gateway 后,也不要将 Cloud Run 开放给任何人直接调用,否则请求可以绕过网关。应限制 Cloud Run 的调用权限,只授权 API Gateway 使用的服务账号和确有需要的运维身份。
API Key 更适合项目识别和配额控制,不能单独作为支付用户的身份认证机制。面向最终用户时,应使用经过验证的登录令牌,例如 JWT。后端还要再次核对用户、订单和支付金额之间的关系。
验证固定 IP 是否生效
部署完成后,可以让 Cloud Run 临时请求一个会返回调用方公网 IP 的测试端点,确认返回值与预留的 payment-egress-ip 一致。
验证完成后,应删除这个诊断接口,以免暴露不必要的网络信息。同时,还要在 Cloud NAT 日志和 Payment API 服务端记录中核对实际源 IP。
更简单的替代方案
如果不需要自动扩缩容,也可以在 Compute Engine 上部署反向代理或支付服务,并为虚拟机绑定静态公网 IP:
客户端 → HTTPS Load Balancer 或虚拟机 → Payment API
这种方案结构较简单,但系统更新、实例故障、扩容和高可用都需要自行处理。如果部署多个实例,还要确保所有实例都经过同一组固定出口 IP,并将这些 IP 全部加入许可名单。
GKE 也可以通过 Cloud NAT 提供固定出口 IP,但如果系统只有一个支付代理,引入 Kubernetes 通常会增加额外的运维复杂度。
安全与支付场景注意事项
IP 许可名单不是完整的身份验证
固定 IP 只能证明请求来自指定的网络出口,不能替代 API 凭证、请求签名或 mTLS。如果 Payment API 支持,还应启用:
- OAuth 2.0 或短期访问令牌
- HMAC 请求签名
- mTLS
- 时间戳与 nonce 防重放
- 最小权限的商户凭证
必须实现幂等
网络超时并不等于支付失败。如果客户端或网关直接重试,可能造成重复扣款。
每个支付请求都应携带稳定且唯一的 Idempotency-Key。网关还要保存订单号、支付状态和支付提供方返回的交易编号,并在重试前查询已有结果。
不要信任客户端提交的金额
客户端提交的金额只能作为请求线索。后端应根据订单数据重新计算并核对以下内容:
- 商品价格
- 数量
- 币种
- 优惠
- 运费
- 已支付状态
否则,攻击者可能篡改请求中的金额。
不要记录敏感数据
日志中不要输出:
- 银行卡号
- CVV
- 完整访问令牌
- 请求签名密钥
- 完整身份证件信息
- 未脱敏的支付响应
如果系统直接处理银行卡数据,还需要评估 PCI DSS 的适用范围。应尽量使用支付提供方的托管收银台或 tokenization,避免让银行卡信息经过自己的网关。
谨慎设置自动重试
查询类请求可以进行有限次数的重试。对于创建支付或扣款请求,只有在 Payment API 明确支持幂等键时才能安全重试,同时应设置指数退避和最大重试次数。
推荐方案汇总
以 Google Cloud 为主的生产应用可以采用以下组合:
API Gateway
+ Cloud Run
+ Direct VPC egress 或 Serverless VPC Access
+ Cloud NAT
+ Reserved Static External IP
+ Secret Manager
Cloud NAT 和静态公网 IP 用于满足 Payment API 的 IP 许可名单要求,API Gateway 负责入口治理,Cloud Run 负责支付业务逻辑。无论最终用户的 IP 如何变化,Payment API 看到的源地址都是配置好的固定出口 IP。
备注:内容仅供参考。