本文主要是介绍对接浦发银行支付(四)-- 支付回调接口,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
一、背景
和退款接口可能是同步不一样,支付必须是异步实现。在拉起支付后,等待用户输入密码,然后支付方最后回调我们对外提供的接口。
支付功能本身简单易懂,下面浦发银行和公司研发环境的交互流程:
我们需要开放一个对外的回调接口,便于浦发银行来回调,告知我们用户支付成功了,然后我们更新订单状态,进一步处理自己的业务逻辑。
具体到对接浦发银行,它的签名是在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。(对接的时候看文档,未联调,还不理解其回调报文怎么不返回呢)
这篇关于对接浦发银行支付(四)-- 支付回调接口的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!