API Reference

附2:签名加密

public static void main(String[] args) {
    String appSecret = "appSecretappSecret";  //APP秘密
    TreeMap<String, String> params = new TreeMap<String, String>();
    //请求内容
    params.put("templateId", "110129512080522");
    params.put("language", "en");
    params.put("to", "8618912123457");
    params.put("code", "123213");
    params.put("sender", "8618912123456");
    //掩码内容
    params.put("appKey", "KazakhKazakh");
    params.put("timestamp", "202012012001");
    params.put("nonce", "123123123123123");
    String computed = SignatureUtils.sign(appSecret, params);
    System.out.println("signature:" + computed);
}
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;

import java.text.MessageFormat;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;

/**
 * 参数进行加签验签的关键字
 */
public class SignatureUtils {

    public static final String NONCE = "nonce";

    public static final String TIMESTAMP = "timestamp";

    public static final String SIGNATURE = "signature";

    /**
     * <pre>
     * 计算签名的算法:
     * 1.参数校验:参数Map中需要包含nonce和timestamp。
     * 2.参数排序:将参数名按照字典序排序。
     * 3.参数拼接:将排序后的参数名按照参数名参数值参数名参数值的形式拼接成一个新字符串(param_string)。
     * 4.签名计算:用了commons-codec中的DigestUtils.sha256Hex(param_string)方法
     *          (1)对新字符串用app_secret拼接,app_secret放前面。app_secret+param_string得到new_text
     *          (2)new_text转成UTF-8字节数组text_bytes
     *          (3)sha256(text_bytes)得到signed_bytes
     *          (4)将singed_bytes的每个byte转成两个byte,并转成小写
     *          (5)数组转字符串
     * 举例:
     * 如掺入的originalParams是:(TreeMap是天然字典序)
     * {
     *     "app_key":"TL8sy9jWxxgA6bXr",
     *     "nonce":"a81cba32-18a8-465c-a6d1-83cad4c59445",
     *     "timestamp":"1635584380"
     * }
     * 拼接以后的结果是:
     * TL8sy9jWxxgA6bXra81cba32-18a8-465c-a6d1-83cad4c594451635584380
     * 使用DigestUtils.sha256Hex计算签名并转成小写16进制
     * DigestUtils.sha256Hex(appSecret+"TL8sy9jWxxgA6bXra81cba32-18a8-465c-a6d1-83cad4c594451635584380")
     * </pre>
     *
     * @param appSecret      app密钥
     * @param originalParams
     * @return
     */
    public static String sign(String appSecret, TreeMap<String, String> originalParams) {
        if (CollectionUtils.isEmpty(originalParams)) {
            return null;
        }
        isValidParameters(originalParams, true);
        originalParams.remove(SIGNATURE);
        StringBuilder sb = new StringBuilder();
        Iterator<Map.Entry<String, String>> it = originalParams.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String, String> entry = it.next();
            String value = entry.getValue();
            if (StringUtils.isNotBlank(value)) {
                sb.append(value);
            }
        }
        return sha256(appSecret, sb.toString());
    }

    /**
     * 验证签名,仅提供计算验证。其他业务逻辑校验在外面进行
     *
     * @return 验证结果
     * @Param appSecret 业务逻辑根据requestMap中app_key去找到对应的app_secret
     * @Param requestMap 包含signature的请求Map
     * 1.如果有IP来源验证在业务逻辑处自己处理,
     * 2.timestamp时间跨度校验在业务处做校验。推荐用将timestamp与当前时间相比在1到5分钟之内(防止客户端与网络服务器的时间不一致。但是考虑到时区问题,这一期可以不校验timestamp参数)
     * 3.防重放在外面业务逻辑处做校验。建议从requestMap中拿到nonce,用nonce做redisKey存一个1到5分钟有效期的东西(有效期时长建议与timestamp的校验逻辑一致)。防止请求被黑客截取以后重放。
     */
    public static boolean verifySignature(String appSecret, TreeMap<String, String> requestMap) {
        if (CollectionUtils.isEmpty(requestMap)) {
            return false;
        }
        boolean isValid = isValidParameters(requestMap, false);
        if (!isValid) {
            return false;
        }
        TreeMap<String, String> temp = new TreeMap<>();
        temp.putAll(requestMap);
        String signature = temp.remove(SIGNATURE);
        if (StringUtils.isBlank(signature)) {
            return false;
        }
        String computed = sign(appSecret, temp);
        return StringUtils.equals(signature, computed);
    }

    private static boolean isValidParameters(TreeMap<String, String> originalParams, boolean throwIfNotValid) {
        String message = null;
        if (!originalParams.containsKey(NONCE)) {
            message = MessageFormat.format("{0}参数不能为空", NONCE);
        }
        if (!originalParams.containsKey(TIMESTAMP)) {
            message = MessageFormat.format("{0}参数不能为空", TIMESTAMP);
        }
        if (StringUtils.isNotBlank(message)) {
            if (throwIfNotValid) {
                throw new IllegalArgumentException(message);
            }
            return false;
        }
        return true;
    }

    private static String sha256(String salt, String plainText) {
        return shaHex(salt, plainText);
    }

    private static String shaHex(String salt, String plainText) {
        if (StringUtils.isBlank(salt)) {
            throw new IllegalArgumentException("盐值不能为空");
        } else if (StringUtils.isBlank(plainText)) {
            throw new IllegalArgumentException("明文不能为空");
        } else {
            return DigestUtils.sha256Hex(salt + plainText);
        }
    }

    public static void main(String[] args) {
        String appSecret = "ha9kWIiWJS1D9n88";
        TreeMap<String, String> parameters = new TreeMap<>();
        parameters.put(NONCE, "123456");
        parameters.put(TIMESTAMP, "1718807411598");
        parameters.put("templateId", "20240620006");
        parameters.put("language", "en");
        parameters.put("to", "8618355092197");
        parameters.put("appKey", "eGGp97MQ");
        System.out.println(JSONObject.toJSONString(parameters));
        String signature = sign(appSecret, parameters);
        System.out.println(signature);
        TreeMap<String, String> newParameters = new TreeMap<>();
        newParameters.putAll(parameters);
        newParameters.put(SIGNATURE, signature);
        System.out.println(JSONObject.toJSONString(newParameters));
        boolean validate = verifySignature(appSecret, newParameters);
        System.out.println(validate);

    }


}