支付宝支付完成回调ip白名单如何配置?
结论
支付宝支付异步通知的来源 IP 并不是固定不变的,因此不建议把“来源 IP 白名单”作为验证回调真实性的主要手段。支付宝可能因机房切换、容灾调度或网络架构调整而更换通知出口 IP。如果只硬编码少量 IP,正常回调可能会被拦截。
商户系统应把支付宝的 RSA2 签名验证作为主要安全措施,同时核对订单号、商户身份、金额、币种和交易状态。如果网络安全策略要求配置 IP 白名单,应从支付宝开放平台官方文档或技术支持处获取当前适用于具体商户、产品和环境的 IP 段,并建立更新机制,不要直接使用网上流传的 IP 列表。
为什么不能只校验来源 IP
来源 IP 只能表明请求经过了某个网络出口,无法证明通知内容真实且未被修改。具体原因包括:
- 支付宝可能调整通知出口,IP 列表会过期。
- 代理服务器、负载均衡和云防护服务可能改变应用层看到的客户端 IP。
X-Forwarded-For等请求头可以伪造。只有可信代理负责覆盖和维护这些请求头时,应用才能采信。- 即使请求来自允许的 IP,也仍需防范参数篡改、重复通知和订单串改。
支付宝异步通知中包含 sign、sign_type 等参数。商户应使用支付宝公钥验签,以确认通知由支付宝签发,并检查通知内容是否被修改。
推荐配置方式
1. 确保通知地址可以被支付宝访问
创建支付订单时,需要正确配置 notify_url。该地址应满足以下条件:
- 使用公网可访问的 HTTPS 地址。
- 不依赖用户登录状态、Cookie 或验证码。
- 不进行页面跳转。
- 能接收支付宝发送的表单参数,通常为
application/x-www-form-urlencoded。 - 防火墙、WAF、API 网关和负载均衡均允许访问该路径。
同步返回地址 return_url 只负责用户页面跳转,不能代替 notify_url,也不能作为判断支付结果的依据。
2. 在接入层处理 IP 限制
如果公司安全规范要求配置 IP 白名单,可以按以下方式实施:
- 在支付宝开放平台对应产品的官方文档中查询通知出口 IP 或网络接入说明。
- 如果无法确认,通过支付宝开放平台工单或技术支持获取当前有效范围。
- 区分生产环境和沙箱环境,不要假设两者使用相同的 IP。
- 使用 CIDR 网段配置,不要随意把域名解析结果当作通知出口 IP。
- 设置变更告警和更新流程,防止支付宝调整出口后所有回调都失败。
- 如果入口前有 CDN、WAF 或反向代理,应在最外层入口放行支付宝来源,并确保应用只信任该代理写入的真实客户端 IP。
如果无法获得权威且便于维护的 IP 范围,可以只开放专用通知路径,再通过签名验证、限流和监控保障安全,不必维护来源未经确认的静态 IP 列表。
3. 验证支付宝签名
以下示例使用支付宝 Java SDK。参数名称和方法签名可能随 SDK 版本变化,请以项目实际使用的官方 SDK 为准。
import com.alipay.api.internal.util.AlipaySignature;
import jakarta.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
public boolean verifyAlipayNotify(
HttpServletRequest request,
String alipayPublicKey) throws Exception {
Map<String, String> params = new HashMap<>();
for (Map.Entry<String, String[]> entry
: request.getParameterMap().entrySet()) {
String[] values = entry.getValue();
params.put(
entry.getKey(),
values == null ? "" : String.join(",", values)
);
}
return AlipaySignature.rsaCheckV1(
params,
alipayPublicKey,
"UTF-8",
"RSA2"
);
}
这里应使用与当前应用配置相对应的支付宝公钥,而不是商户应用公钥或商户应用私钥。如果应用采用支付宝公钥证书模式,应使用对应 SDK 提供的证书验签方式。
不要先自行排序、拼接参数或进行二次 URL 解码,再执行验签。应优先把收到的完整参数集合交给官方 SDK 处理。
4. 校验业务数据
签名验证通过后,还要查询本地订单并核对相关字段:
public String handleNotify(HttpServletRequest request) {
try {
Map<String, String> params = extractParams(request);
boolean valid = AlipaySignature.rsaCheckV1(
params,
alipayPublicKey,
"UTF-8",
"RSA2"
);
if (!valid) {
return "failure";
}
String outTradeNo = params.get("out_trade_no");
String tradeNo = params.get("trade_no");
String tradeStatus = params.get("trade_status");
String appId = params.get("app_id");
String sellerId = params.get("seller_id");
String totalAmount = params.get("total_amount");
Order order = orderService.findByOutTradeNo(outTradeNo);
if (order == null) {
return "failure";
}
if (!configuredAppId.equals(appId)
|| !configuredSellerId.equals(sellerId)
|| !order.getAmount().toPlainString().equals(totalAmount)) {
return "failure";
}
if ("TRADE_SUCCESS".equals(tradeStatus)
|| "TRADE_FINISHED".equals(tradeStatus)) {
orderService.markPaidIdempotently(outTradeNo, tradeNo);
}
return "success";
} catch (Exception e) {
return "failure";
}
}
实际项目还应根据使用的支付产品核对 buyer_id、币种等字段。比较金额时建议使用 BigDecimal,避免 double 带来的精度问题。
5. 正确返回响应
通知处理成功后,应直接返回支付宝协议要求的纯文本:
success
不要返回 JSON、HTML 或其他提示文字。如果验签失败、订单不存在或关键字段不一致,不应更新订单状态,也不能返回 success。
必须注意的安全细节
- 保证幂等性:支付宝可能重复发送通知。同一个
out_trade_no或trade_no多次到达时,系统不能重复发货、增加余额或记账。 - 使用数据库事务:订单状态更新和后续业务操作应保持一致。必要时可使用唯一约束、状态机或消息队列处理。
- 不要信任请求头中的 IP:只有确定请求经过自有可信代理时,才能按照代理规则读取真实客户端 IP。
- 记录必要日志:记录通知时间、订单号、支付宝交易号、验签结果和处理结果,但不能把私钥或完整的敏感信息写入日志。
- 控制处理时间:回调接口应尽快完成校验并返回。耗时业务可以在可靠落库后异步执行。
- 主动查询兜底:如果长时间没有收到通知,可以调用支付宝交易查询接口核对交易状态。查询结果仍需与本地订单信息匹配。
- 区分密钥模式:普通公钥模式与公钥证书模式的配置和验签方法不同,不能混用。
合理的安全模型是:网络层限制访问范围,应用层严格验签,业务层核对订单并保证幂等。IP 白名单可以作为附加防护,但不能代替支付宝签名验证。
备注:内容仅供参考。
