支付宝spi接口设计验签和返回结果加签注意点,支付宝使用JSONObject对象
支付宝spi接口设计验签和返回结果加签注意点,支付宝使用JSONObject对象
SPI 三方服务接入指南
https://opendocs.alipay.com/isv/spiforisv
服务端实现 Demo
以下 Demo 是通过 Java 实现的 SPI 服务样例,包括验签 支付宝请求报文、业务逻辑处理、商家加签 以及 响应报文构造 的逻辑。
该 Demo 仅供参考,不同语言环境可根据该 Demo 的处理思路自行实现。
@RequestMapping(value = "/isv/spi/service",consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
@RequestMapping(value = "/isv/spi/service") @ResponseBody public String spiService(@RequestParam Map < String , String > params ) { // http响应结果载体 JSONObject result = new JSONObject(); // 业务处理结果载体 JSONObject response = new JSONObject(); // 1、验签支付宝请求报文 boolean isPass = AlipaySignature.rsaCheckV1( params , alipayPublicKey , "UTF-8" , "RSA2" ); if(isPass) { // 2、验签成功:处理业务逻辑,并构造业务处理结果 response.put("code" , "10000"); response.put("msg" , "Success"); response.put("biz" , "value"); JSONObject person = new JSONObject(); person.put("age" , "18"); person.put("height" , "180"); response.put("person" , person); // response中嵌套复杂类型数据结构场景 } else { // 验签失败:构造错误码 response.put("code" , "40004"); response.put("msg" , "Business Failed"); response.put("sub_code" , "ISV-VERIFICATION-FAILED"); response.put("sub_msg" , "验签失败"); } // 3、业务处理结果加签 (可选,查看 服务基础配置 章节) // contentToSign为 {"code":"10000","msg":"Success","biz":"value","person":{"age":"18","height":"180"}} String contentToSign = response.toJSONString(); String sign = AlipaySignature.rsaSign(contentToSign,isvPrivateKey ,"UTF-8","RSA2"); // 4、构造http响应结果 result.put("sign" , sign); //可选,查看 服务基础配置 result.put("response" , response); // 返回json格式响应报文 return result.toJSONString(); }
扩展:return result.toJSONString(); // resultJson
支付宝拿到这个返回的字符串。如何来验签呢?
//模拟接收端的方式 JSONObject jsonObject = JSONObject.parseObject(resultJson); String responseJson = jsonObject.getString("response"); String signJson = jsonObject.getString("sign"); System.out.println("responseJson=" + responseJson); System.out.println("signJson=" + signJson); boolean isPass = AlipaySignature.rsaCheck(responseJson, signJson,pub_key, "UTF-8", "RSA2"); System.out.println("isPass=" + isPass);
需要注意点:
1. 接口使用Map来接收 @RequestParam Map < String , String > params
// 1、验签支付宝请求报文
boolean isPass = AlipaySignature.rsaCheckV1( params , alipayPublicKey , "UTF-8" , "RSA2" );
接口流程是:先验签,然后再解析,验证不用管map里面是什么参数,支付宝请求传什么参数,商户正常验证就行。而不是使用对象或单个字段来接收,无法确保接收的数据是全部的。不然影响签名。
同理:商户对结果加签,支付宝也是不管json串传什么参数,将json串转换JSONObject,然后get需要验签的response节点的数据做验签。(参考以上://模拟接收端的方式 代码)
支付宝调用过来的:是支付宝使用支付宝私钥签名,商户端使用支付宝公钥验签。
同理:如果是商户对结果加签,是使用商户的私钥签名,支付宝接收到后使用商户的公钥来验签。
使用map获取签名的字符串方法:String content = AlipaySignature.getSignCheckContentV1(params);
//map的加签字符是可以固定顺序的。同理:json字符串固定顺序只能通过使用相同的对象(JSONObject)来转换达到字符相同。
public static String getSignCheckContentV1(Map<String, String> params) { if (params == null) { return null; } params.remove("sign"); params.remove("sign_type"); StringBuilder content = new StringBuilder(); List<String> keys = new ArrayList<String>(params.keySet()); Collections.sort(keys); int index = 0; for (int i = 0; i < keys.size(); i++) { String key = keys.get(i); String value = params.get(key); if (StringUtils.areNotEmpty(key, value)) { content.append((index == 0 ? "" : "&") + key + "=" + value); index++; } } return content.toString(); }
2. 验签报错,是因为从钉钉里面复制的sign是url编码后的。 而实际接口是未url编码的字符串。
spiServiceCreate occur exception : com.alipay.api.AlipayApiException: RSA2验签遭遇异常,请检查公钥格式或签名是否正确。Signature length not correct: got 267 but was expecting 256
对比看了下,这个日志里面的sign是url编码过的,而在接口请求过来的sign是没有url编码。实际在验证签名的时候,不能使用编码后的字符,如果已经编码过,需要解码。
3.验签用公钥。加签名用私钥。需要注意两端的公私钥,公钥是一致的,如果不一致,则验签不通过。
4.返回结果key参数去掉。无需返回,接口文档中是有这个。 实际会影响对方验证签名。
错误格式,不需要key,影响验签:
{ "response" :{ "code" : "10000" , "msg" : "Success" , "key" : "value" }, "sign" : "xxx" }
正确格式:
{ "response" :{ "code" : "10000" , "msg" : "Success" , "name" : "lisi" }, "sign" : "TqnBnkILs86FJWRqWWZptqIpSKLIp2vnwod177h7GLyWuLhzgRHpXgXd8GoD4flyHrHBTycQdiUjWw6VqCE5rYHrJU3iYqI1e0MLlhCb"
数据格式:
response JSONObject code String msg String name String 业务字段 sign String
5.机构合约编号最好用个规范字符串,具有业务语义的字符串,而不是全部的随机数+UUID字符串
比如:日期开头20240613+业务标识+业务号+随机数
6.Gson == 转义成了 \u003d\u003d, 需要使用 fastJson,涉及到url编码的字符串尽量不使用Gson。
7.需要对比一下,json字符串里面的字段的顺序。 顺序也会影响签名。关键,比如:123,132,321是3个不同的签名字符。
JSON.toJSONString
String contentToSign = jsonObject2.toJSONString();
测试发现:上面两个转换成json字符串,实际上json字符串中的字段顺序不一致,导致签名和验签的contentToSign不一致,验签错误。
结论:支付宝验签是使用的:JSONObject方式来转的json,所以商户端也需要使用JSONObject来转json字符串。
8. response对象是JSONObject对象,而不是String json字符串。
JSONObject jsonObject = (JSONObject) JSON.toJSON(realResponse); //对象转JSONObject
//spiBaseResponseVo.setResponse(JSON.toJSONString(realResponse)); //错误
//测试代码(解决方案,使用JSONObject,同理:项目中可以使用业务VO对象,在签名获取contentToSign字符串的时候,将业务对象转换为JSONObject也可以解决。) import com.alibaba.fastjson.JSONObject; import com.alipay.api.internal.util.AlipaySignature; public class AlipaySignature5Test { //商户私钥 private static String prv_key = "商户私钥"; //商户公钥 private static String pub_key = "商户公钥"; public static void main(String[] args) throws Exception{ // http响应结果载体 JSONObject result = new JSONObject(); // 业务处理结果载体 JSONObject response = new JSONObject(); response.put("code" , "10000"); response.put("msg" , "Success"); response.put("inst_ser_contract_no" , "20240614Test123456789"); // 3、业务处理结果加签 (可选,查看 服务基础配置 章节) // contentToSign为 {"code":"10000","msg":"Success","biz":"value","person":{"age":"18","height":"180"}} String contentToSign = response.toJSONString(); System.out.println("content=" + contentToSign); String sign2 = AlipaySignature.rsaSign(contentToSign,prv_key ,"UTF-8","RSA2"); System.out.println("sign2=" + sign2); // 4、构造http响应结果 result.put("sign" , sign2); //可选,查看 服务基础配置 result.put("response" , response); // 返回json格式响应报文 String resultJson = result.toJSONString(); System.out.println("接口返回的结果=" + resultJson); //直接拿resultJson验签会返回false //boolean isPass = AlipaySignature.rsaCheck(resultJson, sign2,pub_key, "UTF-8", "RSA2"); //接收端的方式 JSONObject jsonObject = JSONObject.parseObject(resultJson); String responseJson = jsonObject.getString("response"); String signJson = jsonObject.getString("sign"); System.out.println("responseJson=" + responseJson); System.out.println("signJson=" + signJson); boolean isPass = AlipaySignature.rsaCheck(responseJson, signJson,pub_key, "UTF-8", "RSA2"); System.out.println("isPass=" + isPass); } }