一、業務場景發生
最近在跟一些合作公司作業務對接,在對方的APP中接入我們的H5支付,包括微信和支付寶。
那就開搞,進展順利,貌似一切都在掌握之中,給到對方一個鏈接即可調起支付。形如:
https://www.batsing.com/api/h5pay?appid=1234567001&userid=10&money=0.01&paytype=weixin
通過QQ或者瀏覽器打開即可付款發貨,絲滑完美。
然而。。。接入到他們APP中卻報錯了,趕緊去微信開發文檔看看
微信官方給的方案很簡單,讓APP開發的去修改webview,但這,合理嗎?
對於自家做APP的是可以。但對於像我們這種做第三方的,這顯然不是一個優秀的解決途徑,那怎么辦呢?
二、 問題本質
出現這個問題的本質是什么呢?是APP中頁面跳轉到MWEB_URL的請求中沒有帶上Referer。
(APP中的webview把一些http請求頭給吃掉了,referer不會像普通瀏覽器那樣自動識別轉發出去)
既然前端的解決方案不友好,那就換成后端來解決,強大的curl可以模擬一切http請求。
三、Hack跟蹤微信H5支付過程
在微信下單接口解析xml得到 MWEB_URL,然后對照着微信開發文檔,經過一番參數調試,偽造了請求IP和referer,請求IP要與調用下單接口時傳遞的客戶端IP一致,referer要與微信商戶后台上填寫的授權域名一致。
然后捕捉到了正常的微信支付頁面代碼。
這是curl發送模擬請求的代碼
<?php $mweb_url = 'https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx20161110163838f231619da20804912345&package=1037687096'; $headers = [ "CLIENT-IP:" . $_SERVER['REMOTE_ADDR'], "X-FORWARDED-FOR:" . $_SERVER['REMOTE_ADDR'], ]; $result = Common_Http::curl_get_content($mweb_url, [ 'CURLOPT_HTTPHEADER' => $headers, 'CURLOPT_REFERER' => 'https://www.batsing.com' ]); echo'<pre>';print_r(htmlspecialchars($result));echo'</pre>';
打印出來得到的內容如下,我們只看 window.onload 部分就可以了。

<!DOCTYPE html> <html> <head lang="en"> <meta http-equiv=Content-Type content="text/html;charset=utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="format-detection" content="telephone=no"> <title>weixin</title> <style>.f10{font-size:10px}.f11{font-size:11px}.f12{font-size:12px}.f13{font-size:13px}.f14{font-size:14px}.f15{font-size:15px}.f16{font-size:16px}.f17{font-size:17px}.f18{font-size:18px}.f19{font-size:19px}.f20{font-size:20px}body{font-size:14px}h1,h2,h3,h4,h5{font-weight:400;font-style:normal}h1,.h1{font-size:20px}h2,.h2{font-size:18px}h3,.h3{font-size:16px}h4,.h4{font-size:14px}h5,.h5{font-size:12px}a,a:visited{color:#007aff}.text_color{color:#888}.title_color{color:#000}.desc{color:#b2b2b2}.warn{color:#b71414}.nickname{color:#576b95}.tips{font-size:13px;color:#b2b2b2}body{background-color:#fff}body.msg_dark{background-color:#2e3132;color:#fff}.page_msg{padding:75px 15px 0;text-align:center}.icon_area{margin-bottom:19px}.text_area{margin-bottom:25px}.text_area .title{margin-bottom:12px}.opr_area{margin-bottom:25px}.extra_area{margin-bottom:20px}@media screen and (min-height:416px){.extra_area{position:fixed;left:0;bottom:0;width:100%}}.btn{display:block;margin-left:auto;margin-right:auto;padding-left:14px;padding-right:14px;font-size:16px;text-align:center;text-decoration:none;overflow:visible;height:40px;border-radius:5px;-moz-border-radius:5px;-webkit-border-radius:5px;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;color:#fff;line-height:40px;-webkit-tap-highlight-color:rgba(255,255,255,0)}.btn.btn_inline{display:inline-block}.btn_default{background-color:#d1d1d1}.btn_default:not(.btn_disabled):visited{color:#fff}.btn_default:not(.btn_disabled):active{color:rgba(255,255,255,.4);background-color:#a7a7a7}.btn_primary{background-color:#04be02}.btn_primary:not(.btn_disabled):visited{color:#fff}.btn_primary:not(.btn_disabled):active{color:rgba(255,255,255,.4);background-color:#039702}.btn_warn{background-color:#ef4f4f}.btn_warn:not(.btn_disabled):visited{color:#fff}.btn_warn:not(.btn_disabled):active{color:rgba(255,255,255,.4);background-color:#c13e3e}.btn.btn_mini{height:25px;line-height:25px;font-size:14px}button.btn,input.btn{width:100%;border:0;outline:0;-webkit-appearance:none}button.btn:focus,input.btn:focus{outline:0}button.btn_inline,input.btn_inline{width:auto}.btn_disabled{color:rgba(255,255,255,.6)}.btn+.btn{margin-top:10px}.btn.btn_inline+.btn.btn_inline{margin-top:auto;margin-left:10px}.btn_area{margin-left:-5px;margin-right:-5px;font-size:0}.btn_area.btn_area_inline{margin-left:auto;margin-right:auto;display:-webkit-box;display:-webkit-flex;display:-moz-box;display:-ms-flexbox;display:flex}.btn_area.btn_area_inline .btn{margin-top:auto;margin-right:10px;width:100%;-webkit-box-flex:1;-webkit-flex:1;-moz-box-flex:1;-ms-flex:1;box-flex:1;flex:1;display:inline-block \9;width:48% \9;margin-left:1% \9;margin-right:1% \9}.btn_area.btn_area_inline .btn:last-child{margin-right:0}span.btn button{display:block;width:100%;height:100%;background-color:transparent;border:0;outline:0;color:#fff}span.btn button:active{color:rgba(255,255,255,.4)}span.btn.btn_loading button,span.btn.btn_disabled button{color:#fff}.icon_msg{width:100px;height:100px;vertical-align:middle;display:inline-block;border-radius:50%;-moz-border-radius:50%;-webkit-border-radius:50%}.icon_msg.warn{background-color:#f86161;color:#fff;font-size:60px;font-style:normal}html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{font:14px/1.5em "Helvetica Neue",Helvetica,Arial,sans-serif;background-color:#efeff4;line-height:1.6}body,h1,h2,h3,h4,h5,p,ul,ol,dl,dd,fieldset,textarea{margin:0}fieldset,legend,textarea,input,button{padding:0}button,input,select,textarea{font-family:inherit;font-size:100%;margin:0;*font-family:"Helvetica Neue",Helvetica,Arial,sans-serif}ul,ol{padding-left:0;list-style-type:none}a img,fieldset{border:0}a{text-decoration:none}</style> </head> <body> <div class="body"> <div id="errpage" class="page_msg"> </div> </div> <script src="//wx.gtimg.com/wxpay_h5/fingerprint2.min.1.4.1.js"></script> <script type="text/javascript"> var is_postmsg=""; if( 0!==0 && is_postmsg=="1" ) { parent.postMessage(JSON.stringify({ action : "send_deeplink_fail", data : { deeplink : "" }, error : { error_code : "0", error_msg : "ok" } }), ""); } if( 0===0) { window.onload=function() { // var fp=new Fingerprint2(); // fp.get(function(result) { // var fingerprint=""; /* if(fingerprint!=result && fingerprint) { document.getElementById("errpage").innerHTML='<div class="icon_area"><i class="icon_msg warn">!</i></div> \ <div class="text_area"> \ <h2 id="111" class="title"> '+result+'網絡環境未能通過安全驗證,請稍后再試</h2> \ </div>'; return; }*/ var is_postmsg=""; if(is_postmsg=="1") { parent.postMessage(JSON.stringify({ action : "send_deeplink", data : { deeplink : "weixin://wap/pay?prepayid%3Dwx0917024062319287307475501524736300&package=2450968634&noncestr=1591693360&sign=07b94cb3986c14bab48ec240f805f9a5" } }), ""); } else { var url="weixin://wap/pay?prepayid%3Dwx0917024062319287307475501524736300&package=2450968634&noncestr=1591693360&sign=07b94cb3986c14bab48ec240f805f9a5"; var redirect_url=""; top.location.href=url; if(redirect_url) { setTimeout( function(){ top.location.href=redirect_url; }, 5000 ); } else { setTimeout( function(){ window.history.back(); }, 5000); } } } // ); } } </script> </body> </html>
一開始還以為一直被判斷“網絡環境未能通過安全驗證”,然后才發現那段是被注釋掉的,被虛晃了一槍。
關鍵部分,頁面里有兩段一樣的 weixin://wap/pay? 開頭的網址,很明顯這個網址就是要拉起手機里的微信應用發起支付的。
其他的js代碼邏輯也很簡單,先從頂層跳轉到weixin:// 即是拉起支付,然后5秒鍾后跳轉到重定向地址(若無重定向地址則返回上一頁)。
我們嘗試用手機直接訪問這個 weixin:// 網址,確實可以拉起手機支付,支付也可以順利完成到賬。
那可以確定,IP地址和referer校驗只是在 https://wx.tenpay.com/ 這里有做,到了 weixin://wap/pay 這一步他就不再校驗這些設備信息了。
四、制作新的H5支付頁面
最后,我們參照微信H5支付頁面,拼出一個類似的H5界面,實現跳轉支付以及5秒后跳轉到支付完成頁;
這樣就可以實現在webview中也能順利發起微信H5支付,並實現同樣的跳轉邏輯了。