1、微信支付的流程
如下三張手機截圖,我們在微信網頁端看到的支付,表面上看到的是 “點擊支付按鈕 - 彈出支付框 - 支付成功后出現提示頁面”,實際上的核心處理過程是:
- 點擊支付按鈕時,執行一個Ajax到后台
- 后台通過前台的部分信息(如商品名額,金額等),將其組裝成符合微信要求格式的xml,然后調用微信的“統一下單接口”
- 調用成功后微信會返回一個組裝好的xml,我們提取之中的消息(預支付id也在其中)以JSON形式返回給前台
- 前台將該JSON傳參給微信內置JS的方法中,調其微信支付
- 支付成功后,微信會將本次支付相關信息返回給我們的服務器

2、微信支付功能開發詳解
2.1 設置支付目錄和授權域名
登陸公眾號,進行支付相關的目錄和域名設置,詳情參考《
微信支付官方文檔 - 開發步驟》,我這里簡單貼幾張官方的圖就行了,這步比較簡單,就不過多說明了,只提其中一點:對於微信支付授權的目錄,發起微信支付的頁面必須精確地位於授權目錄下,假如支付頁面為 http://www.a.com/wx/pay/a.html,那么授權目錄必須為 http://www.a.com/wx/pay/,其他的如 http://www.a.com/wx/ , https://www.a.com/wx/pay/(http和https是不一樣的),http://a.com/wx/pay/(千萬別忘了www)都是不行的。填寫了這些非法目錄無法調起支付。


2.2 組裝xml,調用統一下單接口,獲取prepay_id
2.2.1 組裝xml
點擊支付按鈕后,寫一個Ajax將前台部分信息發送給后台,然后組裝xml,調用統一下單接口。該接口在《
微信支付官方文檔 - 統一下單》進行了很詳細的解釋,我在這里進行部分說明:
參數 | 說明 | 備注 |
appId | 開發者應用ID,在 “開發 - 基本配置” 查看 | |
mch_id | 微信支付的商戶號,在 “微信支付 - 商戶信息” 查看 | |
device_info | 終端設備號(門店號或收銀設備ID) | PC網頁或公眾號內支付,則傳 “WEB” |
body | 商品或支付的簡單描述 | |
trade_type | 可取值JSAPI,NATIVE,APP等,我們這里使用的是JSAPI | JSAPI 公眾號支付;NATIVE 原生掃碼支付;APP app支付 |
nonce_str | 隨機字符串 | 參考算法:《微信支付官方文檔 - 安全規范》 |
notify_url | 通知地址,微信支付成功后,微信服務器會發送信息到該url | |
out_trade_no | 商戶系統內部訂單號,由商戶自定義,訂單號要保持唯一性 | |
total_fee | 訂單總金額,單位:分 | |
openid | 用戶標識,用戶在該公眾號下的唯一身份標識 | |
sign | 簽名 | 參考算法:《微信支付官方文檔 - 安全規范》 |
key | API密鑰,在 “微信商戶平台 - 賬戶中心 - API安全 - API密鑰” |
其他的都比較簡單,重要的在於這兩個涉及算法的參數,nonce_str 和 sign,這里說明一下:
- nonce_str 隨機字符串,用於保證簽名不可預測
- 算法:
- 官方建議調用隨機數函數生成,然后轉為字符串
- sign 簽名
- 算法:
- 所有發送或接收的數據按參數名ASCII碼從小到大排序,使用鍵值對形式拼接為字符串(如 key1=value1&key2=value2…)
- ASCII碼的字典排序,可以利用TreeMap幫我們自動實現
- 將拼接好的字符串最后,再拼接上API密鑰,即key,得到新的字符串
- 將新的字符串進行MD5加密,並將加密后字符串全部轉換為大寫
按照以上的這些說明,進行xml的拼裝,貼上我自己的測試代碼(注:為方便測試,部分數據我直接寫入方法了,如body、openId等):
String appId = WeChatAPI.getAppID();
String body = "JSAPI支付測試";
String merchantId = WeChatAPI.getMerchantID();
String tradeNo = String.valueOf(new Date().getTime());
String nonceStr1 = SignUtil.createNonceStr();
String notifyUrl = "http://k169710n05.51mypc.cn/pay/do/afterPaySuccess.q";
String openId = "okAkc0muYuSJUtvMf25UQHnqYvM4";
String totalFee = "1";
TreeMap<String, String> map = new TreeMap<String, String>();
map.put("appid", appId);
map.put("mch_id", merchantId);
map.put("device_info", "WEB");
map.put("body", body);
map.put("trade_type", "JSAPI");
map.put("nonce_str", nonceStr1);
map.put("notify_url", notifyUrl);
map.put("out_trade_no", tradeNo);
map.put("total_fee", totalFee);
map.put("openid", openId);
String sign = SignUtil.createSign(map);
String xml = "<xml>" +
"<appid>" + appId + "</appid>" +
"<body>" + body +"</body>" +
"<device_info>WEB</device_info>" +
"<mch_id>" + merchantId + "</mch_id>" +
"<nonce_str>" + nonceStr1 + "</nonce_str>" +
"<notify_url>" + notifyUrl +"</notify_url>" +
"<openid>" + openId + "</openid>" +
"<out_trade_no>" + tradeNo + "</out_trade_no>" +
"<total_fee>" + totalFee + "</total_fee>" +
"<trade_type>JSAPI</trade_type>" +
"<sign>" + sign + "</sign>" +
"</xml>";
35
1
String appId = WeChatAPI.getAppID();
2
String body = "JSAPI支付測試";
3
String merchantId = WeChatAPI.getMerchantID();
4
String tradeNo = String.valueOf(new Date().getTime());
5
String nonceStr1 = SignUtil.createNonceStr();
6
String notifyUrl = "http://k169710n05.51mypc.cn/pay/do/afterPaySuccess.q";
7
String openId = "okAkc0muYuSJUtvMf25UQHnqYvM4";
8
String totalFee = "1";
9
10
TreeMap<String, String> map = new TreeMap<String, String>();
11
map.put("appid", appId);
12
map.put("mch_id", merchantId);
13
map.put("device_info", "WEB");
14
map.put("body", body);
15
map.put("trade_type", "JSAPI");
16
map.put("nonce_str", nonceStr1);
17
map.put("notify_url", notifyUrl);
18
map.put("out_trade_no", tradeNo);
19
map.put("total_fee", totalFee);
20
map.put("openid", openId);
21
String sign = SignUtil.createSign(map);
22
23
String xml = "<xml>" +
24
"<appid>" + appId + "</appid>" +
25
"<body>" + body +"</body>" +
26
"<device_info>WEB</device_info>" +
27
"<mch_id>" + merchantId + "</mch_id>" +
28
"<nonce_str>" + nonceStr1 + "</nonce_str>" +
29
"<notify_url>" + notifyUrl +"</notify_url>" +
30
"<openid>" + openId + "</openid>" +
31
"<out_trade_no>" + tradeNo + "</out_trade_no>" +
32
"<total_fee>" + totalFee + "</total_fee>" +
33
"<trade_type>JSAPI</trade_type>" +
34
"<sign>" + sign + "</sign>" +
35
"</xml>";
注意:
-
body參數如果直接填寫中文,在調用接口時會出現“簽名錯誤”,要以ISO8859-1編碼 -
所以 String body = new String("body內容字符串".getBytes("ISO8859-1")); -
但即便如此,在支付完成后微信推送的“微信支付憑證”中,商品詳情中的中文也依然顯示的亂碼
- body參數內容如果包含中文,那么在調用接口時會出現“簽名錯誤”
- 在網上找了很多方法,有了如上刪除線部分的方法,但是仍然是有問題的,因為支付成功后的憑證里中文是亂碼
- 后來終於在網上各種倒騰,找到了原因,確實是編碼問題,但問題不在body是否使用ISO8859-1,而在MD5的加密算法中是否使用UTF-8
- 所以 md.update(sourceStr.getBytes("UTF-8"));
兩個算法的代碼如下:
/**
* 生成隨機數
* <p>算法參考:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3</p>
* @return 隨機數字符串
*/
public static String createNonceStr() {
SecureRandom random = new SecureRandom();
int randomNum = random.nextInt();
return Integer.toString(randomNum);
}
/**
* 生成簽名,用於在微信支付前,獲取預支付時候需要使用的參數sign
* <p>算法參考:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3</p>
* @param params 需要發送的所有數據設置為的Map
* @return 簽名sign
*/
public static String createSign(TreeMap<String, String> params) {
String signValue = "";
String stringSignTemp = "";
String stringA = "";
//獲得stringA
Set<String> keys = params.keySet();
for (String key : keys) {
stringA += (key + "=" + params.get(key) + "&");
}
stringA = stringA.substring(0, stringA.length() - 1);
//獲得stringSignTemp
stringSignTemp = stringA + "&key=" + WeChatAPI.getMerchantKey();
//獲得signValue
signValue = encryptByMD5(stringSignTemp).toUpperCase();
log.debug("預支付簽名:" + signValue);
return signValue;
}
/**
* MD5加密
*
* @param sourceStr
* @return
*/
public static String encryptByMD5(String sourceStr) {
String result = "";
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(sourceStr.getBytes("UTF-8"));
byte b[] = md.digest();
int i;
StringBuffer buf = new StringBuffer("");
for (int offset = 0; offset < b.length; offset++) {
i = b[offset];
if (i < 0)
i += 256;
if (i < 16)
buf.append("0");
buf.append(Integer.toHexString(i));
}
result = buf.toString();
} catch (NoSuchAlgorithmException e) {
System.out.println(e);
}
return result;
}
66
1
/**
2
* 生成隨機數
3
* <p>算法參考:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3</p>
4
* @return 隨機數字符串
5
*/
6
public static String createNonceStr() {
7
SecureRandom random = new SecureRandom();
8
int randomNum = random.nextInt();
9
return Integer.toString(randomNum);
10
}
11
12
13
/**
14
* 生成簽名,用於在微信支付前,獲取預支付時候需要使用的參數sign
15
* <p>算法參考:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3</p>
16
* @param params 需要發送的所有數據設置為的Map
17
* @return 簽名sign
18
*/
19
public static String createSign(TreeMap<String, String> params) {
20
String signValue = "";
21
String stringSignTemp = "";
22
String stringA = "";
23
24
//獲得stringA
25
Set<String> keys = params.keySet();
26
for (String key : keys) {
27
stringA += (key + "=" + params.get(key) + "&");
28
}
29
stringA = stringA.substring(0, stringA.length() - 1);
30
//獲得stringSignTemp
31
stringSignTemp = stringA + "&key=" + WeChatAPI.getMerchantKey();
32
//獲得signValue
33
signValue = encryptByMD5(stringSignTemp).toUpperCase();
34
log.debug("預支付簽名:" + signValue);
35
return signValue;
36
}
37
38
39
/**
40
* MD5加密
41
*
42
* @param sourceStr
43
* @return
44
*/
45
public static String encryptByMD5(String sourceStr) {
46
String result = "";
47
try {
48
MessageDigest md = MessageDigest.getInstance("MD5");
49
md.update(sourceStr.getBytes("UTF-8"));
50
byte b[] = md.digest();
51
int i;
52
StringBuffer buf = new StringBuffer("");
53
for (int offset = 0; offset < b.length; offset++) {
54
i = b[offset];
55
if (i < 0)
56
i += 256;
57
if (i < 16)
58
buf.append("0");
59
buf.append(Integer.toHexString(i));
60
}
61
result = buf.toString();
62
} catch (NoSuchAlgorithmException e) {
63
System.out.println(e);
64
}
65
return result;
66
}
2018.05.22 補充:
以前寫的時候沒注意過緣由,今天在思考公司內部api調用時簡單的安全方式,突然想到了微信的這個。參數字典序后進行md5加密,就是為了防止請求的參數被過程中篡改。你想說,參數我知道,字典序md5加密的簽名算法我知道,我改了參數我按同樣的方式也修改簽名,不是一樣的嗎?那你可能遺漏了一個東西,就是商戶的密鑰key,這個東西也是參與進了md5加密算法的,也就是說,第三者修改了參數,但是因為沒有密鑰,它無法修改簽名為正確的簽名,篡改后的請求到達服務器就會被發現有問題。
所以有時候密鑰不是說一定是用來解密的,單純用來加密就可以了,這跟加密的算法也是有關系的,md5本身就是不可逆的算法,我只需要比對簽名是否一致即可。
2.2.2 調用統一下單接口,獲取prepay_id
有了組裝好的xml,現在我們直接使用POST方式的請求發送給微信提供的接口就可以了,如果一切順利,我們會收到微信返回的xml字符串,格式示例如下:
<xml>
<return_code><![CDATA[SUCCESS]]></return_code>
<return_msg><![CDATA[OK]]></return_msg>
<appid><![CDATA[wx2421b1c4370ec43b]]></appid>
<mch_id><![CDATA[10000100]]></mch_id>
<nonce_str><![CDATA[IITRi8Iabbblz1Jc]]></nonce_str>
<openid><![CDATA[oUpF8uMuAJO_M2pxb1Q9zNjWeS6o]]></openid>
<sign><![CDATA[7921E432F65EB8ED0CE9755F0E86D72F]]></sign>
<result_code><![CDATA[SUCCESS]]></result_code>
<prepay_id><![CDATA[wx201411101639507cbf6ffd8b07629950874]]></prepay_id>
<trade_type><![CDATA[JSAPI]]></trade_type>
</xml>
12
1
<xml>
2
<return_code><![CDATA[SUCCESS]]></return_code>
3
<return_msg><![CDATA[OK]]></return_msg>
4
<appid><![CDATA[wx2421b1c4370ec43b]]></appid>
5
<mch_id><![CDATA[10000100]]></mch_id>
6
<nonce_str><![CDATA[IITRi8Iabbblz1Jc]]></nonce_str>
7
<openid><![CDATA[oUpF8uMuAJO_M2pxb1Q9zNjWeS6o]]></openid>
8
<sign><![CDATA[7921E432F65EB8ED0CE9755F0E86D72F]]></sign>
9
<result_code><![CDATA[SUCCESS]]></result_code>
10
<prepay_id><![CDATA[wx201411101639507cbf6ffd8b07629950874]]></prepay_id>
11
<trade_type><![CDATA[JSAPI]]></trade_type>
12
</xml>
其中我們最需要的就是 prepay_id,這個值在后續需要用到。這段過程比較簡單,其中提取prepay_id我是用的正則,我直接貼代碼好了:
String url = WeChatAPI.getUrl_prePay();
String result = NetUtil.sendRequest(url, "POST", xml);
String reg = "<prepay_id><!\\[CDATA\\[(\\w+)\\]\\]></prepay_id>";
Pattern pattern = Pattern.compile(reg);
Matcher matcher = pattern.matcher(result);
String prepayId = "";
while (matcher.find()) {
prepayId = matcher.group(1);
log.debug("預支付ID:" + prepayId);
}
11
1
String url = WeChatAPI.getUrl_prePay();
2
String result = NetUtil.sendRequest(url, "POST", xml);
3
4
String reg = "<prepay_id><!\\[CDATA\\[(\\w+)\\]\\]></prepay_id>";
5
Pattern pattern = Pattern.compile(reg);
6
Matcher matcher = pattern.matcher(result);
7
String prepayId = "";
8
while (matcher.find()) {
9
prepayId = matcher.group(1);
10
log.debug("預支付ID:" + prepayId);
11
}
2.3 回傳參數,調起微信支付JS
2.3.1 回傳參數
這時候,已經有了預支付ID,但是后台的處理還沒有結束,我們還沒有把該有的信息返回給前台。那么前台需要哪些東西呢?《 微信支付官方文檔 - 微信內H5調起支付》有詳細的解釋,這里再貼一下:參數 | 說明 | 備注 |
appId | 開發者應用ID,在 “開發 - 基本配置” 查看 | |
timeStamp | 時間戳,標准北京時間,秒級(10位數字) | |
nonceStr | 隨機字符串 | 參考算法:《微信支付官方文檔 - 安全規范》 |
package | 訂單詳情擴展字符串,其實就是預支付ID | 示例: prepay_id=*** |
signType | 簽名方式,暫支持MD5 | |
paySign | 簽名 | 參考算法:《微信支付官方文檔 - 安全規范》 |
有了之前的經驗,想必到這里對這些的獲取已經沒有什么問題了,但是仍然有幾個
注意的地方:
- package的值是 “prepay_id=***”,而不是 "***" 的方式(***表示之前獲取的prepay_id)
- timeStamp注意使用標准北京時間,可以使用Calendar設置Locale為CHINA,因為是秒級所以記得除以1000
- paySign簽名要重新生成,算法還是之前的,但是參數需要除自己以外的 appId、timeStamp、nonceStr、package、signType
- 之前xml中參數appid是小寫,這里的appId是大寫的I
好了,因為前台接受到參數以后會以JSON的形式發送給微信服務器,所以我們這里后台,直接就把這些參數封裝到一個JSONObject中就行了,然后轉成JSON的形式發給前台。下面貼一下我的測試代碼,簽名算法和之前一樣,我這里就不重復貼出來了:
Date beijingDate = Calendar.getInstance(Locale.CHINA).getTime();
String nonceStr2 = SignUtil.createNonceStr();
JSONObject json = new JSONObject();
json.put("appId", appId);
json.put("timeStamp", beijingDate.getTime() / 1000);
json.put("nonceStr", nonceStr2);
json.put("package", "prepay_id=" + prepayId);
json.put("signType", "MD5");
TreeMap<String, String> map2 = new TreeMap<String, String>();
map2.put("appId", appId);
map2.put("timeStamp", String.valueOf(beijingDate.getTime() / 1000));
map2.put("nonceStr", nonceStr2);
map2.put("package", "prepay_id=" + prepayId);
map2.put("signType", "MD5");
String paySign = SignUtil.createSign(map2);
json.put("paySign", paySign);
String re = json.toJSONString();
AjaxSupport.sendSuccessText(null, re);
21
1
Date beijingDate = Calendar.getInstance(Locale.CHINA).getTime();
2
String nonceStr2 = SignUtil.createNonceStr();
3
JSONObject json = new JSONObject();
4
json.put("appId", appId);
5
json.put("timeStamp", beijingDate.getTime() / 1000);
6
json.put("nonceStr", nonceStr2);
7
json.put("package", "prepay_id=" + prepayId);
8
json.put("signType", "MD5");
9
10
TreeMap<String, String> map2 = new TreeMap<String, String>();
11
map2.put("appId", appId);
12
map2.put("timeStamp", String.valueOf(beijingDate.getTime() / 1000));
13
map2.put("nonceStr", nonceStr2);
14
map2.put("package", "prepay_id=" + prepayId);
15
map2.put("signType", "MD5");
16
String paySign = SignUtil.createSign(map2);
17
18
json.put("paySign", paySign);
19
String re = json.toJSONString();
20
21
AjaxSupport.sendSuccessText(null, re);
2.3.2 使用微信內置的JS調起微信支付
前台的調用就很簡單了,看下官方給的示例代碼:
function onBridgeReady(){
WeixinJSBridge.invoke(
'getBrandWCPayRequest', {
"appId":"wx2421bk1c4370c43b", //公眾號名稱,由商戶傳入
"timeStamp":"1395712654", //時間戳,自1970年以來的秒數
"nonceStr":"e61463f8efa94090b1f366cccfbbb444", //隨機串
"package":"prepay_id=u802345jfgjsdfgsdg888",
"signType":"MD5", //微信簽名方式:
"paySign":"70EA570631E4B79628FBCS90534C63FF7FADD89" //微信簽名
},
function(res){
if(res.err_msg == "get_brand_wcpay_request:ok" ) {}
// 使用以上方式判斷前端返回,微信團隊鄭重提示:res.err_msg將在用戶支付成功后返回ok,但並不保證它絕對可靠。
}
);
}
if (typeof WeixinJSBridge == "undefined"){
if( document.addEventListener ){
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
}else if (document.attachEvent){
document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
}
}else{
onBridgeReady();
}
26
1
function onBridgeReady(){
2
WeixinJSBridge.invoke(
3
'getBrandWCPayRequest', {
4
"appId":"wx2421bk1c4370c43b", //公眾號名稱,由商戶傳入
5
"timeStamp":"1395712654", //時間戳,自1970年以來的秒數
6
"nonceStr":"e61463f8efa94090b1f366cccfbbb444", //隨機串
7
"package":"prepay_id=u802345jfgjsdfgsdg888",
8
"signType":"MD5", //微信簽名方式:
9
"paySign":"70EA570631E4B79628FBCS90534C63FF7FADD89" //微信簽名
10
},
11
function(res){
12
if(res.err_msg == "get_brand_wcpay_request:ok" ) {}
13
// 使用以上方式判斷前端返回,微信團隊鄭重提示:res.err_msg將在用戶支付成功后返回ok,但並不保證它絕對可靠。
14
}
15
);
16
}
17
if (typeof WeixinJSBridge == "undefined"){
18
if( document.addEventListener ){
19
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
20
}else if (document.attachEvent){
21
document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
22
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
23
}
24
}else{
25
onBridgeReady();
26
}
使用時直接替換掉invoke方法中的參數即可,實際上如果后台直接是傳遞的JSON字符串到前台,可以直接解析為JS對象作為參數,下面貼我自己的代碼:
$().invoke("/pay/do/pay.q", null, function (re) {
var result = JSON.parse(re);
function onBridgeReady(){
WeixinJSBridge.invoke(
'getBrandWCPayRequest', result, function(res){
alert(JSON.stringify(res));
if(res.err_msg == "get_brand_wcpay_request:ok" ) {
//doit 這里處理支付成功后的邏輯,通常為頁面跳轉
}
}
);
}
if (typeof WeixinJSBridge == "undefined"){
if( document.addEventListener ){
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
}else if (document.attachEvent){
document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
}
}else{
onBridgeReady();
}
});
25
1
$().invoke("/pay/do/pay.q", null, function (re) {
2
var result = JSON.parse(re);
3
function onBridgeReady(){
4
WeixinJSBridge.invoke(
5
'getBrandWCPayRequest', result, function(res){
6
alert(JSON.stringify(res));
7
if(res.err_msg == "get_brand_wcpay_request:ok" ) {
8
//doit 這里處理支付成功后的邏輯,通常為頁面跳轉
9
}
10
}
11
);
12
}
13
14
if (typeof WeixinJSBridge == "undefined"){
15
if( document.addEventListener ){
16
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
17
}else if (document.attachEvent){
18
document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
19
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
20
}
21
}else{
22
onBridgeReady();
23
}
24
25
});
這里還有個
坑,是iOS和Android系統不同導致的,如上代碼:
- 如果你在 var result = JSON.parse(re); 之前再添加一個用於debug的輸出語句 alert(re);
- 你可以看到傳過來的各項參數,其中timeStamp的值是沒有雙引號的,這會導致在iOS中支付出現錯誤,提示缺少timeStamp參數


所以為了兼容,必須要將這個轉換成字符串,帶上雙引號:
$().invoke("/pay/do/pay.q", null, function (re) {
var result = JSON.parse(re);
result['timeStamp'] = result['timeStamp'] + "";
function onBridgeReady(){
WeixinJSBridge.invoke(
'getBrandWCPayRequest', result, function(res){
alert(JSON.stringify(res));
if(res.err_msg == "get_brand_wcpay_request:ok" ) {
//doit 這里處理支付成功后的邏輯,通常為頁面跳轉
}
}
);
}
if (typeof WeixinJSBridge == "undefined"){
if( document.addEventListener ){
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
}else if (document.attachEvent){
document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
}
}else{
onBridgeReady();
}
});
24
1
$().invoke("/pay/do/pay.q", null, function (re) {
2
var result = JSON.parse(re);
3
result['timeStamp'] = result['timeStamp'] + "";
4
function onBridgeReady(){
5
WeixinJSBridge.invoke(
6
'getBrandWCPayRequest', result, function(res){
7
alert(JSON.stringify(res));
8
if(res.err_msg == "get_brand_wcpay_request:ok" ) {
9
//doit 這里處理支付成功后的邏輯,通常為頁面跳轉
10
}
11
}
12
);
13
}
14
if (typeof WeixinJSBridge == "undefined"){
15
if( document.addEventListener ){
16
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
17
}else if (document.attachEvent){
18
document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
19
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
20
}
21
}else{
22
onBridgeReady();
23
}
24
});
另外,在這個頁面調試有個小技巧,將微信回調的JS對象序列化為JSON字符串,進行彈窗顯示:alert(JSON.stringify(res));

2.4 校驗信息的正確性
實際上在完成上面的步驟以后,已經可以進行微信支付了。這最后一步主要是為了確認支付信息的正確性,以及傳遞給我們本次支付的一些信息,以便業務處理。
支付成功后,微信會將本次支付的相關信息,以流的方式發送給我們指定的url地址,而我們指定的url地址,就是第一次組裝xml時 <notify_url> 中填寫的地址,下面我們可以先回顧一下:
...
String notifyUrl = "http://k169710n05.51mypc.cn/pay/do/afterPaySuccess.q";
...
String xml = "<xml>" +
"<appid>" + appId + "</appid>" +
"<body>" + body +"</body>" +
"<device_info>WEB</device_info>" +
"<mch_id>" + merchantId + "</mch_id>" +
"<nonce_str>" + nonceStr1 + "</nonce_str>" +
"<notify_url>" + notifyUrl +"</notify_url>" +
"<openid>" + openId + "</openid>" +
"<out_trade_no>" + tradeNo + "</out_trade_no>" +
"<total_fee>" + totalFee + "</total_fee>" +
"<trade_type>JSAPI</trade_type>" +
"<sign>" + sign + "</sign>" +
"</xml>";
16
1
...
2
String notifyUrl = "http://k169710n05.51mypc.cn/pay/do/afterPaySuccess.q";
3
...
4
String xml = "<xml>" +
5
"<appid>" + appId + "</appid>" +
6
"<body>" + body +"</body>" +
7
"<device_info>WEB</device_info>" +
8
"<mch_id>" + merchantId + "</mch_id>" +
9
"<nonce_str>" + nonceStr1 + "</nonce_str>" +
10
"<notify_url>" + notifyUrl +"</notify_url>" +
11
"<openid>" + openId + "</openid>" +
12
"<out_trade_no>" + tradeNo + "</out_trade_no>" +
13
"<total_fee>" + totalFee + "</total_fee>" +
14
"<trade_type>JSAPI</trade_type>" +
15
"<sign>" + sign + "</sign>" +
16
"</xml>";
而我們要做的,就是接受到這些信息后,進行處理,並對微信服務器做出應答。如果微信收到商戶的應答不是成功或超時,微信認為通知失敗,微信會通過一定的策略定期重新發起通知,盡可能提高通知的成功率,但微信不保證通知最終能成功。詳情請參考《
微信支付官方文檔 - 支付結果通知》
需要做三件事:
- 解析微信發來的信息,通過重新簽名的方式驗證信息的正確性,確認信息是否是微信所發
- return_code和result_code都是SUCCESS的話,處理商戶自己的業務邏輯
- 應答微信,告訴它說我們收到信息了,不用再發了(如果不進行應答,則微信服務器會通過一定的策略定期重新發起通知)
過程也很簡單,將微信發來的流信息解析出來之后,再次調用之前的簽名算法,用計算出來的算法,和微信發來的xml中的簽名sign進行對比,如果相同,則說明是微信返回的通知,響應給微信即可。
注意:驗證調用返回或微信主動通知簽名時,傳送的sign參數不參與簽名,而是將生成的簽名與該sign值作校驗。也就是說,微信發來的xml中包含元素sign,該元素內容不參與簽名算法之中,而是和最后算法的結果進行比較的,所以傳參進行算法的時候不用加入sign值。
好了,現在我們先看下微信發回來的流信息是什么,實際上文檔里有說明,就是個xml,我們看下官方的示例:
<xml>
<appid><![CDATA[wx2421b1c4370ec43b]]></appid>
<attach><![CDATA[支付測試]]></attach>
<bank_type><![CDATA[CFT]]></bank_type>
<fee_type><![CDATA[CNY]]></fee_type>
<is_subscribe><![CDATA[Y]]></is_subscribe>
<mch_id><![CDATA[10000100]]></mch_id>
<nonce_str><![CDATA[5d2b6c2a8db53831f7eda20af46e531c]]></nonce_str>
<openid><![CDATA[oUpF8uMEb4qRXf22hE3X68TekukE]]></openid>
<out_trade_no><![CDATA[1409811653]]></out_trade_no>
<result_code><![CDATA[SUCCESS]]></result_code>
<return_code><![CDATA[SUCCESS]]></return_code>
<sign><![CDATA[B552ED6B279343CB493C5DD0D78AB241]]></sign>
<sub_mch_id><![CDATA[10000100]]></sub_mch_id>
<time_end><![CDATA[20140903131540]]></time_end>
<total_fee>1</total_fee>
<trade_type><![CDATA[JSAPI]]></trade_type>
<transaction_id><![CDATA[1004400740201409030005092168]]></transaction_id>
</xml>
19
1
<xml>
2
<appid><![CDATA[wx2421b1c4370ec43b]]></appid>
3
<attach><![CDATA[支付測試]]></attach>
4
<bank_type><![CDATA[CFT]]></bank_type>
5
<fee_type><![CDATA[CNY]]></fee_type>
6
<is_subscribe><![CDATA[Y]]></is_subscribe>
7
<mch_id><![CDATA[10000100]]></mch_id>
8
<nonce_str><![CDATA[5d2b6c2a8db53831f7eda20af46e531c]]></nonce_str>
9
<openid><![CDATA[oUpF8uMEb4qRXf22hE3X68TekukE]]></openid>
10
<out_trade_no><![CDATA[1409811653]]></out_trade_no>
11
<result_code><![CDATA[SUCCESS]]></result_code>
12
<return_code><![CDATA[SUCCESS]]></return_code>
13
<sign><![CDATA[B552ED6B279343CB493C5DD0D78AB241]]></sign>
14
<sub_mch_id><![CDATA[10000100]]></sub_mch_id>
15
<time_end><![CDATA[20140903131540]]></time_end>
16
<total_fee>1</total_fee>
17
<trade_type><![CDATA[JSAPI]]></trade_type>
18
<transaction_id><![CDATA[1004400740201409030005092168]]></transaction_id>
19
</xml>
其中除了sign的值,其他的值需要做成集合進行簽名算法,然后結果和sign值對比,相同的話,給微信一個應答,應答的格式官方也給出了示例,如下:
<xml>
<return_code><![CDATA[SUCCESS]]></return_code>
<return_msg><![CDATA[OK]]></return_msg>
</xml>
4
1
<xml>
2
<return_code><![CDATA[SUCCESS]]></return_code>
3
<return_msg><![CDATA[OK]]></return_msg>
4
</xml>
總之,這一部分還是很簡單的,就直接上我的代碼了:
/**
* 支付成功后的處理
* <p>微信支付成功后,對微信返回的信息進行校驗</p>
* @return
*/
public String afterPaySuccess() {
HttpServletRequest request = ServletActionContext.getRequest();
HttpServletResponse response = ServletActionContext.getResponse();
TreeMap<String, String> map = new TreeMap<String, String>();
try {
//解析xml,存入map
InputStream inputStream = request.getInputStream();
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(inputStream);
Element rootElement = document.getRootElement();
List<Element> elements = rootElement.elements();
String reg = "<!\\[CDATA\\[(.+)\\]\\]>";
Pattern pattern = Pattern.compile(reg);
for (Element element : elements) {
String key = element.getName();
String value = element.getText();
Matcher matcher = pattern.matcher(value);
while (matcher.find()) {
value = matcher.group(1);
}
map.put(key, value);
}
//如果微信結果通知為失敗
if ("FAIL".equals(map.get("return_code"))) {
log.debug(map.get("return_msg"));
return NONE;
}
//doit 處理商戶業務邏輯
//簽名對比,應答微信服務器
String signFromWechat = map.get("sign");
map.remove("sign");
String sign = SignUtil.createSign(map);
if (sign.equals(signFromWechat)) {
String responseXml = "<xml>" +
"<return_code><![CDATA[SUCCESS]]></return_code>" +
"<return_msg><![CDATA[OK]]></return_msg>" +
"</xml>";
response.getWriter().write(responseXml);
}
} catch (IOException e) {
e.printStackTrace();
} catch (DocumentException e) {
e.printStackTrace();
}
return NONE;
}
x
1
/**
2
* 支付成功后的處理
3
* <p>微信支付成功后,對微信返回的信息進行校驗</p>
4
* @return
5
*/
6
public String afterPaySuccess() {
7
HttpServletRequest request = ServletActionContext.getRequest();
8
HttpServletResponse response = ServletActionContext.getResponse();
9
10
TreeMap<String, String> map = new TreeMap<String, String>();
11
try {
12
//解析xml,存入map
13
InputStream inputStream = request.getInputStream();
14
SAXReader saxReader = new SAXReader();
15
Document document = saxReader.read(inputStream);
16
Element rootElement = document.getRootElement();
17
List<Element> elements = rootElement.elements();
18
19
String reg = "<!\\[CDATA\\[(.+)\\]\\]>";
20
Pattern pattern = Pattern.compile(reg);
21
for (Element element : elements) {
22
String key = element.getName();
23
String value = element.getText();
24
Matcher matcher = pattern.matcher(value);
25
while (matcher.find()) {
26
value = matcher.group(1);
27
}
28
map.put(key, value);
29
}
30
31
//如果微信結果通知為失敗
32
if ("FAIL".equals(map.get("return_code"))) {
33
log.debug(map.get("return_msg"));
34
return NONE;
35
}
36
37
//doit 處理商戶業務邏輯
38
39
//簽名對比,應答微信服務器
40
String signFromWechat = map.get("sign");
41
map.remove("sign");
42
String sign = SignUtil.createSign(map);
43
if (sign.equals(signFromWechat)) {
44
String responseXml = "<xml>" +
45
"<return_code><![CDATA[SUCCESS]]></return_code>" +
46
"<return_msg><![CDATA[OK]]></return_msg>" +
47
"</xml>";
48
response.getWriter().write(responseXml);
49
}
50
51
} catch (IOException e) {
52
e.printStackTrace();
53
} catch (DocumentException e) {
54
e.printStackTrace();
55
}
56
57
return NONE;
58
}
另外,如果在執行支付流程中,有部分數據希望能放在支付完成后再處理,可以在組裝xml的時候放置在attach標簽中;然后在支付完成后微信發送來的xml中,會將原數據在此返回。需要注意的是,該attach有字符串的長度限制(詳見文檔),所以試圖直接在支付處理時直接把某個類的JSON格式放進來留做事后處理,是會出錯的(我就是這樣踩了坑),所以用來傳遞一些核心數據就行了。
再另外,對於最后這部分,看看微信推薦我們的做法是:
當收到通知進行處理時,首先檢查對應業務數據的狀態,判斷該通知是否已經處理過,如果沒有處理過再進行處理,如果處理過直接返回結果成功。在對業務數據進行狀態檢查和處理之前,要采用數據鎖進行並發控制,以避免函數重入造成的數據混亂。另,商戶系統對於支付結果通知的內容一定要做簽名驗證,並校驗返回的訂單金額是否與商戶側的訂單金額一致,防止數據泄漏導致出現“假通知”,造成資金損失。
3、參考鏈接
附件列表