支付宝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);
    }
}

 

posted on 2024-06-15 15:25  oktokeep  阅读(86)  评论(2编辑  收藏  举报