微信支付踩过的坑
一、确认支付父界面跳转
这么做是为了获取用户openid,详情文档:
网页授权获取用户基本信息 - 微信公众平台开发者文档
window.location.href = "/Wechat/pay/toPayInfo.do?money="+money+"&type="+type;
二、微信支付文档
微信JSSDK说明文档 - 微信公众平台开发者文档
主要使用:
通过config接口注入权限验证配置
wx.config({
debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: '', // 必填,公众号的唯一标识
timestamp: , // 必填,生成签名的时间戳
nonceStr: '', // 必填,生成签名的随机串
signature: '',// 必填,签名,见附录1
jsApiList: [] // 必填,需要使用的JS接口列表,所有JS接口列表见附录2
});
发起一个微信支付请求
wx.chooseWXPay({
timestamp: 0, // 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
nonceStr: '', // 支付签名随机串,不长于 32 位
package: '', // 统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=***)
signType: '', // 签名方式,默认为'SHA1',使用新版支付需传入'MD5'
paySign: '', // 支付签名
success: function (res) {
// 支付成功后的回调函数
}
});
- 备注:prepay_id 通过微信支付统一下单接口拿到,paySign 采用统一的微信支付 Sign 签名生成方法,
注意这里 appId 也要参与签名,appId 与 config 中传入的 appId 一致,
即最后参与签名的参数有appId, timeStamp, nonceStr, package, signType,key
。 最坑的就是这里,paySign签名需要key的参加
key设置路径:微信商户平台(pay.weixin.qq.com)–>账户设置–>API安全–>密钥设置
- 请注意该接口只能在你配置的支付目录下调用,同时需确保支付目录在JS接口安全域名下。
- 微信支付开发文档:https://pay.weixin.qq.com/wiki/doc/api/index.html
三、3个签名
config的signature签名:
签名算法:附录1-JS-SDK使用权限签名算法
Map<String, String> signMap = new HashMap<>();
signMap.put("jsapi_ticket", jsapi_ticket);
signMap.put("noncestr", nonceStr);
signMap.put("timestamp", timeStamp);
signMap.put("url", url);
String stringA = SortUtils.sort(signMap);
String signature = EncryptUtils.getSha1(stringA);
prepay_id的sign签名:
签名算法:【微信支付】公众号支付开发者文档
Map<String, String> preMap = new HashMap<>();
preMap.put("appid", appid);
preMap.put("body", body);
preMap.put("mch_id", mch_id);
preMap.put("nonce_str", nonceStr);
preMap.put("notify_url", notify_url);
preMap.put("openid", open_id);
preMap.put("out_trade_no", outTradeNo);
preMap.put("spbill_create_ip", spBillCreateIP);
preMap.put("total_fee", total + "");
preMap.put("trade_type", "JSAPI");
String preStringA = SortUtils.sort(preMap) + "&key=" + key;
String sign = EncryptUtils.getMD5(preStringA).toUpperCase();
chooseWXPay的paySign签名:
签名算法同上
Map<String, String> payMap = new HashMap<>();
payMap.put("appId", appid);
payMap.put("nonceStr", nonceStr);
payMap.put("package", prepay_id);
payMap.put("signType", "MD5");
payMap.put("timeStamp", timeStamp);
// GTMD的腾讯,文档这么写的:
// 备注:prepay_id 通过微信支付统一下单接口拿到,paySign 采用统一的微信支付 Sign 签名生成方法,注意这里 appId 也要参与签名,appId 与 config 中传入的 appId 一致,
// 即最后参与签名的参数有appId, timeStamp, nonceStr, package, signType。
// 微信支付开发文档:https://pay.weixin.qq.com/wiki/doc/api/index.html
// 没说有 + "&key=" + key
String payStringA = SortUtils.sort(payMap) + "&key=" + key;
String paySign = EncryptUtils.getMD5(payStringA).toUpperCase();
四、PayAction.java
@Controller
@RequestMapping("/pay")
public class PayAction {
@RequestMapping("/toPayInfo")
public ModelAndView toPayInfo(HttpServletRequest request) {
ModelAndView view = new ModelAndView();
try {
String money = request.getParameter("money");
String type = request.getParameter("type");
String appid = "xxx";
// 这里直接写回调地址,不能写动态地址,否则redirect_uri参数错误
String redirect_uri = "http://xxx.xxx/Wechat/pay/payment.jsp"
+ "?money=" + money + "&type=" + type;
// 关键(传递中文参数,否则重定向到下一个请求中文参数为null)
redirect_uri = URLEncoder.encode(redirect_uri, "UTF-8");
String url = "https://open.weixin.qq.com/connect/oauth2/authorize?appid="
+ appid
+ "&redirect_uri="
+ redirect_uri
+ "&response_type=code"
+ "&scope=snsapi_base"
+ "&state=123#wechat_redirect";
view.setViewName("redirect:" + url);
} catch (Exception e) {
e.printStackTrace();
}
return view;
}
@ResponseBody
@RequestMapping("/getPayInfo")
public Object getPayInfo(HttpServletRequest request) {
Map<String, Object> map = new HashMap<>();
try {
// 初始化参数
String nonceStr = StringUtil.generateRandomString(16);
String timeStamp = DateUtils.getCurrentTimestamp() + "";
String outTradeNo = "NO" + System.currentTimeMillis() / 1000
+ StringUtil.generateRandomString(6);
String body = "缴费";
String spBillCreateIP = ReqUtils.getRealIp(request);
// request参数
String money = request.getParameter("money");
String type = request.getParameter("type");
String code = request.getParameter("code");
// 应用参数
int total = (int) (Float.parseFloat(money) * 100);
String appid = "xxx";
String secret = "xxx";
String mch_id = "xxx";
String key = "xxx";
// 获取openid
String open_url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid="
+ appid
+ "&secret="
+ secret
+ "&code="
+ code
+ "&grant_type=authorization_code";
JSONObject open_json = JSONObject.fromObject(execute(open_url));
String open_id = open_json.getString("openid");
// 获取access_token
String acct_url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid="
+ appid + "&secret=" + secret;
JSONObject json = JSONObject.fromObject(execute(acct_url));
String access_token = json.getString("access_token");
// 获取jsapi_ticket
String jsapi_url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token="
+ access_token + "&type=jsapi";
JSONObject jsapi_json = JSONObject.fromObject(execute(jsapi_url));
String jsapi_ticket = jsapi_json.getString("ticket");
// config签名,url需要动态,否则签名失败
String url = request.getScheme() + "://" + request.getServerName()
+ request.getRequestURI() + "?money=" + money + "&type="
+ type;
Map<String, String> signMap = new HashMap<>();
signMap.put("jsapi_ticket", jsapi_ticket);
signMap.put("noncestr", nonceStr);
signMap.put("timestamp", timeStamp);
signMap.put("url", url);
String stringA = SortUtils.sort(signMap);
String signature = EncryptUtils.getSha1(stringA);
// 获取prepay_id
String prepay_url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
String notify_url = "http://xxx.xxx/Wechat/pay/notify.do";
// 计算sign
Map<String, String> preMap = new HashMap<>();
preMap.put("appid", appid);
preMap.put("body", body);
preMap.put("mch_id", mch_id);
preMap.put("nonce_str", nonceStr);
preMap.put("notify_url", notify_url);
preMap.put("openid", open_id);
preMap.put("out_trade_no", outTradeNo);
preMap.put("spbill_create_ip", spBillCreateIP);
preMap.put("total_fee", total + "");
preMap.put("trade_type", "JSAPI");
String preStringA = SortUtils.sort(preMap) + "&key=" + key;
// 需要转为大写
String sign = EncryptUtils.getMD5(preStringA).toUpperCase();
// 组装prepay参数
StringBuffer params = new StringBuffer();
params.append("<xml>");
params.append("<appid>" + appid + "</appid>");
params.append("<mch_id>" + mch_id + "</mch_id>");
params.append("<body>" + body + "</body>");
params.append("<nonce_str>" + nonceStr + "</nonce_str>");
params.append("<sign>" + sign + "</sign>");
params.append("<notify_url>" + notify_url + "</notify_url>");
params.append("<out_trade_no>" + outTradeNo + "</out_trade_no>");
params.append("<spbill_create_ip>" + spBillCreateIP
+ "</spbill_create_ip>");
params.append("<total_fee>" + total + "</total_fee>");
params.append("<trade_type>JSAPI</trade_type>");
params.append("<openid>" + open_id + "</openid>");
params.append("</xml>");
// 请求prepay_id
String result_str = execute(prepay_url, params.toString());
Map<String, String> result = MobiMessage
.readStringXmlOut(result_str);
String prepay_id = "prepay_id=" + result.get("prepay_id");
System.out.println(prepay_id);
// 计算paySign "appId nonceStr package signType timeStamp"
Map<String, String> payMap = new HashMap<>();
payMap.put("appId", appid);
payMap.put("nonceStr", nonceStr);
payMap.put("package", prepay_id);
payMap.put("signType", "MD5");
payMap.put("timeStamp", timeStamp);
// GTMD的腾讯,文档这么写的:
// 备注:prepay_id 通过微信支付统一下单接口拿到,paySign 采用统一的微信支付 Sign 签名生成方法,注意这里 appId 也要参与签名,appId 与 config 中传入的 appId 一致,
// 即最后参与签名的参数有appId, timeStamp, nonceStr, package, signType。
// 微信支付开发文档:https://pay.weixin.qq.com/wiki/doc/api/index.html
// 没说有 + "&key=" + key
String payStringA = SortUtils.sort(payMap) + "&key=" + key;
// 需要转为大写
String paySign = EncryptUtils.getMD5(payStringA).toUpperCase();
map.put("appId", appid);
map.put("timeStamp", timeStamp);
map.put("nonceStr", nonceStr);
map.put("signature", signature);
map.put("prepay_id", prepay_id);
map.put("signType", "MD5");
map.put("paySign", paySign);
map.put("money", money);
map.put("type", type);
} catch (Exception e) {
e.printStackTrace();
}
return map;
}
@ResponseBody
@RequestMapping("/notify")
public Object notify(HttpServletRequest request) {
Map<String, Object> map = RequestParamUtil.handleParamToMap(request);
System.out.println("notify:" + map);
return map;
}
// 执行HTTP请求
public String execute(String url) throws Exception {
CloseableHttpClient client = HttpClientBuilder.create().build();
HttpPost httpPost = new HttpPost(url);
// 执行
HttpResponse response = client.execute(httpPost);
return EntityUtils.toString(response.getEntity());
}
public String execute(String url, String params) throws Exception {
CloseableHttpClient client = HttpClientBuilder.create().build();
HttpPost httpPost = new HttpPost(url);
httpPost.setEntity(new StringEntity(params, "UTF-8"));
// 执行
HttpResponse response = client.execute(httpPost);
return EntityUtils.toString(response.getEntity(), "UTF-8");
}
}
四、payment.jsp
引入<script type="text/javascript" src="http://res.wx.qq.com/open/js/jweixin-1.0.0.js"></script>
var appId, timestamp, nonceStr, prepay_id, signType, paySign;
$.ajax({
url: "/Wechat/pay/getPayInfo.do",
data: {type: '<%=type%>', money: '<%=money%>', code: '<%=code%>'},
type: "post",
dataType: "json",
success: function (data) {
//console.info(data);
wx.config({
debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: data.appId, // 必填,公众号的唯一标识
timestamp: data.timeStamp, // 必填,生成签名的时间戳
nonceStr: data.nonceStr, // 必填,生成签名的随机串
signature: data.signature,// 必填,签名,见附录1
jsApiList: ['chooseWXPay'] // 必填,需要使用的JS接口列表,所有JS接口列表见附录2
});
appId = data.appId;
timestamp = data.timeStamp;
nonceStr = data.nonceStr;
prepay_id = data.prepay_id;
signType = data.signType;
paySign = data.paySign;
}
});
function pay () {
wx.chooseWXPay({
timestamp: timestamp, // 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
nonceStr: nonceStr, // 支付签名随机串,不长于 32 位
package: prepay_id, // 统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=***)
signType: signType, // 签名方式,默认为'SHA1',使用新版支付需传入'MD5'
paySign: paySign, // 支付签名
success: function (res) {
// 支付成功后的回调函数
alert("success:" + res);
}
});
};
五、SortUtils.java
public class SortUtils {
/**
* key排序连接返回
*
* @param map
* @return
*/
public static String sort(Map<String, String> map) {
StringBuffer sb = new StringBuffer();
if (null != map && !map.isEmpty()) {
String[] keys = new String[] {};
keys = map.keySet().toArray(keys);
Arrays.sort(keys);
for (int i = 0; i < keys.length; i++) {
if (i == 0) {
sb.append(keys[i] + "=" + map.get(keys[i]));
} else {
sb.append("&" + keys[i] + "=" + map.get(keys[i]));
}
}
}
return sb.toString();
}
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("appId", "1");
map.put("nonceStr", "2");
map.put("package", "3");
map.put("signType", "4");
map.put("timeStamp", "5");
System.out.println(sort(map));
}
}
六、EncryptUtils.java
public class EncryptUtils {
public static String getSha1(String str) {
try {
StringBuffer sb = new StringBuffer();
MessageDigest digest = MessageDigest.getInstance("SHA1");
digest.update(str.getBytes("UTF-8"));
byte[] md = digest.digest();
for (int i = 0; i < md.length; i++) {
String sha = Integer.toHexString(md[i] & 0xFF);
if (sha.length() < 2) {
sb.append("0");
}
sb.append(sha);
}
return sb.toString();
} catch (Exception e) {
e.printStackTrace();
return "";
}
}
public static String getMD5(String str) {
try {
StringBuffer sb = new StringBuffer();
MessageDigest digest = MessageDigest.getInstance("MD5");
digest.update(str.getBytes("UTF-8"));
byte[] md = digest.digest();
for (int i = 0; i < md.length; i++) {
String sha = Integer.toHexString(md[i] & 0xFF);
if (sha.length() < 2) {
sb.append("0");
}
sb.append(sha);
}
return sb.toString();
} catch (Exception e) {
e.printStackTrace();
return "";
}
}
}