- 同城配送api签名根据官方给出的java示例来布局,最后解密的时候提示auth tag验证失败?
同城配送api签名根据官方给出的java示例来布局,加密跟签名都没有问题,请求后返回是ok的,但是根据官方的java提供的示例解密就报错,报auth tag验证失败
2023-12-04 - 同城配送API安全请求响应数据解密 auth tag验证失败?
根据官方提供的示例,加密签名请求后,对响应数据解密时,提示auth tag验证失败,报Tag mismatch!异常,解密时间戳参数也是按请求时的参数传入的。
02-18 - 同城配送,创建配送单失败?
data:{"data":"VXf/9G2UYpfa0jH+zCwimhXWOq0DegfzSkzl0xxrfMZ4wwjo6179TIhyh4SV0vL6kEvZI8ivGW9kTbmRByK9IT29fRltP7hSqh1CIOMcmmMYT0bDamqpgQkDdfj/n8uQ05rsgC1bGWtDtfxvNN8jvG084HnkOagb5jZSFWmCgwDmbhpE4g4eyBEPtBkBVaKCrJaUcDuWaAw2zJg7SSwHe7v7ry5c7AjsvddC9fvOTnqcN2g06ANha4kpYWk+WTM7h3eB+3co2zQ4FuHC4w7EzYgb05dvcsHZKDqrFmgDUcG/1Lx8oaYpKdEFCoWsj+0+Ow4w7Gtnb7HpYpQX1NUlfhBrfh6fNVGc/uYFoACPd2+vbgME+EXggGUrvcBjcf+rJ6Uiy6X0qx5LKD0JnODyxKf8dOi1vbEJwcxY9LcAG6UG6yzN26+szsvN7f8zuDLbifEQj2tUcmEdvLrKay8+Ige9wbk21rQ81I5YjNAxN6H7O4SvMZJCDDgPbNvlcKt2YUugg+znKHGJGKka9v6qoQOQ0MIHMDPxNeMFVjojiixeRe1w7uJITmA08SJRHO0v00ugOBfSERdZTpRXMLT/oZOl7r9LajHXXGFiv3nBkqX7EgZ6ceD4XOfIeqMMJHy0Xc/VFVbvkLbwwHoPzYOgdiT7zDq9XuI4e1BnRC7w+TvH2HIDTA8gDhC+JFPN3ZnETPLTfj907zp/DEaSJ/xt+nLwRlkAQjJXAwfvEQN8SsWQoKDuDyEUbc8elXQOI6bKY0+nLQAARihX3SMlGsuCvwqFV6E5qOlT8BwOt5WXnc+2VHL294LaSgk37ajqly8ABkjnr1QArZhBVjRvmqBHhVge192yKTq2Ad0Nt49rNJW3rYYqU7363fDmKf2SM9YLFqoOdIyFubzz4t4ga6tDB57UriQ5a4qLKcU3phkMkSm85QiaAXx/MWypRUMfaqC61T/zo7XJlMf64qXFBNAhWpJYWoaH1HfNJBgP30B05WEydWAQMQ5AdfBz2Z6vG9PQzwtksLadvXV5dg==","authtag":"BatQkjf2IjD9QX49WZoOoQ==","iv":"I2obQrEUsxo20gWV"}head:[Content-Type:"application/json;charset=utf-8", Wechatmp-TimeStamp:"1695110978", Wechatmp-Appid:"wx************bc", Accept:"application/json", Wechatmp-Signature:"QhMVHBMJhRSQ3GGmIWMrRjPuWOtSUxD5MH9jG63nInIV4yOuaJ7U3leoklYjB1mpZRtaBYOq1ZSD1I1QkuYcvtJCQTF+9M6IkifZd8DLXbr02vgBIY35IvG2Q6ccAJN6MVlwYVi3yrDNPV2pP9cnXgrBlqZBIsTfnmxvWHcTpCSSpQUHA1974Uoo2+/hwGho2d31jU1IZ3Im16JPJ+SYzW9vvZ+zguLByJtecJya7JdH8yUGvcG4+I4oTSK3ZZjRGS9YLMJvbQ9KvUogQtnjLDhSauBNZcVKXADSlm6iZxWnEoamJf3xuwPD9NvFomvW4e+zdRYpD/OXE+6m0vsTTA=="] return: {"errcode":40237,"errmsg":"invalid Wechatmp-Appid match request rid: 65095743-1a78f03b-5d732fd7"} 使用的最新的token,调用其他小程序接口都是可以通的
2023-09-19 - 同城配送排坑贴,打工人永不加班!
同城配送,微信最新出炉的,微信官方对接好多个运力方,价格会稍微低一点,我们只需要充值就可以使用了,但是因为是新出的,文档还是在线文档,而且比较绕,看讨论组里面比较多人也遇到坑,我对接完了就细细盘点一下吧,废话不多说,开搞! 同城配送产品介绍:https://docs.qq.com/doc/DVWRCSGllWWZBbG9t 同城配送API文档:https://doc.weixin.qq.com/doc/w3_ADkAtAZ1ACchtE1J1bXRzGqhpUnYX?scode=AJEAIQdfAAoySHzTsMADkAtAZ1ACc 官方加解密教程Demo:https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/getting_started/api_signature.html 我用的开发语言是JAVA!!其他语言的兄弟姐妹们可以借鉴一下找找思路,本帖我是调用同城配送的查询店铺接口 [图片] 第1⃣️坑:错误代码:934011, 错误信息:request_body is required 这个是因为接口需要进行加密传输,如果请求头Header不带这几个参数的话就会报这个错,appid和timestamp参数的话还好说,signature这个需要后续通过不同参数拼接进行base64才可以计算出来(后面会贴代码) [图片] 第2⃣️坑:签名失败{"errcode": 40234,"errmsg": "invalid signature rid: 64b0c114-748bb871-181713e7"} 这个也是比较头疼的,也是比较多人遇到的错误,原因是加密出来的签名有问题。 加密需要拿到 对称密钥和 非对称密钥 登录小程序后台-开发管理-》开发设置-》API安全 贴图:非对称的密文只有在初始化的时候可以下载(已经开启了的可以按修改,保存前下载,确保下载回来的是----BEGIN PRIVATE KEY----开头的,因为官方前两天有bug,点击下载回来的还是非对称明文,不过腾讯已经说让相关人员去优化了) [图片] [图片] 我暂且给这几个参数取名ABCD,方便后续看代码! 直接贴数据加密的代码:代码是官方给出的加密demo,这里比较简单,直接看代码,按照上图的ABCD对好入座 import javax.crypto.Cipher; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.Arrays; import java.util.Base64; import com.google.gson.Gson; import com.google.gson.JsonObject; public class AES_Enc { private static JsonObject getReqForSign(JsonObject ctx, JsonObject req) { Gson gson = new Gson(); // 开发者本地信息 String local_appid = ctx.get("local_appid").getAsString(); String url_path = ctx.get("url_path").getAsString(); String local_sym_sn = ctx.get("local_sym_sn").getAsString(); String local_sym_key = ctx.get("local_sym_key").getAsString(); //加密签名使用的统一时间戳 long localTs = System.currentTimeMillis() / 1000; String nonce = generateNonce(); req.addProperty("_n", nonce); req.addProperty("_appid", local_appid); req.addProperty("_timestamp", localTs); String plaintext = gson.toJson(req); String aad = url_path + "|" + local_appid + "|" + localTs + "|" + local_sym_sn; byte[] realKey = Base64.getDecoder().decode(local_sym_key); byte[] realIv = generateRandomBytes(12); byte[] realAad = aad.getBytes(StandardCharsets.UTF_8); byte[] realPlaintext = plaintext.getBytes(StandardCharsets.UTF_8); try { Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); SecretKeySpec keySpec = new SecretKeySpec(realKey, "AES"); GCMParameterSpec parameterSpec = new GCMParameterSpec(128, realIv); cipher.init(Cipher.ENCRYPT_MODE, keySpec, parameterSpec); cipher.updateAAD(realAad); byte[] ciphertext = cipher.doFinal(realPlaintext); byte[] encryptedData = Arrays.copyOfRange(ciphertext, 0, ciphertext.length - 16); byte[] authTag = Arrays.copyOfRange(ciphertext, ciphertext.length - 16, ciphertext.length); String iv = base64Encode(realIv); String data = base64Encode(encryptedData); String authtag = base64Encode(authTag); JsonObject reqData = new JsonObject(); reqData.addProperty("iv", iv); reqData.addProperty("data", data); reqData.addProperty("authtag", authtag); JsonObject reqforsign = new JsonObject(); reqforsign.addProperty("req_ts", localTs); reqforsign.addProperty("req_data", reqData.toString()); return reqforsign; } catch (Exception e) { e.printStackTrace(); } return null; } private static String generateNonce() { byte[] nonce = generateRandomBytes(16); return base64Encode(nonce).replace("=", ""); } private static byte[] generateRandomBytes(int length) { byte[] bytes = new byte[length]; new SecureRandom().nextBytes(bytes); return bytes; } private static String base64Encode(byte[] data) { return Base64.getEncoder().encodeToString(data); } private static JsonObject getCtx() { JsonObject ctx = new JsonObject(); // 仅做演示,敏感信息请勿硬编码 ctx.addProperty("local_sym_key", "YcMLjYryM9L3I*************+W3dUZDm6Fj8="); ctx.addProperty("local_sym_sn", "6bda832d********ba2c5c072df"); ctx.addProperty("local_appid", "wx55e9*******d09"); ctx.addProperty("url_path", "https://api.weixin.qq.com/cgi-bin/express/intracity/querystore"); return ctx; } private static JsonObject getRawReq() { JsonObject req = new JsonObject(); req.addProperty("wx_store_id", "40000000************"); return req; } public static void main(String[] args) { JsonObject req = getRawReq(); JsonObject ctx = getCtx(); JsonObject reqforsign = getReqForSign(ctx, req); if (reqforsign != null) { System.out.println(reqforsign.get("req_ts").getAsLong()); System.out.println(reqforsign.get("req_data").getAsString()); } } public static JsonObject getData() { JsonObject req = getRawReq(); JsonObject ctx = getCtx(); JsonObject reqforsign = getReqForSign(ctx, req); return reqforsign; } } [图片] [图片] 代码参数说明如图,此时执行main方法打印如下: [图片] OK!到此请求参数的加密就有了,加下来就到请求头的签名了。 第3⃣️坑:非对称加密的私钥 我们直接下载回来的PRIVATE KEY是不能够直接使用的,直接使用签名的时候会报错:java.lang.IllegalArgumentException: Illegal base64 character 2d 官方也有说明:需要转换一下 [图片] 具体操作: 将下载回来的私钥改一下名:我们改成:private_key.pem [图片] 在私钥的当前路径打开终端 先执行: openssl rsa -in private_key.pem -outform der -out private_key.der 再执行: openssl pkcs8 -topk8 -inform der -in private_key.der -outform pem -out private_key_pkcs8.pem -nocrypt OK!现在我们得到了这三个 [图片] 其中private_key_pkcs8.pem里面的内容我们就可以直接替换官方的demo代码了!! 废话不多说,直接上代码 // RSAwithSHA256 import com.google.gson.JsonObject; import java.nio.charset.StandardCharsets; import java.security.*; import java.security.interfaces.RSAPrivateKey; import java.security.spec.MGF1ParameterSpec; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.PSSParameterSpec; import java.util.Base64; public class RSA_Sign { public static String getSignature(JsonObject ctx, JsonObject req) { String signatureBase64 = null; // 开发者本地信息 String local_appid = ctx.get("local_appid").getAsString(); String url_path = ctx.get("url_path").getAsString(); String local_sym_sn = ctx.get("local_sym_sn").getAsString(); String local_private_key = ctx.get("local_private_key").getAsString(); // 待请求API数据 long reqTs = req.get("req_ts").getAsLong(); String reqData = req.get("req_data").getAsString(); String payload = url_path + "\n" + local_appid + "\n" + reqTs + "\n" + reqData; byte[] dataBuffer = payload.getBytes(StandardCharsets.UTF_8); try { local_private_key = local_private_key.replace("-----BEGIN PRIVATE KEY-----", ""); local_private_key = local_private_key.replace("-----END PRIVATE KEY-----", ""); local_private_key = local_private_key.replaceAll("\\s+", ""); byte[] decoded = Base64.getDecoder().decode(local_private_key); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decoded); RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(keySpec); Signature signature = Signature.getInstance("RSASSA-PSS"); // salt长度,需与SHA256结果长度(32)一致 PSSParameterSpec pssParameterSpec = new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1); signature.setParameter(pssParameterSpec); signature.initSign(priKey); signature.update(dataBuffer); byte[] sigBuffer = signature.sign(); signatureBase64 = Base64.getEncoder().encodeToString(sigBuffer); } catch (Exception e) { e.printStackTrace(); } return signatureBase64; /* 最终请求头字段 { "Wechatmp-Appid": local_appid, "Wechatmp-TimeStamp": req_ts, "Wechatmp-Signature": sig, } */ } private static JsonObject getReq() { JsonObject req = new JsonObject(); req.addProperty("req_ts", 1689259852); req.addProperty("req_data", "{\"iv\":\"EehrptDmV/5gxjUT\",\"data\":\"zHlBtadb+dfEHwk4x1c4GZdz1MDSDV/3CAy4qYuMdkkxPOXDiQfJZCxnjBjivKNwPAZfKZZh3nv/xQyM8ZYUL9VUHP4M2kAcOMbvKLKnB7j4Lxwioiir4R+IHV7mwWNcL9SrztZ1FP9Nzg7kxc6xD2RrJkzPQ3W46w==\",\"authtag\":\"8Md6QGPY9HpPcLv86dkzyQ==\"}"); return req; } private static JsonObject getCtx() { JsonObject ctx = new JsonObject(); // 仅做演示,敏感信息请勿硬编码 String localPrivateKey = "-----BEGIN PRIVATE KEY-----\n" + "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC971V5m8Ou/MO3\n" + "CEP9MP3tUXJu2fE25YbIFPysRx6SD7hXxCZjNRjQ6q7DENCpt2541v5IdJe3kd8K\n" + "y5oLVDyzcTx6hWnyCInbS3lZJ27aTTS7LesOf8120uU3ma0CbICRhSH/oLWx9DNj\n" + "hpQbQQjracja42VGEqHm/3YlTy6CX0ygfOQ36iuNTvOFErdCffWild9YOZe757l+\n" + "ivTg0hoYq+/WzTCf3F0dsqn7og0eX6AXILBLnqs29cwtc/ov9BdDUxhCuCxghpTn\n" + "Pdd/l7zYOZYfZZM80q2wZWqkbh+75eQ2YAwjw7pBz905DdnGCdoNj7cORE78lQnF\n" + "YuT8VqV2YErQDR6gPZ2KEEtA0paHxBIeuluWgQqfdMGQqsNjRC2l2+Mx2Ly6CjzR\n" + "w70X3Db5x3QCtVRYz8J5303lOcrjFG2uB4w6dRWrCRbnwdlxFbIMdMTG4QKBgQCP\n" + "8RDygSRXrQYpEgWk2GqaxuQSoM0uAw5qhYmV75MeV+ZnBp0ITMvYjfrN9j43U/rC\n" + "l+gjz/KhMrMrxfAcx60TkOhD3W/6xsdNf2wXczP4Gvev5iBmEGLu5IGs4rkh8/AA\n" + "r5VNg659CCIkJMivYpr0GHU+1/ql06ZXpae5UZxgyQKBgQDcQq5dJVsEe0ky5G2O\n" + "GYZ/EPzOBlD+KRJyPVNzwKZws7UlhnNApwwp76/9UR+2PPhl/g5om6oBGygIAYhw\n" + "mx9wZkb1T3+qT2xJ7QMPRKZXfkthmay/7yigGTVRU1DhqmYfY35G+bsg3EsdeyHS\n" + "J5VUJ83ecM67+q+1FBekTZa+iA==\n" + "-----END PRIVATE KEY-----"; ctx.addProperty("local_private_key", localPrivateKey); ctx.addProperty("local_sym_sn", "e84cdda4*********0cee78f277"); ctx.addProperty("local_appid", "wx55******94d09"); ctx.addProperty("url_path", "https://api.weixin.qq.com/cgi-bin/express/intracity/querystore"); return ctx; } public static String getSign(JsonObject data) { JsonObject req = data; JsonObject ctx = getCtx(); String sign = getSignature(ctx, req); return sign; } public static void main(String[] args) { JsonObject req = getReq(); JsonObject ctx = getCtx(); String res = getSignature(ctx, req); System.out.println(res); } } 代码说明: [图片] [图片] 执行main方法打印出来的就是加密签名了,放到请求头里面,如果这个main方法执行错误了,就是你的私钥有问题了 [图片] 最后我们发起请求的参数都全了,去调用一下!! [图片] [图片] 上面是请求返回的响应,也是通过加密的内容,并且请求反应的请求头里面的内容我们也是需要用到的,解密获取返回内容的时候需要用到 请求返回结果解密,直接上代码 // AES256_GCM import javax.crypto.Cipher; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.util.Base64; import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; public class AES_Dec { public static Object getRealResp(JsonObject ctx, JsonObject resp) { byte[] decryptedBytes = null; // 开发者本地信息 String local_appid = ctx.get("local_appid").getAsString(); String url_path = ctx.get("url_path").getAsString(); String local_sym_sn = ctx.get("local_sym_sn").getAsString(); String local_sym_key = ctx.get("local_sym_key").getAsString(); // API响应数据,解密只需要响应头时间戳与响应数据 long respTs = resp.get("resp_ts").getAsLong(); String respData = resp.get("resp_data").getAsString(); JsonParser parser = new JsonParser(); JsonElement resp_data = parser.parse(respData); String iv = resp_data.getAsJsonObject().get("iv").getAsString(); String data = resp_data.getAsJsonObject().get("data").getAsString(); String authtag = resp_data.getAsJsonObject().get("authtag").getAsString(); // 构建AAD String aad = url_path + "|" + local_appid + "|" + respTs + "|" + local_sym_sn; // 拼接cipher和authtag byte[] dataBytes = Base64.getDecoder().decode(data); byte[] authtagBytes = Base64.getDecoder().decode(authtag); byte[] new_dataBytes = new byte[dataBytes.length + authtagBytes.length]; System.arraycopy(dataBytes, 0, new_dataBytes, 0, dataBytes.length); System.arraycopy(authtagBytes, 0, new_dataBytes, dataBytes.length, authtagBytes.length); byte[] aadBytes = aad.getBytes(StandardCharsets.UTF_8); byte[] ivBytes = Base64.getDecoder().decode(iv); Object realResp = null; try { byte[] keyBytes = Base64.getDecoder().decode(local_sym_key); SecretKeySpec secretKey = new SecretKeySpec(keyBytes, "AES"); Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, ivBytes); cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec); cipher.updateAAD(aadBytes); try { decryptedBytes = cipher.doFinal(new_dataBytes); } catch (Exception e) { System.out.println("auth tag验证失败"); return null; } // 解密结果 String decryptedData = new String(decryptedBytes, StandardCharsets.UTF_8); JsonElement element = parser.parse(decryptedData); Gson gson = new Gson(); realResp = gson.fromJson(element, Object.class); long localTs = System.currentTimeMillis() / 1000; // 安全检查,根据业务实际需求判断 if (element.getAsJsonObject().get("_appid").getAsString() == local_appid // appid不匹配 || element.getAsJsonObject().get("_timestamp").getAsLong() != respTs // timestamp与Wechatmp-TimeStamp不匹配 || localTs - element.getAsJsonObject().get("_timestamp").getAsLong() > 300 // 响应数据的时候与当前时间超过5分钟 ) { System.out.println("安全字段校验失败"); return null; } } catch (Exception e) { e.printStackTrace(); } return realResp; } private static JsonObject getCtx() { JsonObject ctx = new JsonObject(); // 仅做演示,敏感信息请勿硬编码 ctx.addProperty("local_sym_key", "YcMLjYryM9L3IFg*********3dUZDm6Fj8="); ctx.addProperty("local_sym_sn", "6bda832d57b*******2c5c072df"); ctx.addProperty("local_appid", "wx55*********d09"); ctx.addProperty("url_path", "https://api.weixin.qq.com/cgi-bin/express/intracity/querystore"); return ctx; } private static JsonObject getResp() { JsonObject resp = new JsonObject(); String respData = "{\"iv\":\"QrZZqNdqxToexKwy\",\"data\":\"CfoqyhQ1oYg2zJ7tvqO4t5\\/KA2zUD33y2D3FIbFShkclYaONiLqROmG8CfHm0EXMADNcvp4EQuxKxrGZbVE97qrymbnpreTT\\/XQtly62A84KrGCLIqi8SF\\/jM75QFH6qEuwy32gSsl7sv2GxDSCdb4JkpFPmskjsZgP3z9vBFUPklTiJtLRDfy\\/MPg9sYT3eVFuD636NO01\\/NkYPAa9WUIIlbDlqk\\/O0xMgBdi84JwqtxW+VuRqSkge30EluRBoiH4j3ngCuK5JGqfPw1MFXL0e55V72iBsSa5fqHDnskl62pKUcR+\\/h4Znw7H5f3U5WnUtsKAzH3hg22+VIgUp5veLOw1PI94c9Ks5A26+4OWIh9wmgUuInZo20g1j50G8ENfNPVcDApSG0P\\/yKJZFAsQrvx2c+pU6sVllo+XTp4eoz2wCNrEedEZ\\/ism02GWmwTtgWALFIedib44fOyNuIfNWwXMg\\/ywzBN37Ercip7vI+Iyd+wPTCjB046wmZYSzVelLY5qc97X7jo6m8+np3VS+NHr09tNGmjG5L\\/FMHO99WuURJqSNdXB3swiOlVbtRCYwA4epzhhnv2b6TzYN9UOLw\",\"authtag\":\"n5OB+4ZdaQU\\/ZE6wE3GO2w==\"}"; resp.addProperty("resp_ts", 1689266097); resp.addProperty("resp_data", respData); return resp; } public static void main(String[] args) { JsonObject resp = getResp(); JsonObject ctx = getCtx(); Object res = getRealResp(ctx, resp); System.out.println(res); } } [图片] 替换一下这里的加密内容和响应时间戳执行即可! 我们执行一下main方法打印出来内容: [图片] OK!大功告成,从请求的加密到结果的解密都完成了!! 具体封装的话大家可以自己去弄,最后贴一下发起请求的方法吧, final String accessToken = this.getWxMaService(request.getWxAppId()).getAccessToken(); log.info("accessToken is =>{}", this.getWxMaService(request.getWxAppId()).getAccessToken()); final JsonObject data = AES_Enc.getData(); final Long reqTs = data.get("req_ts").getAsLong(); final String reqData = data.get("req_data").getAsString(); final String sign = RSA_Sign.getSign(data); log.info("data is ={}", data); final HttpResponse response = HttpRequest.post("https://api.weixin.qq.com/cgi-bin/express/intracity/querystore?access_token=" + accessToken) .header("Wechatmp-Appid", "wx5********4d09") .header("Wechatmp-TimeStamp", reqTs.toString()) .header("Wechatmp-Signature", sign) .header("Content-Type", "application/json;charset=utf-8") .body(reqData) .execute(); final String respSign = response.header("Wechatmp-Signature"); final String respAppId = response.header("Wechatmp-Appid"); final String respTs = response.header("Wechatmp-TimeStamp"); final String respSerial = response.header("Wechatmp-Serial"); log.info("respSign is {}", respSign); log.info("respAppId is {}", respAppId); log.info("respTs is {}", respTs); log.info("respSerial is {}", respSerial); log.info("请求返回结果是:{}", response.body());
2023-07-14