注意事项:
如果所有参数都是按照微信要求,且可以通过微信提供的签名验证工具,但仍然报错:签名错误
可能原因:使用 restTemplate(springboot 封装的用于发送请求的对象)如果不设置请求头,编码格式默认为ISO8859-1,会导致签名算法验证通过,但是微信仍然会返回签名错误的提示
开发步骤
1、自己是先把必要的参数要的参数放到了Map中,下面是简化的代码,至于具体怎么封装返回值与参数的,大家根据自己需要
2、通过参数生成签名:将参数(key=val格式)按照字典循序排序--->在末尾加上keys键值对--->进行加密算法
3、将参数形成的xml,并调用微信统一下单接口,获得返回值
Map<String,Object> data = new HashMap<>();
data.put("body", "产品支付");
data.put("attach", "产品支付");
data.put("total_fee", 10);
Date nowTime = new Date();
SimpleDateFormat sdf= new SimpleDateFormat("yyyyMMddHHmmss");
data.put("out_trade_no", sdf.format(nowTime));
data.put("time_start", sdf.format(nowTime));
data.put("time_expire", sdf.format(nowTime.getTime() + 600000));
data.put("goods_tag", "产品支付");
data.put("trade_type", "JSAPI");
data.put("openid", jsApiPay.getOpenid());//
data.put("notify_url", wxPayConfig.GetConfig().GetNotifyUrl());//异步通知url
data.put("appid", wxPayConfig.GetConfig().GetAppID());//公众账号ID
data.put("mch_id", wxPayConfig.GetConfig().GetMchID());//商户号
data.put("spbill_create_ip", wxPayConfig.GetConfig().GetIp());//终端ip
data.put("nonce_str", GenerateNonceStr(32));//随机字符串 方法见下文
data.put("sign_type", SIGN_TYPE_HMAC_SHA256);//签名类型
//SIGN_TYPE_HMAC_SHA256 全局变量 = "HMAC-SHA256"
data.put("sign", makeSign(SIGN_TYPE_HMAC_SHA256,data)); //生成签名 方法见下文
//将参数map转化成xml格式,并调用微信统一下单接口(要求参数为xml字符串)
String xml = toXml(data); //方法见下文
//进行请求
URI uri = null;
try {
uri = new URI("https://api.mch.weixin.qq.com/pay/unifiedorder") ;
} catch (URISyntaxException e) {
e.printStackTrace();
}
//使用 restTemplate(springboot 封装的用于发送请求的对象)如果不设置请求头,编码格式默认为ISO8859-1,会导致签名算法验证通过,但是微信仍然会返回签名错误的提示
HttpHeaders headers = new HttpHeaders();
MediaType type = MediaType.parseMediaType("application/xml; charset=UTF-8");
headers.setContentType(type);
HttpEntity<String> requestEntity = new HttpEntity<>(xml, headers);
ResponseEntity<String> responseEntity = restTemplate.postForEntity(uri,requestEntity,String.class);
String s1 = "";
try {
s1 = new String(responseEntity.getBody().getBytes("ISO8859-1"), "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
//见返回结果的字符串封装成自定义对对象,
Map<String, String> map = xmlToMap(s1);//将xml转化成map对象,方法见下文
JSONObject jsonObject = null;
String jsonStr ="";
if(map !=null){
jsonObject = JSONObject.fromObject(map);
if(jsonObject!=null){
jsonStr = jsonObject.toString();
}
}
//将返回结果转成对象 objectMapper(springboot 带有的对象,可将json字符串反射到自定义对象中,)
try {
//wxPayBaseResult 是根据微信接口返回的结果封装的自定义对象
wxPayBaseResult = objectMapper.readValue(jsonStr, WxPayBaseResult.class);
} catch (IOException e) {
e.printStackTrace();
}
生成随机字符串nonce_str的方法:
/**
* 参数 len 需要生成字符串的长度
* 生成随机串,随机串包含字母或数字
* @return 随机串
*/
public String GenerateNonceStr(int len)
{
String val = "";
Random random = new Random(); // 随机生成器
for (int i = 0; i < len; i++) {
// 在[0,2)值域随机生成一个数除2,得到以下要判断的格式
String str = random.nextInt(2) % 2 == 0 ? "num" : "char";
if ("char".equalsIgnoreCase(str)) {
// 产生字母(大小写判断)
int nextInt = random.nextInt(2) % 2 == 0 ? 65 : 97;
// 字符串拼接
val += (char) (nextInt + random.nextInt(26));
} else if ("num".equalsIgnoreCase(str)) { // 产生随机数字并转成字符串
val += String.valueOf(random.nextInt(10));
}
}
return val;
}
生成签名
/**
* signType 生成签名方式 inputObj:必须参数集合
* @生成签名,详见签名生成算法
* @return 签名, sign字段不参加签名 SHA256
*/
public String makeSign(String signType,Map<String,Object> inputObj)
{
StringBuffer sb = new StringBuffer();
StringBuffer sbkey = new StringBuffer();
//将集合M内非空参数值的参数按照参数名ASCII码从小到大排序(字典序)
Map<String, Object> sortedMap = new TreeMap<String, Object>(parameters);
Set es = sortedMap.entrySet();
//转url格式
Iterator it = es.iterator();
while(it.hasNext()) {
Map.Entry entry = (Map.Entry)it.next();
String k = (String)entry.getKey();
Object v = entry.getValue();
//空值不传递,不参与签名组串
if(null != v && !"".equals(v)) {
sb.append(k + "=" + v + "&");
sbkey.append(k + "=" + v + "&");
}
}
//在string后加入API KEY,获取自己的支付key
sbkey.append("key="+this.wxPayConfig.GetConfig().GetKey());
System.out.println("#2.连接商户key:"+sbkey.toString());
//str += "&key=" + WxPayConfig.GetConfig().GetKey();
if (signType == SIGN_TYPE_MD5)
{
//MD5加密,结果转换为大写字符 MD5Util工具 在方法在下文
String sign = MD5Util.crypt(sbkey.toString()).toUpperCase();
return sign;
}
else if(signType==SIGN_TYPE_HMAC_SHA256)
{ //自己写的加密方法在下文
return CalcHMACSHA256Hash(sbkey.toString(), this.wxPayConfig.GetConfig().GetKey());
}else{
return "";//sign_type 不合法
}
}
自己写的HMAC-SHA256加密方法
//SIGN_TYPE_HMAC_SHA256 加密生成签名字符串
//参数plaintext 将参数转化为url格式后的字符串
//参数 salt 自己的商户key
private String CalcHMACSHA256Hash(String plaintext, String salt)
{
String result = "";
byte[] baText2BeHashed =null;
byte[] baSalt =null;
try {
baText2BeHashed = plaintext.getBytes("UTF-8");
baSalt = salt.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
Mac sha256_HMAC = null;
try {
sha256_HMAC = Mac.getInstance("HmacSHA256");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
SecretKeySpec secret_key = new SecretKeySpec(baSalt, "HmacSHA256");
try {
sha256_HMAC.init(secret_key);
} catch (InvalidKeyException e) {
e.printStackTrace();
}
//对消息进行UTF-8转化 为了防止中文加密与微信的算法不匹配
byte[] bytes = sha256_HMAC.doFinal(baText2BeHashed);
result = Hex.encodeHexString(bytes, true).toUpperCase();
System.out.println("3.生成sign并转成大写:"+result);
return result;
}
MD5加密工具类:
public class MD5Util {
/**
* Encodes a string 2 MD5
*
* @param str String to encode
* @return Encoded String
* @throws NoSuchAlgorithmException
*/
public static String crypt(String str) {
if (str == null || str.length() == 0) {
throw new IllegalArgumentException("String to encript cannot be null or zero length");
}
StringBuffer hexString = new StringBuffer();
try {
MessageDigest md = MessageDigest.getInstance("MD5");
try {
md.update(str.getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
byte[] hash = md.digest();
for (int i = 0; i < hash.length; i++) {
if ((0xff & hash[i]) < 0x10) {
hexString.append("0" + Integer.toHexString((0xFF & hash[i])));
} else {
hexString.append(Integer.toHexString(0xFF & hash[i]));
}
}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return hexString.toString();
}
/**
* 对字符串md5加密
*
* @param str
* @return
* @throws Exception
*/
public static String getMD5Str(String str){
try {
// 生成一个MD5加密计算摘要
MessageDigest md = MessageDigest.getInstance("MD5");
// 计算md5函数
md.update(str.getBytes());
// digest()最后确定返回md5 hash值,返回值为8为字符串。因为md5 hash值是16位的hex值,实际上就是8位的字符
// BigInteger函数则将8位的字符串转换成16位hex值,用字符串来表示;得到字符串形式的hash值
return new BigInteger(1, md.digest()).toString(16);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static void main(String[] args) {
getMD5Str("http://storyrobot.oss-cn-shanghai.aliyuncs.com/json/6c670076da1540d4943377127ea26a1d.json");
}
}
2、将map集合转化成xml,并当成参数调用微信统一下单接口
/**
* @将Dictionary转成xml
* @return 经转换得到的xml串
**/
public String toXml(Map<String,Object> parm)
{
StringBuffer xml = new StringBuffer();
xml.append("<xml>");
if(parm != null){
for (Map.Entry<String, Object> entry : parm.entrySet()) {
if(entry != null ){
}
xml.append("<").append(entry.getKey()).append(">");
//isNotNullOrEmptyStr是判断不为空的方法
if(entry.getValue() instanceof String){
xml.append("<![CDATA[");
xml.append(entry.getValue());
xml.append("]]>");
}else{
xml.append(entry.getValue());
}
xml.append("</").append(entry.getKey()).append(">");
}
}
xml.append("</xml>");
String result ="";
if(xml != null){
try {
result = xml.toString();
//result = new String(xml.toString().getBytes("utf-8"),"ISO8859-1");
} catch (Exception e) {
e.printStackTrace();
}
}
return result;
}
xml转化成map集合
/**
* xml转成map
* @return Map<String, String>
**/
public Map<String, String> xmlToMap(String xml) {
try {
Map<String, String> data = new HashMap<>();
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
InputStream stream = new ByteArrayInputStream(xml.getBytes("UTF-8"));
org.w3c.dom.Document doc = documentBuilder.parse(stream);
doc.getDocumentElement().normalize();
NodeList nodeList = doc.getDocumentElement().getChildNodes();
for (int idx = 0; idx < nodeList.getLength(); ++idx) {
Node node = nodeList.item(idx);
if (node.getNodeType() == Node.ELEMENT_NODE) {
org.w3c.dom.Element element = (org.w3c.dom.Element) node;
data.put(element.getNodeName(), element.getTextContent());
}
}
stream.close();
return data;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
自己的xml
<xml>
<time_expire><![CDATA[20200318171406]]></time_expire>
<nonce_str><![CDATA[r5392r99S0IEtZF91Arl5994068153Pn]]></nonce_str>
<time_start><![CDATA[20200318170406]]></time_start>
<openid><![CDATA[**********]]></openid>
<sign><![CDATA[3B42CACF29F2F1F741A828F769D5F0580F9B46F4711EB964AB08BF50C8796947]]></sign>
<body><![CDATA[产品支付]]></body>
<notify_url><![CDATA[*****/resultNotify]]></notify_url>
<mch_id><![CDATA[**********]]></mch_id>
<spbill_create_ip><![CDATA[127.0.01]]></spbill_create_ip>
<out_trade_no><![CDATA[**********]]></out_trade_no>
<goods_tag><![CDATA[产品支付]]></goods_tag>
<total_fee>10</total_fee>
<appid><![CDATA[**********]]></appid>
<trade_type><![CDATA[JSAPI]]></trade_type>
<attach><![CDATA[产品支付]]></attach>
<sign_type><![CDATA[HMAC-SHA256]]></sign_type>
</xml>