同城配送,微信最新出炉的,微信官方对接好多个运力方,价格会稍微低一点,我们只需要充值就可以使用了,但是因为是新出的,文档还是在线文档,而且比较绕,看讨论组里面比较多人也遇到坑,我对接完了就细细盘点一下吧,废话不多说,开搞!
同城配送产品介绍: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());
非常好
你好,请问下,这个价格是在哪里拿到的?
是不是少了个延签的步骤??
大佬, 我一直报这个错,{"errcode":40237,"errmsg":"invalid Wechatmp-Appid match request rid: 6621f669-6845d9e3-71539882"} 到底用什么appid? 我的加密是用了开放平台的文件
非常感谢,很有帮助。已经顺利对接完毕。
使用wxJava微信工具,自定义了一个http-post Exector, 对 AES_Enc、RSA_Sign、AES_Dec稍作改造,动态配置密钥。
请问订单回调通知这块怎么处理的,我在下单时写了回调地址,然后下单成功,调用模拟订单变化接口后,微信上接收到服务通知,但程序上收不到消息
流程走通了!
下载的非对称秘钥是txt 文件 -----BEGIN RSA PRIVATE KEY----- 开头的 多了个RAS
这行 报java.security.NoSuchAlgorithmException: RSASSA-PSS Signature not available 是因为什么
java.security.NoSuchAlgorithmException: RSASSA-PSS Signature not available 说我签名不可用是为啥呀
Signature signature = Signature.getInstance("RSASSA-PSS", new BouncyCastleProvider());