资讯详情

资讯详情

Java支付宝支付对接实战:签名、请求与验签全流程解析

先放下结论支付宝支付接口的对接本质上就三件事——签名、请求、验签。你要是能把这三件事的逻辑吃透剩下的事情无非就是照着文档填参数。但如果你只是去支付宝开放平台把SDK拉下来照着demo一顿复制粘贴那你大概率会在“签名失败”“验签失败”“回调收不到”这几个坑里来回折腾一整周。我自己是从PC网站支付一路接到手机网站支付、App支付、当面付的这几年下来大大小小的商户项目也做了十几个踩过的坑比很多人见过的接口都多。今天就以Java后端为例把支付宝支付对接的全过程掰开揉碎了讲一遍既然是“奶爸级别”我就默认看这篇文章的人完全没接触过支付接口所有概念我都会解释到“能给你媳妇讲明白”的程度。先说清楚这篇文能帮你解决什么问题第一你会有一套可以跑通的完整支付流程第二你会理解每一步为什么要这么做而不是盲目照抄第三你会知道上线前哪些坑必须提前避开。适合谁看适合第一次接支付接口的Java开发也适合被支付回调折磨过但没系统梳理过的朋友。1. 对接前的准备账号、应用、密钥到底怎么回事1.1 开放平台账号和应用创建不管你要接的是当面付扫码枪、电脑网站支付PC端收银台、手机网站支付H5还是App支付第一步都是先去支付宝开放平台open.alipay.com用企业或个人身份注册账号然后在“控制台-网页移动应用”里创建一个应用。这里有一个容易犯迷糊的地方你创建的应用本身是不带支付能力的需要你在这个应用里点击“添加能力”然后选择你要用的支付产品比如“电脑网站支付”或者“手机网站支付”。添加完能力之后应用会进入审核状态审核通过之前你只能用沙箱环境来测试。审核时间一般几个小时到一天不等我遇到过审核卡了两天的所以建议你注册完账号就先把应用创建好、把能力加上再慢慢看文档别等到代码写完了才去申请。创建应用的过程中会让你填一个“应用网关”和一个“授权回调地址”。前者是支付宝异步通知要回调到你服务器的地址后者是用户支付完跳转回来的页面地址。这两个地址在沙箱阶段可以先随便填等会儿联调的时候再改成你内网穿透的地址。1.2 密钥生成与配置一张图讲清公钥和私钥的关系这一步是劝退最多新手的环节因为涉及四把钥匙而且名字都很像。我慢慢捋。首先你要在本地生成一对RSA密钥对包括一个应用私钥和一个应用公钥。这两个钥匙一个是用来给请求签名的私钥一个是给支付宝验证你的身份的公钥。生成好之后你把应用公钥上传到支付宝开放平台支付宝那边会给你返回一个支付宝公钥。注意支付宝公钥不是你在开放平台控制台上下载的那个“支付宝公钥”而是系统根据你上传的应用公钥自动生成的一串字符。很多人就是在这里搞混了结果验签一直失败。为了帮你彻底搞清楚我列一个对照表钥匙名称存哪里用来干什么应用私钥你自己的服务器绝不能泄露对所有请求参数做签名应用公钥上传到支付宝开放平台支付宝用来验证你的签名支付宝公钥你的服务器配置文件验证支付宝向你发的通知和回调支付宝私钥支付宝服务器对通知和回调参数做签名说白了你发给支付宝的请求要用你的私钥签名支付宝收到后用你上传上去的应用公钥验签反过来支付宝发给你的回调要用支付宝私钥签名你收到后用支付宝公钥验签。这样就实现了双向身份认证谁也冒充不了谁。生成密钥对的方式很简单在电脑上用命令行工具生成也可以直接用支付宝的“密钥生成工具”一键生成。我用的是工具生成之后会得到一个私钥.txt和一个公钥.txt分别保存好就行。这里重点提醒一句应用私钥千万别提交到Git仓库也千万别发给任何人连同事都不行。这玩意一旦泄露别人就能冒充你的商户身份发起订单后果很严重。1.3 沙箱环境测试时的免费“模拟器”沙箱环境是支付宝给你的一套完整的模拟线上环境里面有一整套模拟的商家、买家、账号、APP还有专门的“沙箱钱包App”可以让你在手机上真的去扫一个模拟二维码、走一遍支付流程。很多人不理解沙箱的价值以为沙箱就是用来“试一下能不能调通”其实沙箱最大的价值在于让你可以用很低的成本把整个支付闭环跑一遍。你可以用一个模拟买家账号真正完成付款然后观察你服务器收到的异步通知长什么样这在联调阶段能省下大量时间。沙箱环境有两个关键入口一个是沙箱控制台里面能看到你的沙箱应用IDappId、沙箱支付宝公钥、沙箱买家账号和支付密码另一个是沙箱版的商家应用应用ID和密钥都是独立的跟正式的完全不通用。你在沙箱里把代码调通之后上线的时候只要把配置换成正式环境的APPID、应用私钥和支付宝公钥就行代码一行都不用改前提是你把配置写在了配置文件里。2. 支付流程的整体设计先看全局再看细节2.1 支付闭环的四个关键环节很多人一上来就急着写代码结果越写越乱就是因为脑子里没有一张完整的流程图。我不画图用文字把整个支付闭环给你捋一遍。整个流程从用户在你的网页/App上点击“去支付”开始到你收到支付结果并更新订单状态结束一共经历四个环节下单请求你的后端根据用户要购买的商品向支付宝发起一个创建订单的请求支付宝返回一个支付链接或者一段用于唤起收银台的参数。跳转支付用户被引导到支付宝收银台页面或唤起支付宝App完成支付。这个阶段用户看到的是支付宝的界面你什么都做不了也不该做什么。异步通知用户支付成功之后支付宝服务器会向你的后端接口发一个异步通知POST请求告诉你“这笔订单支付成功了”。这是整个衔接里最关键的环节你的系统要在这一步更新订单状态、加余额、发货等。同步跳转用户支付完成之后支付宝会把他重新引导回你配置的“回调页面”。很多人把同步跳转理解成“支付成功就可以给用户发货了”这是天大的误会。同步跳转只是给用户看的页面展示它随时可能因为用户关闭页面而丢失而且也无法保证用户一定支付成功。真正的业务操作一切以异步通知为准。这四个环节里第1步和第4步是前端能感知到的第2步和第3步是用户无感知的。你要搭建的后端能力其实就是两个接口一个是“创建订单”接口一个是“接收异步通知”接口。剩下的比如订单查询、退款查询都是为了让这两个接口更健壮而存在的辅助接口。2.2 为什么异步通知是“唯一真相”我刚接触支付的时候就犯过一个经典错误在同步跳转的接口里直接给用户开通会员。后来踩了坑才知道用户支付成功后那个同步跳转的页面用户如果中途断网、杀进程、或者关闭浏览器支付宝是没办法把这个页面推给用户的。用户把钱付了但你的系统不给他发货接下来就是赔钱、投诉、差评一条龙。所以支付宝设计了异步通知这套机制。用户的钱到了支付宝之后支付宝会以最快的速度向你的服务器发起通知如果通知失败它还会在24小时内连续重试多次确保你的系统只要活着就能收到支付结果。用生活化的方式理解一下同步跳转就是支付宝里的小喇叭在你耳边喊一声“付成功了”你听没听到、信不信都无所谓异步通知就是快递公司的签收单钱货两清这件事必须有一张白纸黑字的记录你必须在上面签字确认。2.3 金额计算与订单幂等性设计在编写代码之前还有一个特别重要且容易忽视的设计金额必须精确到分。支付宝的所有接口金额单位都是“元”但允许带两位小数比如88.00、0.01。这在Java里如果你用double或者float来做乘法、加法很容易出现精度问题比如0.10.20.30000000000000004这种传到支付宝那边就会提示“金额不合法”。我的做法是数据库里金额一律用DECIMAL(10,2)类型Java代码里金额运算一律用BigDecimal只有最终传给支付宝的时候才转成字符串。前端传来的金额后端必须以自己数据库里存的商品价格为准从来不要信任前端传过来的金额。这个道理很简单前端页面可以被篡改你可以自己写个脚本把“支付金额0.01”发到后端后端如果不校验直接下单那就等着商家赔钱吧。再说订单幂等性。你的系统要生成一个唯一的业务订单号out_trade_no每次用户点击支付应该先去数据库创建一个订单拿到订单号再用这个订单号去调支付宝接口。同一个订单号重复调用支付宝接口支付宝会直接告诉你“重复下单”不会真的创建两笔支付宝订单。这就防止了用户快速连点两下“去支付”按钮生成两笔支付宝订单的情况。3. 实操环节后端代码一步步实现3.1 引入SDK与基础配置支付宝官方提供了Java SDK我建议直接用官方推荐的Alipay Easy SDK封装得比较简洁下沉了很多细节。你不需要把几百兆的完整SDK引进来只需要在pom.xml里加对应模块的依赖dependency groupIdcom.alipay.sdk/groupId artifactIdalipay-easysdk/artifactId version2.2.0/version /dependency然后在项目启动的时候初始化一下把从支付宝开放平台那边拿到的配置填进去Configuration public class AlipayConfig { Value(${alipay.appId}) private String appId; Value(${alipay.merchantPrivateKey}) private String merchantPrivateKey; Value(${alipay.alipayPublicKey}) private String alipayPublicKey; Value(${alipay.notifyUrl}) private String notifyUrl; PostConstruct public void init() { Factory.setOptions(new Config() .setProtocol(https) .setGatewayHost(openapi.alipay.com) .setSignType(RSA2) .setAppId(appId) .setMerchantPrivateKey(merchantPrivateKey) .setAlipayPublicKey(alipayPublicKey) .setNotifyUrl(notifyUrl)); } }这里有两个特别注意的点。第一gatewayHost沙箱环境和正式环境不一样沙箱要填openapi.alipaydev.com正式才填openapi.alipay.com。第二signType一定要用RSA2这是SHA256WithRSA的签名算法安全性更高支付宝现在已经不推荐用老的RSA了。你可能会问setMerchantPrivateKey和setAlipayPublicKey存的是什么前面已经说了前者是你本机生成的私钥后者是支付宝开放平台控制台里给你展示的那串公钥。这两个东西建议放在配置文件里不同环境对应不同值不要在代码里写死。3.2 创建订单接口给用户一个支付链接以电脑网站支付为例我们写一个创建订单的接口。这个接口的职责是接收前端传来的商品信息创建本地订单然后调用支付宝接口拿到一段HTML表单或一个跳转URL返回给前端。PostMapping(/createOrder) ResponseBody public String createOrder(RequestParam Long goodsId) { // 1. 查商品、算价格价格以数据库为准拒绝前端传参 Goods goods goodsService.getById(goodsId); BigDecimal totalAmount goods.getPrice(); // 单位元 // 2. 生成业务订单号并落库 String orderNo generateOrderNo(); // 例如20240517103000123456 Order order new Order(); order.setOrderNo(orderNo); order.setGoodsId(goodsId); order.setAmount(totalAmount); order.setStatus(WAIT_PAY); orderService.save(order); // 3. 调用支付宝电脑网站支付接口 AlipayTradePagePayModel model new AlipayTradePagePayModel() .setSubject(goods.getName()) .setOutTradeNo(orderNo) .setTotalAmount(totalAmount.toPlainString()) .setProductCode(FAST_INSTANT_TRADE_PAY); return Factory.Payment.Page().pay(model.getModel()); }这段代码里setOutTradeNo传的就是你自己生成的业务订单号setTotalAmount是精确到分的金额字符串setSubject是商品标题。pay()方法返回的是一段HTML字符串前端拿到之后只需把这段 HTML 塞到一个隐藏的 div 里然后调用表单的 submit 方法就能自动跳转到支付宝收银台页面。这里有个容易忽略的细节支付宝的out_trade_no必须是你同一笔业务订单支付的唯一标识不能重复。如果用户支付到一半关闭了页面过一会再回来点击“继续支付”你应该用同一个订单号再次调用支付宝接口而不是新建一个订单号。3.3 接收异步通知整个对接里最核心的一环现在到了整个对接的核心也是很多自己摸索的朋友最容易翻车的地方。异步通知是支付宝主动向你的服务器发起的POST请求你需要在项目里写一个接口接收它并且正确处理返回内容。PostMapping(/alipay/notify) public String alipayNotify(HttpServletRequest request) throws AlipayApiException { // 1. 从request中获取支付宝POST过来的所有参数 MapString, String params new HashMap(); MapString, String[] requestParams request.getParameterMap(); for (String name : requestParams.keySet()) { String[] values requestParams.get(name); StringBuilder valueStr new StringBuilder(); for (int i 0; i values.length; i) { valueStr.append(i 0 ? : ,).append(values[i]); } params.put(name, valueStr.toString()); } // 2. 验签确认这是支付宝官方发来的通知而不是伪造的 boolean signVerified AlipaySignature.rsaCheckV1( params, alipayPublicKey, UTF-8, RSA2 ); if (!signVerified) { return failure; } // 3. 校验业务数据 String outTradeNo params.get(out_trade_no); // 你自己的订单号 String tradeNo params.get(trade_no); // 支付宝交易号 String totalAmount params.get(total_amount); // 支付金额 String tradeStatus params.get(trade_status); // 交易状态 // 重点根据订单号查数据库校验金额是否一致 Order order orderService.getByOrderNo(outTradeNo); if (order null || order.getStatus().equals(PAID)) { // 订单不存在或已处理直接返回success防止重复处理 return success; } if (order.getAmount().compareTo(new BigDecimal(totalAmount)) ! 0) { log.error(订单金额不一致orderNo{}, 库内金额{}, 支付宝通知金额{}, outTradeNo, order.getAmount(), totalAmount); return failure; } // 4. 只有TRADE_SUCCESS或TRADE_FINISHED才是最终支付成功 if (TRADE_SUCCESS.equals(tradeStatus) || TRADE_FINISHED.equals(tradeStatus)) { // 更新订单状态为已支付记录支付宝交易号 orderService.markPaid(order.getId(), tradeNo); } // 5. 处理成功必须给支付宝返回纯文本success return success; }这段代码的几个关键点我拆开讲。验签是第一步。支付宝的通知接口是公网地址任何人只要能猜到你的回调URL就可以往这个地址发请求。如果你不做验签别人伪造一个“支付成功”的通知你的系统就给用户发货那就亏大了。重复通知是常态。支付宝的通知机制是保证最终一致性的它会一直重试直到你返回success。如果一个通知因为网络原因丢了支付宝会重发如果你的接口处理了一半挂了支付宝也会重发。所以我在代码里做了幂等处理如果订单已经是PAID状态直接返回success不再重复处理。返回内容必须是纯文本success。很多新手在回调接口里返回JSON、返回HTML、或者返回true结果支付宝一直重试还把错误日志刷爆。支付宝的约定就是你返回success全小写表示处理成功不用重试返回其他任何内容都表示处理失败支付宝会继续重试。我见过有人在里面返回了success后面带个空格都导致无限重试的这种细节真的要留意。3.4 同步跳转与订单查询兜底同步跳转接口和异步通知不同它只负责给用户展示一个“支付成功/失败”的页面不处理任何业务逻辑。如果非要在同步跳转里干点啥顶多是根据out_trade_no去查一下本地订单状态然后把订单号传给前端页面让前端去展示对应结果。需要注意的是用户在收银台点完“完成”之后支付宝跳转回你的页面时带有out_trade_no和trade_no参数但这些参数没有签名所以千万不能信任它们的值只能用来展示。同步跳转丢了不代表支付失败这就是我前面反复强调的“一切以异步通知为准”。但万一异步通知因为某种极端原因一直没到呢支付宝也考虑到了这个问题提供了主动查询订单的接口你可以定期调用alipay.trade.query接口根据自己的out_trade_no去问支付宝“这笔订单到底支付了没有”。public boolean queryPayResult(String outTradeNo) throws Exception { AlipayTradeQueryModel model new AlipayTradeQueryModel() .setOutTradeNo(outTradeNo); AlipayTradeQueryResponse response Factory.Payment.Common().query(model); if (response.isSuccess()) { return TRADE_SUCCESS.equals(response.getTradeStatus()) || TRADE_FINISHED.equals(response.getTradeStatus()); } return false; }在正式环境中定时任务比如每分钟跑一次查询那些状态还是“待支付”但已经超过某个时间阈值的订单把真正的成功状态同步回来这也是对异步通知的一种兜底。我自己做项目时会把“处理支付结果”的逻辑抽成一个公共方法异步通知和主动查询都调这同一个方法确保两条路径的处理逻辑完全一致。4. 本地联调沙箱、内网穿透和测试场景4.1 用沙箱App模拟真实支付联调的时候你不需要真的花真金白银去支付。支付宝沙箱环境提供了一整套模拟工具包括一个沙箱版支付宝App你可以在手机上安装它然后用沙箱给的测试买家账号登录扫描PC网站支付生成的二维码或者唤起H5支付完整地走一遍支付流程。沙箱买家账号在开放平台控制台的“沙箱环境”页面能看到里面包含一个手机号和支付密码。你需要在沙箱钱包App里用这个账号登录然后做支付操作。和线下支付不同沙箱支付没有真实的付款环节点击确认支付就直接成功但你依然能观察到支付成功之后支付宝发起的异步通知、你的服务器端如何处理、同步跳转是否正常等完整链路。有一点很疼沙箱环境的通知是发到公网地址的。你本地电脑跑着localhost:8080支付宝服务器不可能访问到你的电脑所以你需要一个内网穿透工具把本地的端口映射到公网上。4.2 内网穿透让支付宝能“敲”到你的电脑我本地联调用的工具是natapp类似的还可以选ngrok、cpolar原理都一样把本地的某个端口暴露到一个公网域名上这样支付宝的通知就能通过公网域名访问到你内网机器上的接口。步骤如下下载内网穿透客户端注册一个免费隧道本地端口填你SpringBoot服务的端口比如8080。启动穿透服务你会得到一个公网地址比如http://abc123.natapp.cc。把这个地址拼上你的回调路径填进支付宝沙箱应用的后台配置里应用网关填http://abc123.natapp.cc/alipay/notify授权回调地址填http://abc123.natapp.cc/alipay/return。重新启动项目就可以在本地完整调试整个支付流程了。免费版的内网穿透会有一个随机域名不用纠结这个域名好不好看它只是为了调试用的。正式上线时你把公网地址换成你真实的服务器域名就行。4.3 联调测试要覆盖的几种场景联调不是“付一笔钱成功就完事了”我建议你把下面这些场景全部跑一遍再考虑上线正常支付成功用户创建订单、跳转收银台、完成支付观察异步通知是否正常入库。用户关闭页面不支付订单一直处于“待支付”支付超时后你的系统应该正常关闭订单不能影响其他订单。支付成功但通知超过24小时才来模拟网络极端情况你的系统必须能正确处理延迟通知不能因为时间差就误判订单状态。同一订单重复通知模拟支付宝重试通知你的接口要保证幂等性不能重复给用户加余额。金额不一致的通知人为构造一个金额不对的通知验证你的系统能不能识别出来并拒绝处理。验签失败的通知用错误的密钥发一个通知验证你的系统是否直接拒绝。如果你能把上面这些场景全部跑通你的支付模块基本就稳了。5. 常见问题与排查技巧实录5.1 必坑指南最容易翻车的8个问题问题现象根本原因解决办法调下单接口报“签名错误”应用私钥没配对或网关地址错误检查私钥是否有换行符沙箱使用openapi.alipaydev.com验签一直失败把“应用公钥”当成“支付宝公钥”验签了在控制台复制“支付宝公钥”不是上传的那个公钥异步通知一直收不到应用网关没填对或用了内网地址确保回调地址是公网可达地址且没有IP白名单限制异步通知无限重试接口返回值不是纯文本success返回内容必须严格是success不能有空格、引号、JSON用户支付成功但订单还是待支付处理了同步跳转没处理异步通知所有业务逻辑以异步通知为准同步跳转只做展示订单状态被重复更新没有做幂等处理处理前先判断订单状态已处理过的直接返回success金额计算对不上用了double或float算钱一律用BigDecimal以数据库价格为准上线后沙箱环境的密钥串了配置文件里写死了沙箱的APPID和网关用Spring Profile区分dev和prod配置5.2 日志是排查支付问题的最好朋友支付对接出问题的时候最怕的就是你连日志都没打全。我见过太多人在群里问“为什么回调没触发”“为什么订单状态不对”最后一看日志文件里连最基本的入参都没打排查半天全靠猜。我的习惯是在支付相关的接口里至少打以下几类日志创建订单时打印订单号、金额、商品信息、支付宝返回的入参。调支付宝接口时打印接口名、请求参数脱敏后的、响应参数、耗时。接收异步通知时打印所有通知参数、验签结果、处理结果。主动查询订单时打印查询条件、查询结果、是否命中异常分支。有了这些日志你回放问题的时候基本能还原整个时间线。特别是异步通知重试的场景只有日志才能告诉你支付宝到底发了多少次通知、每次都带什么参数、你的系统每次都返回了什么。5.3 公钥私钥相关的三个细节坑关于密钥我再补充三个很多人都踩过的细节。第一私钥复制到配置文件时确保换行符没丢。有些编辑器在复制私钥时会自动去掉换行或者加上奇怪的空格导致签名失败。最稳妥的做法是配置文件里用\n显式换行或者直接把私钥放在单独的文件里加载。第二支付宝公钥和控制台上传的应用公钥长得不一样。支付宝公钥由支付宝生成并展示应用公钥是你自己生成的。验签用的是支付宝公钥上传用的是应用公钥。这个搞反了第一步就过不去。第三线上和沙箱的密钥完全不通用。你切环境的时候要确保APPID、应用私钥、支付宝公钥、网关地址这四样东西全部一起切。我就见过有人只改了APPID和网关私钥还是沙箱的结果线上所有请求全部签名失败。6. 上线前最后检查清单支付模块上线不是“代码能跑就上”以下这几项我都吃过亏列出来给你当自检清单。正式环境的APPID是否替换成正式应用的APPID。正式环境的应用私钥是否正确且没有提交到代码仓库。应用是否已经添加了支付能力并且审核通过。应用网关和授权回调地址是否已经改成了正式环境的公网域名。协议是否使用HTTPS支付宝生产环境要求HTTPS回调。金额计算是否全程使用BigDecimal前端是否不能传金额。回调接口是否做了验签、幂等、金额校验。是否已经打了足够多的日志方便线上排查问题。是否已经跑通了退款、关闭订单等售后流程。定时对账任务是否已配置建议每天跑一次拉取支付宝账单和自己数据库核对。其中“对账任务”可能很多人会忽略但这恰恰是保证资金安全的关键一环。支付宝提供了账单下载接口你可以每天拉取前一日的账单明细跟自己数据库里的支付记录进行核销比对如果发现哪笔钱支付宝说你收了但你这边没记录或者反过来那就要人工介入了。我之前接手的一个项目就是因为没有对账导致连续三天的退款订单漏处理最后赔了不少钱。再分享一个我的个人习惯正式环境上线第一周我会每天手动跑一次订单查询任务主动比对支付宝和本地数据的一致性同时观察异步通知日志里有没有“重复通知”“延迟通知”的异常情况。等连续三天数据完全对上再把这个任务改成自动执行。支付对接这件事说难不难核心就那么多东西说简单也不简单因为它涉及资金任何一个细节错了都可能造成实际损失。但只要你自己把全流程从下单到回调、从查询到对账完整地走一遍并且把上面这些坑都避开你就能在心里对这套系统建立足够的掌控感。后面无论接到微信支付还是其他支付渠道底层逻辑都是同一套你只会越做越顺。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →