对接浦发银行支付(四)-- 支付回调接口

2024-04-20 08:28

本文主要是介绍对接浦发银行支付(四)-- 支付回调接口,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、背景

和退款接口可能是同步不一样,支付必须是异步实现。在拉起支付后,等待用户输入密码,然后支付方最后回调我们对外提供的接口。

支付功能本身简单易懂,下面浦发银行和公司研发环境的交互流程:

我们需要开放一个对外的回调接口,便于浦发银行来回调,告知我们用户支付成功了,然后我们更新订单状态,进一步处理自己的业务逻辑。

在这里插入图片描述
具体到对接浦发银行,它的签名是在htt header的"X-SPDB-SIGNATURE"中,body中的报文是加密的,待我们解密。
第二步,我们使用浦发行的公钥以及密文

二、开放支付回调接口

1、内外网穿透

把公司研发环境下的Kong机器通过端口映射到外网,定义一个端口。

2、Kong自定义插件

支付服务一共有许多接口,大多供内部调用,并不对外暴露。
kong作为api网关,开发一个自定义插件,按需暴露支付回调接口。

在这里插入图片描述

URL前缀是: http://{你的外网IP}:{端口}/pay

配置Kong自定义插件:
在这里插入图片描述
对外暴露的支付回调接口,特别需要注意安全,防止他人篡改报文。

所以才有加密和签名这两道墙,而不是明文的方式。

一旦被攻破了这两道防线,用户则无需真正支付,而让你的订单完成“支付”。

可真的是一分钱都不用花~ 慎重!!

3、定义支付服务的回调接口

支付服务定义一个接口:

    @PostMapping("/api/xxx/callback")public String hzbankPayNotifyRes(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {String spdbSign = new String(request.getHeader("X-SPDB-SIGNATURE").getBytes(), CharsetUtil.UTF_8);String requestResultJson = IOUtils.toString(request.getInputStream(), request.getCharacterEncoding());if (log.isInfoEnabled()) {log.info("浦发银行支付回调通知, spdbSign={}, requestResultJson={}", spdbSign, requestResultJson);}
// 略}

三、浦发开放平台配置RSA密钥

这一段直接取自浦发银行的文档,熟悉配置的,可以跳过~

先新增“APP”,点击“开发者公钥”,选择“外调RSA公钥”。

RSA密钥对,可以在线生成,当然也可以把已有的赋值。
在这里插入图片描述
输入RSA公钥,上传的公钥需携带头部“-----BEGIN PUBLIC KEY-----”及尾部“-----END PUBLIC KEY-----”,点击保存后, 等待几秒会自动回显浦发RSA公钥,用于验签。
在这里插入图片描述
作为程序开发,你需要保存好合作方公钥及私钥,公钥交给浦发,它会使用RSA公钥对支付回调的报文进行加密;私钥配置在支付服务的程序里,用于对支付回调的密文进行解密。

除此之外,你还需要保存并配置浦发公钥,它是用于签名的验证。

四、支付回调处理逻辑

        # 解密报文String decryptBody = SpdbSignUtil.decryptStr(requestResultJson, rsaPrivatekey);Map<String, Object> resultMap = JSON.parseObject(decryptBody, HashMap.class);// 下面两个key是用来传递签名和明文,待验签用resultMap.put(SpdbConfig.SIGN, spdbSign);resultMap.put(SpdbConfig.NOTIFY_BODY, decryptBody);// 处理支付订单// 验证签名SpdbSignUtil.verifySign(MapUtil.getStr(resultMap, SpdbConfig.NOTIFY_BODY),MapUtil.getStr(resultMap, SpdbConfig.SIGN), spdbRsaPublickey));

1、报文解密

http request body是json格式,为了安全性,采用标准RSA方式加密,待我们解密。

具体的示例代码见下文,它使用hutool的crypto开源项目,本身没什么难度。

但是,我想要说的是:
浦发银行的支付回调设计不合理,不合理的点在于:解密需要RSA密钥,而RSA密钥是配置在浦发开放平台的APP下。

理论上每个APP可以配置不同的RSA密钥对,如果支付服务对接了多个浦发银行的商户,在解密的时候,是需要知道使用哪个商户的RSA密钥。

而这显然是无法做到的!!

我的建议是浦发银行在回调的时候,除签名字段外,把商户ID字段也通过http header传递给商户。

如下,我们便可以先取得商户ID,反查商户的RSA密钥,下一步才可以解密http body里的密文为明文。

现在的解决办法是:既然rsa密钥对是固定的值,直接在程序里配置。(如果硬是要配置在商户信息中,那就必须保证所有的商户共用一套RSA密钥对。)

2、验签

按下文给的验签示例,它依赖于支付回调接口中的签名(在http header中的字段“X-SPDB-SIGNATURE”)

需要注意的是,验签的方法,除了签名外,另外一个参数是报文的明文,不是支付回调的密文。(所以它依赖于上一步的解密)

千万别把验签方法的入参搞错了,传递的是明文,而非密文。。。

3、代码示例

摘自浦发银行官方文档


import cn.hutool.core.codec.Base64;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.RSA;
import cn.hutool.crypto.asymmetric.Sign;
import cn.hutool.crypto.asymmetric.SignAlgorithm;import java.io.UnsupportedEncodingException;public class Test {/*** 合作方RSA私钥*/private static final String MY_PRIVATE_KEY ="MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALYaGizzujcBJpSf\n" +"cUDBXe1Ovl6PPLqaNie+8bqAObbPefwDvJdn77BUiPUqomBLb+JdUej4VIQfmza9\n" +"n3wWMw67Uq9AccCWeMpJNzzfMk42HaYZ0A3+foi/XUtZREkhXnH+3YKZ6k9fujfA\n" +"VCPFHnwO/MJ5BVzHwSQC9jcS1oqjAgMBAAECgYBqvkI5t2SgeXw0AoJQgwib4lyU\n" +"8UGX4G1+Pt9Tg3ZRQq0unMIfvj0yD1t42tTzvUSIXEt3VJm2GRDStbSW+CxUqOFW\n" +"6zdHU1ySEn1rfAQVgWR7OEEyRo3gXXHycGNwHqNnPwuza8vLaxxoIRGMFUL2aVsc\n" +"y6Td6ZMKkwJ3ndESAQJBAONtZFnZN7hgHmoWGfI5z1OPk/bK7tx6ibwQx7dx5jB2\n" +"02Fn7Upd9ZSmP/DCPige+R8EN6m4VjOlCvPoR4TQDxECQQDM+vAwLVzC/QUe5Jan\n" +"j/7m3CQSMpRCUjX0WzqUj76EBR1fYrHV5cI8r2bSRxEtKGBqk4esNgnJoLVTodtL\n" +"b2ZzAkB/JSYoMQ88rcfzKT4CNJ2bKrbfD17wtjUQhhURksTNLXFJkI+RtuvX2gX/\n" +"NKkJRx+hXns8EElpAAkaiS6KqsLxAkEAyXr20EQmY7sUZ3NE6ltNsFo+UmzI8g+g\n" +"3Rk3EYPhPh9Q6cs3Bgqay8+U/6e/KGYBr4Bn4UwUfs2qrhPwW8uaJQJBAMrGBS7P\n" +"gxsqS4XA4JN71B3zQkkWr/EIccK2T4q3hqRtUEtF8c9NShkFc10jlZ7hL+aDqPg4\n" +"ZQ78AnvXnIW0aFA=";/*** 浦发RSA公钥*/private static final String SPDB_PUBLIC_KEY ="MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAh6sKYf0mpJuOEAavMi5p\n" +"DCjBJUcXY85osHBoAwg37BMOCC6uLlGdssgjp9XgzSJ8iss8z3TT4lOXYqPEIx4c\n" +"wAd62fXHGYWswbVAeDPMAlAKKiMElvSITO8C55Ui95AKo4xu5SHkytRYfJ2u0OD8\n" +"r6G7dpdyXS8lb23PWOxpj0Xs+rlGjkfY9pDHS90CPNFhjnUATPpI0blzT/If1F0l\n" +"GtOy6+ArIS8XaAij0topNlglangv5jBiNdTEP/U0mWH7TWAvG15cwoLSZMz/W+/S\n" +"VrEmnm94WZaYIUtPuY6pdLgnPydsNAEOu6J+uY0J8uPKYY4XsrC8vLajFaeTGL24\n" +"EQIDAQAB";/*** 验证签名* @param decryptBody 报文明文* @param spdbSign 浦发签名,一般放在请求头的"X-SPDB-SIGNATURE"字段中* @return 是否验签通过* @throws UnsupportedEncodingException*/public static boolean verifySign(String decryptBody, String spdbSign) throws UnsupportedEncodingException {Sign sign = SecureUtil.sign(SignAlgorithm.SHA1withRSA, null, SPDB_PUBLIC_KEY);boolean verify = sign.verify(decryptBody.getBytes("utf-8"), Base64.decode(spdbSign));return verify;}/*** 使用合作方私钥解密报文体* @param encryptBody 报文密文* @return 解密后的明文*/public static String decryptStr(String encryptBody) {RSA rsa = new RSA(MY_PRIVATE_KEY, null);String decryptStr = rsa.decryptStr(encryptBody, KeyType.PrivateKey);return decryptStr;}public static void main(String[] args) throws UnsupportedEncodingException {//示例解密String body = "H8dzLP11ulqDeC7neeJ8OP1SmFqtkQxEVb3hJku8FylrpriTAQATa2+y+QQcEDcbm0GnnTaO7iWivbb1I1cjowdj9Tn+pRYicOhplrRIX63KzOmVcBW4LslvRf2olEf1qt0rvj38rIPZMLX7k6RlCmfwTYxYOdkYce8t+N0mzSEtZUyLiyKP2C2EtTsKD2feA0r0SabW/z4E62n5WhO0NK3eBsbzCodXUHYyUyJ4Qp5rq5ol8wJ6mzgJ1bDvQZXHs3AGGzdUgy/+7zQyBsnBfMaMnEVa+s/cDWL8lD2jw46pQeattUvfkpBeqaMH/iRy/q1gmeHCXrTcQVJUfMEPhGdpcr4XSqabainIQDNK79qcfCxU/zCrKaKB17de0G+5BSAhlJ9ZM6mnFHyatYlcCnBYSz5/x0QuPZiUOcDfRoa6jbc5mW5egph48d+O0wRM3USrFrWBGSWI7liMEV6OvJaQXqQyOOeLQhTS5cPfXnxIf0bo+6VvCEdMQMJ2kFlH";String decryptBody = decryptStr(body);System.out.println("解密结果:");System.out.println(decryptBody);//示例验签String sign = "T09EE/ofLiDemcNazhaqNp5a9oH9MOR/aNA3B7Vz/3+qUo//92+VPs/Sn31XFWNHA3KjDuwesTIVwXxvb+XgUxhPCK/AXpFMVqbL3Tw3BL9QpvS9Q+ZsCtM+X52bOB66N9dPNVjZUYnwt2SU704zk4XZFH/CMDP8XLvLdvnoPjyFS532yOKRLidRb3fKKseIRs8bUxmhKb4S7NHIursGQLlzQuPWxzlTHbBAbZJEZNWgO0XWAT0sLcGmQInrR7ujmiB/D4CiGkMp9mtca1B2MBs31eQboLb9+uRtM9kGsHxw/nV/MQJ/ZzBcwC1c7XDnnbJCIA0LRaLlnAYVOg6eiA==";System.out.println("验签结果:");System.out.println(verifySign(decryptBody, sign));}
}

支付账单

在这里插入图片描述
从该账单详情,可以看到以下信息:

  • 4200开头的交易单号是微信的支付流水号,这是微信和浦发银行对账的依据。
  • 1901开头的商户单号是浦发银行的支付流水号,这将是我们和浦发银行对账的依据。

不会展示我们平台的支付流水号,因为我们对于微信来说,是透明的,微信只认浦发银行。

当用户对于支付账单由疑问的时候,客户可以基于这个账单中1901开头的商户单号,要能够反查出我们商城的业务订单信息以及支付订单信息。

五、总结

鉴于上面的明文和密文都是一个示例,不是浦发实际的参数。

最后给出真实的报文及签名内容:

  • 支付回调的签名
eoClolWapK8NZp8lxgA9ED74zrfVUwbJND5vBYpS/MnefVDqrrA+72hhuYTLcfIZo6R41/MSdBfrGlPccHX3JN/jCIG18wo59NNRt6T83timCZZefgZKOVJOMYie3mBK9A/NDHqSHhRT24xxxUH3PvfKOb7/3svMvuWkEK3I/tzyb72Qkqz5InfH3S55VLb+P9GZGkM6kmUin1dhhCC67kHkTbxc9bX7ry+lDShwsXqfgbaHdTw/iF+CVE3795Zn53W1qGWvI1oJUWcko3zAcxAKTy0CEdQRWMSWP+ZsmtRp3Q8px75hRHTcKEeY+UBtvis9vNpZoW4dAbTU5ZSVtg==, 
  • 支付回调的密文
gDrBttO3tKx49tDC6KZN+XdX47rmx0Z28+iUy4rL0j7+KH5LnyVXFIBYoFwmR5NoQUJV49vYbC7vZwCoAkH6WYocyKoDuv2xUpEbwkb/XBQEMPIMoiHhPuixnzxvPQRM7vQDEg4eI5FnYqtYO4wgh7RoZJfEPBeZ9wQHl0G5pLIxDJTs/LNC16FzG1St5CA0zcz9ttkRJZdyBaAzis+8/AaDmv6GxqLin/nlqxHoZCgTxUN5+xMvSkV9pM17Y/NQfLexE6fy1ooqZ1Am5FwbYVfU0UtEWOhZf2JAhT3b956imVBpo7hK6uQymNLG4/temwdvlYg1QFtE31wFJZ9k2m9GBL17ll1F7jdENzoibHtIrGoYIQr1fwb16P7FXTfDb0fT3+eJ20vqHKaLi0oJOmXGzB6zqAwqxCnQQ9YVVJXUjqFPvF49AZKh1ZaJ5s9bYQyuprfAXsy4YQizFyOTmmncK9/R2JiUuK5b6qFnk2UPqtnDxoXuiewk1DSi2lPgelN1IUPe8HBoctO5htbgnXN5RbjiXvx7DfvPafoD/MovwbSrKCooqt1Iww/kL8R2RQeymrdcb5MXxfg9Koap2kcQv873fOh5u0MhwqgcNYytp49eYFI61naTrVikGIYnK2zdupBTHOvpulkAy9nVzrLrQCmhpbhqbpRgvBa5B+4=
  • 解密后的明文:
{"TransTime": "20240417171758","SysSeq": "061632","Channel": "1A31","TransAmt4": "000000000001","ChlTp": "1","MrchOrdrNo3": "0624041717B1815018507","Ccy4": "156","OrdrNo1": "1901041717172200168952061726","ThdPtySeq": "4200002171202404172188555631","OrdrSt": "00","TransDate": "20240417","PyBnkInfo": "OTHERS","TranTp1": "OA","MrchNo": "310319982990001","TerminalNo2": "98A00162"
}

返回报文的有用字段是:

{ # 支付时间
“TransTime”: “20240417171758”,
# 支付金额
“TransAmt4”: “000000000001”,
# 平台支付流水号(商户支付流水号)
“MrchOrdrNo3”: “0624041717B1815018507”,
# 浦发银行支付流水号
“OrdrNo1”: “1901041717172200168952061726”
}

在这里插入图片描述
在这里插入图片描述

程序以浦发银行支付流水号为准,查询支付订单,进而更新订单状态和实际付款时间。

顺便吐槽一下其文档,如果平常开发中有人这样定义字段,不被喷才怪。。

要理解其字段的内涵,必须结合报文内容,才能对得上。

浦发并没有提供代码,这导致你在写代码的时候,字段名都不敢手敲,极容易写错字段名。

最后,我说下,文档里没找到支付时间TransTime。(对接的时候看文档,未联调,还不理解其回调报文怎么不返回呢)

这篇关于对接浦发银行支付(四)-- 支付回调接口的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/919760

相关文章

详解Java如何向http/https接口发出请求

《详解Java如何向http/https接口发出请求》这篇文章主要为大家详细介绍了Java如何实现向http/https接口发出请求,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 用Java发送web请求所用到的包都在java.net下,在具体使用时可以用如下代码,你可以把它封装成一

Java后端接口中提取请求头中的Cookie和Token的方法

《Java后端接口中提取请求头中的Cookie和Token的方法》在现代Web开发中,HTTP请求头(Header)是客户端与服务器之间传递信息的重要方式之一,本文将详细介绍如何在Java后端(以Sp... 目录引言1. 背景1.1 什么是 HTTP 请求头?1.2 为什么需要提取请求头?2. 使用 Spr

Python中的异步:async 和 await以及操作中的事件循环、回调和异常

《Python中的异步:async和await以及操作中的事件循环、回调和异常》在现代编程中,异步操作在处理I/O密集型任务时,可以显著提高程序的性能和响应速度,Python提供了asyn... 目录引言什么是异步操作?python 中的异步编程基础async 和 await 关键字asyncio 模块理论

Java 后端接口入参 - 联合前端VUE 使用AES完成入参出参加密解密

加密效果: 解密后的数据就是正常数据: 后端:使用的是spring-cloud框架,在gateway模块进行操作 <dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>30.0-jre</version></dependency> 编写一个AES加密

如何更优雅地对接第三方API

如何更优雅地对接第三方API 本文所有示例完整代码地址:https://github.com/yu-linfeng/BlogRepositories/tree/master/repositories/third 我们在日常开发过程中,有不少场景会对接第三方的API,例如第三方账号登录,第三方服务等等。第三方服务会提供API或者SDK,我依稀记得早些年Maven还没那么广泛使用,通常要对接第三方

java线程深度解析(一)——java new 接口?匿名内部类给你答案

http://blog.csdn.net/daybreak1209/article/details/51305477 一、内部类 1、内部类初识 一般,一个类里主要包含类的方法和属性,但在Java中还提出在类中继续定义类(内部类)的概念。 内部类的定义:类的内部定义类 先来看一个实例 [html]  view plain copy pu

模拟实现vector中的常见接口

insert void insert(iterator pos, const T& x){if (_finish == _endofstorage){int n = pos - _start;size_t newcapacity = capacity() == 0 ? 2 : capacity() * 2;reserve(newcapacity);pos = _start + n;//防止迭代

京东物流查询|开发者调用API接口实现

快递聚合查询的优势 1、高效整合多种快递信息。2、实时动态更新。3、自动化管理流程。 聚合国内外1500家快递公司的物流信息查询服务,使用API接口查询京东物流的便捷步骤,首先选择专业的数据平台的快递API接口:物流快递查询API接口-单号查询API - 探数数据 以下示例是参考的示例代码: import requestsurl = "http://api.tanshuapi.com/a

股票数据接口-陈科肇

陈科肇 新浪财经 sz-深圳sh-上海历史分价表:http://market.finance.sina.com.cn/pricehis.php?symbol=sz000506&startdate=2016-12-27&enddate=2016-12-27历史成交明细(当日成交明细):http://vip.stock.finance.sina.com.cn/quotes_service/v

关于回调函数和钩子函数基础知识的整理

回调函数:Callback Function 什么是回调函数? 首先做一个形象的比喻:   你有一个任务,但是有一部分你不会做,或者说不愿做,所以我来帮你做这部分,你做你其它的任务工作或者等着我的消息,但是当我完成的时候我要通知你我做好了,你可以用了,我怎么通知你呢?你给我一部手机,让我做完后给你打电话,我就打给你了,你拿到我的成果加到你的工作中,继续完成其它的工作.这就叫回叫,手机