微信小程序【同城配送】及【加密请求】
在小程序后台配置API安全时注意保存密钥
,要不然还得重新弄。
- 封装属性配置类,在加解密的时候会用到
- 封装加解密方法
- 使用
okhttp
封装post加密请求,并将信息解密 - 调用post方法将必要信息加密后发送给微信并得到相应,对其解密
- 对信息进行业务处理
封装属性配置类
创建一个config.properties
,并将其添加到.gitignore
中,避免泄露重要信息。
@Configuration
@PropertySource("classpath:config.properties") //读取配置文件
@ConfigurationProperties(prefix="api")
public class ApiSecurityConfig {
static String sym_key;
static String sym_sn;
static String asym_sn;
static String appid;
// getters and setters
}
加密
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(String url) {
JsonObject ctx = new JsonObject();
// 仅做演示,敏感信息请勿硬编码
ctx.addProperty("local_sym_key", ApiSecurityConfig.getSym_key());
ctx.addProperty("local_sym_sn", ApiSecurityConfig.getSym_sn());
ctx.addProperty("local_appid", ApiSecurityConfig.getAppid());
ctx.addProperty("url_path", url);
return ctx;
}
private static JsonObject getRawReq(Object hashMap) {
Gson gson = new Gson();
String json = gson.toJson(hashMap);
JsonParser parser = new JsonParser();
return parser.parse(json).getAsJsonObject();
}
public static JsonObject getData(Object param, String url) {
JsonObject req = getRawReq(param);
JsonObject ctx = getCtx(url);
return getReqForSign(ctx, req);
}
}
解密
public class AES_Dec {
public static HashMap<String, 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);
HashMap<String, 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, HashMap.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(String url) {
JsonObject ctx = new JsonObject();
ctx.addProperty("local_sym_key", ApiSecurityConfig.getSym_key());
ctx.addProperty("local_sym_sn", ApiSecurityConfig.getSym_sn());
ctx.addProperty("local_appid", ApiSecurityConfig.getAppid());
ctx.addProperty("url_path", url);
return ctx;
}
private static JsonObject getResp(String respData, Long resp_ts) {
JsonObject resp = new JsonObject();
// String respData = "{"iv":"xx","data":"yyy","authtag":"zzz"}";
resp.addProperty("resp_ts", resp_ts);
resp.addProperty("resp_data", respData);
return resp;
}
public static HashMap<String, Object> getRealRespResult(String data, Long ts, String url) {
JsonObject resp = getResp(data, ts);
JsonObject ctx = getCtx(url);
return getRealResp(ctx, resp);
}
}
封装请求
这里使用了OkHttpClient
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.11.0</version>
</dependency>
文档所有接口请求方式均为HTTPS-POST
public class RequestUtil {
private static final OkHttpClient client = new OkHttpClient();
private static final Logger log = LoggerFactory.getLogger(RequestUtil.class);
public static String mapToQueryString(Map<String, String> map) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : map.entrySet()) {
if (sb.length() > 0) {
sb.append("&");
}
sb.append(entry.getKey()).append("=").append(entry.getValue());
}
return sb.toString();
}
public static HashMap<String, Object> doSecurityPost(String url, Object params, HashMap<String, Object> headers) {
try (Response response = post(url, params, headers)) {
ResponseBody body = response.body();
String respSign = response.header("Wechatmp-Signature");
String respAppId = response.header("Wechatmp-Appid");
String respTs = response.header("Wechatmp-TimeStamp");
String respSerial = response.header("Wechatmp-Serial");
if (body == null || respTs == null) {
throw HttpException.badRequest("微信加密请求失败");
}
String responseBodyString = null;
try {
responseBodyString = body.string();
} catch (IOException e) {
System.out.println(e.getMessage());
log.error("获得response body string失败,原因:{}", e.getMessage());
throw new RuntimeException(e);
}
// 解密数据 {errcode=934016.0, errmsg=Order not exist rid: 65192df5-77a7d1b3-117973ad, _n=270eaa5fe4b9e68213ecbd37f417e10e, _appid=wx1e933945b62aebf8, _timestamp=1.696148981E9}
return AES_Dec.getRealRespResult(responseBodyString, Long.decode(respTs), url.split("\\?")[0]);
}
}
public static Response post(String url, Object params, HashMap<String, Object> headers) {
RequestBody requestBody;
String json = "";
if (params instanceof String) {
json = params.toString();
} else if (params != null) {
ObjectMapper objectMapper = new ObjectMapper();
try {
json = objectMapper.writeValueAsString(params);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
requestBody = RequestBody.create(ByteString.encodeUtf8(json), MediaType.parse("application/json; charset=utf-8"));
Request.Builder requestBuilder = new Request.Builder().url(url)
.post(requestBody);
if (headers != null) {
for (Map.Entry<String, Object> entry : headers.entrySet()) {
requestBuilder.addHeader(entry.getKey(), entry.getValue().toString());
}
}
Request request = requestBuilder.build();
try {
return client.newCall(request).execute();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
调用加密方法请求配送价格
这里以preaddorder
为例。
// 不同的接口参数不同,ExpressInfo为查询运费价格所需的参数封装类
public HashMap<String, Object> getExpressFeeResponse(ExpressInfo expressInfo, String path) {
if (Objects.nonNull(expressInfo) && expressInfo.isValid()) {
String url = getConcatUrl(path);
JsonObject data = AES_Enc.getData(expressInfo, path);
String reqData = data.get("req_data").getAsString();
return RequestUtil.doSecurityPost(url, reqData, getWeChatHeader(data, path));
}
throw HttpException.badRequest("获取配送信息参数不正确");
}
public HashMap<String, Object> getWeChatHeader(JsonObject data, String url) {
HashMap<String, Object> headers = new HashMap<>();
headers.put("Wechatmp-Appid", ApiSecurityConfig.getAppid());
String signature = RSA_Sign.getSign(data, url);
headers.put("Wechatmp-Signature", signature);
long localTs = data.get("req_ts").getAsLong();
headers.put("Wechatmp-TimeStamp", Long.toString(localTs));
return headers;
}
getExpressFeeResponse(expressInfo, ExpressInterface.CALCULATE_PRICE.getPath());
public enum ExpressInterface {
CALCULATE_PRICE("preaddorder"),
CREATE_ORDER("addorder"),
CANCEL_ORDER("cancelorder"),
QUERY_ORDER("queryorder");
private final String path;
ExpressInterface(String path) {
this.path = path;
}
public String getPath() {
String BASE_URL = "https://api.weixin.qq.com/cgi-bin/express/intracity/";
return BASE_URL + path;
}
}
到这里应该就没啥问题了。
这是官方的文档,可以进行参考。
同城配送
服务端api签名指南