明天的太阳

导航

微信小程序【同城配送】及【加密请求】

在小程序后台配置API安全时注意保存密钥,要不然还得重新弄。

  1. 封装属性配置类,在加解密的时候会用到
  2. 封装加解密方法
  3. 使用okhttp封装post加密请求,并将信息解密
  4. 调用post方法将必要信息加密后发送给微信并得到相应,对其解密
  5. 对信息进行业务处理

封装属性配置类

创建一个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签名指南

posted on 2023-11-07 18:37  东方来客  阅读(191)  评论(0编辑  收藏  举报