1.背景
最近畫啦啦app打算接入直播買課一個(gè)功能,希望在app端通過配置一個(gè)購買鏈接,然后通過鏈接進(jìn)入支付界面調(diào)起原生的支付寶和微信支付功能。以前接觸的都是通過客戶端的sdk接入,這次的做法是通過攔截h5機(jī)制,然后調(diào)用原生支付。
2.微信支付原理

用文字描述,大概的流程如下:
1.用戶在商戶側(cè)完成下單,使用微信支付進(jìn)行支付
2.由商戶后臺(tái)向微信支付發(fā)起下單請(qǐng)求(調(diào)用統(tǒng)一下單接口)注:交易類型trade_type=MWEB
3.統(tǒng)一下單接口返回支付相關(guān)參數(shù)給商戶后臺(tái),如支付跳轉(zhuǎn)url(參數(shù)名“mweb_url”),商戶通過mweb_url調(diào)起微信支付中間頁
4.中間頁進(jìn)行H5權(quán)限的校驗(yàn),安全性檢查
5.如支付成功,商戶后臺(tái)會(huì)接收到微信側(cè)的異步通知
6.用戶在微信支付收銀臺(tái)完成支付或取消支付,返回商戶頁面(默認(rèn)為返回支付發(fā)起頁面),所以這里需要配置回調(diào)地址
7.商戶在展示頁面,引導(dǎo)用戶主動(dòng)發(fā)起支付結(jié)果的查詢
8~9.商戶后臺(tái)判斷是否接收到微信側(cè)的支付結(jié)果通知,如沒有,后臺(tái)調(diào)用我們的訂單查詢接口確認(rèn)訂單狀態(tài)(查單實(shí)現(xiàn)可參考:支付回調(diào)和查單實(shí)現(xiàn)指引)
10.展示最終的訂單支付結(jié)果給用戶
2.1 iOS端h5攔截調(diào)起原生微信支付
如果是在微信內(nèi)打開的話,是通過wechat-js的原理,會(huì)自動(dòng)打開微信進(jìn)行支付,但是若想在畫啦啦app里面通過鏈接打開微信支付,則需要通過webview 的代理方法,解析url的過程,對(duì)url進(jìn)行判斷,如果符合微信的鏈接,則接管支付需求,調(diào)起原生app支付。下面是具體的流程圖

文字描述
- 1.通過MKWebView 代理方法攔截mweb_url
- 2.配置好Referer和redirect_url,目的是為了可以返回應(yīng)用app和通過授權(quán)域名
- 3.接收到支付通知(weixin://wap/pay),調(diào)用微信支付
- 4.付款完成后,通過第2步設(shè)置的redirect_url 返回到app(app記得配置scheme和白名單)
Referer:HTTP Referer是header的一部分,當(dāng)發(fā)起頁面請(qǐng)求的時(shí)候,一般會(huì)帶上Referer,告訴服務(wù)器來源頁面,微信中間頁會(huì)對(duì)Referer進(jìn)行校驗(yàn),非安全域名將不能正常加載,目前我們配置的后臺(tái)是一級(jí)域名61info.cn
redirect_url :是微信中間頁喚起支付之后,頁面重定向的地址。中間頁喚起微信支付后會(huì)跳轉(zhuǎn)到指定的redirect_url,并且微信App在支付完成時(shí),也是通過redirect_url回調(diào)結(jié)果,redirect_url一般是一個(gè)頁面地址,所以微信支付完成打開safari瀏覽器,因此我們需要通過修改redirect_url,實(shí)現(xiàn)支付完畢跳轉(zhuǎn)回當(dāng)前APP
注意:微信會(huì)校驗(yàn)Referer(來源)和redirect_url(目標(biāo))是否是安全域名。如果不傳redirect_url,微信會(huì)將Referer當(dāng)成redirect_url,喚起支付之后會(huì)重定向到Referer對(duì)應(yīng)的頁面。
建議帶上redirect_url
2.1.1 微信具體攔截OC代碼
`if` `([originUrl rangeOfString:@``"[https://wx.tenpay.com](https://wx.tenpay.com/)"``].location != NSNotFound) {`
`NSDictionary *params = [originUrl getUrlParams];`
`NSString *backUrl = params[@``"redirect_url"``];`
`NSURL *redirURL = nil;`
`if` `(backUrl && [backUrl isKindOfClass:[NSString ``class``]] && backUrl.length > ``0``) {`
`redirURL = [NSURL URLWithString:backUrl];`
`}`
`if` `(!wxScheme || ![wxScheme isKindOfClass:[NSString ``class``]] || wxScheme.length <= ``0``) {`
`return` `YES;`
`}`
`NSString *referer = [NSString stringWithFormat:@``"%@://"``, wxScheme];`
`if` `([backUrl isEqualToString:referer]) {`
`return` `YES;`
`} ``else` `{`
`self.redirectUrl = [backUrl stringByURLDecode];`
`dispatch_async(dispatch_get_main_queue(), ^{`
`NSRange range = [originUrl rangeOfString:@``"redirect_url="``];`
`NSString *reqUrl;`
`//將redirect_url 替換成scheme,微信支付完畢才能跳回App,否則會(huì)打開瀏覽器`
`if` `(range.length > ``0``) {`
`reqUrl = [originUrl substringToIndex:range.location + range.length];`
`reqUrl = [reqUrl stringByAppendingString:referer];`
`} ``else` `{`
`reqUrl = [originUrl stringByAppendingString:[NSString stringWithFormat:@``"&redirect_url=%@"``, referer]];`
`}`
`NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:reqUrl] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:``60.0``];`
`//設(shè)置授權(quán)域名,偽造Referer頭,因?yàn)槲⑿胖虚g頁會(huì)檢驗(yàn)Referer頭,并且Referer對(duì)應(yīng)的值需要包含安全域名`
`[request setValue:referer forHTTPHeaderField:@``"Referer"``];`
`[webView loadRequest:request];`
`});`
`return` `NO;`
`}`
`} ``else` `if` `([originUrl rangeOfString:@``"weixin://wap/pay"``].location != NSNotFound) {`
`if` `([[UIApplication sharedApplication] canOpenURL:url]) {`
`if` `(``@available``(iOS ``10.0``, *)) {`
`[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];`
`} ``else` `{`
`[[UIApplication sharedApplication] openURL:url];`
`}`
`}`
`dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(``3` `* NSEC_PER_SEC)), dispatch_get_main_queue(), ^{`
`if` `(self.redirectUrl) {`
`self.redirectUrl = [self.redirectUrl stringByURLDecode];`
`NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:self.redirectUrl] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:``60.0``];`
`[webView loadRequest:request];`
`self.redirectUrl = nil;`
`}`
`});`
`return` `NO;`
`}`
2.1.2 Xcode 配置
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>wxPay</string>
<key>CFBundleURLSchemes</key>
<array>
<string>微信scheme(安全域名)</string> </array>
</dict>
</array>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>wechat</string>
<string>weixin</string>
</array>
如下圖所示:

3.支付寶支付流程

用文字描述,大致如下:
- 1.用戶在商戶的h5網(wǎng)站下單支付后,商品系統(tǒng)調(diào)用alipay.trade.wap.pay 接口參數(shù)生成訂單數(shù)據(jù),然后在前端頁面通過Form表單的形式向支付寶系統(tǒng)發(fā)送支付請(qǐng)求。
- 2.此時(shí)支付寶會(huì)自動(dòng)將頁面跳轉(zhuǎn)至支付寶H5收銀臺(tái)頁面(攔截就在這一步處理)
- 3.用戶在支付寶App或者H5收銀臺(tái)支付完成后,會(huì)根據(jù)商戶在手機(jī)網(wǎng)站支付API中傳入的前臺(tái)回調(diào)地址return_url 自動(dòng)跳轉(zhuǎn)回商戶頁面,同時(shí)在URL請(qǐng)求中以Query String的形式附帶上支付結(jié)果參數(shù)
3.1 iOS端h5攔截調(diào)起原生支付寶支付
原理和攔截微信支付差不多的,流程圖

原理大致和微信相差不大,所以這里不作解釋
3.1.1 支付寶具體攔截OC代碼
NSString *decodeAlipayUrl = [originUrl stringByURLDecode];
NSRange alipayRange = [decodeAlipayUrl rangeOfString:@"openapi.alipay.com"];
//支付寶H5調(diào)用
if ([url.scheme isEqualToString:@"alipay"]) {
// 1.以?號(hào)來切割字符串
NSArray *urlBaseArr = [originUrl componentsSeparatedByString:@"?"];
NSString *urlBaseStr = urlBaseArr.firstObject;
NSString *urlNeedDecode = urlBaseArr.lastObject;
// 2.將截取以后的Str,做一下URLDecode,方便我們處理數(shù)據(jù)
NSMutableString *afterDecodeStr = [NSMutableString stringWithString:[urlNeedDecode stringByURLDecode]];
// 3.替換里面的默認(rèn)Scheme為自己的Scheme
NSString *afterHandleStr = [afterDecodeStr stringByReplacingOccurrencesOfString:@"alipays" withString:@"ioshllcourselive"];
// 4.然后把處理后的,和最開始切割的做下拼接,就得到了最終的字符串
NSString *finalStr = [NSString stringWithFormat:@"%@?%@", urlBaseStr, [afterHandleStr stringByURLEncode]];
NSURL *toPaymentUrl = [NSURL URLWithString:finalStr];
if ([[UIApplication sharedApplication] canOpenURL:toPaymentUrl]) {
if (@available(iOS 10.0, *)) {
[[UIApplication sharedApplication] openURL:toPaymentUrl options:@{} completionHandler:nil];
} else {
[[UIApplication sharedApplication] openURL:toPaymentUrl];
}
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (self.redirectUrl) {
self.redirectUrl = [self.redirectUrl stringByURLDecode];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:self.redirectUrl] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:60.0];
[webView loadRequest:request];
self.redirectUrl = nil;
}
});
// 2.這里告訴頁面不走了 -_-
return NO;
} else if (alipayRange.location != NSNotFound) {
self.redirectUrl = [[decodeAlipayUrl getUrlValueWithKey:@"return_url"] stringByURLDecode];
}
3.1.2 Xcode 配置
Info.plist配置
<key>LSApplicationQueriesSchemes</key>
<array>
<string>wechat</string>
<string>weixin</string>
</array>
如下圖所示

4.遇到的坑
商家參數(shù)格式有誤,請(qǐng)聯(lián)系商家解決

出現(xiàn)這種情況可能是如下兩個(gè)原因:
- 沒有設(shè)置Referer
-
設(shè)置的域名不可行,在開發(fā)者后臺(tái)沒有配置
解決問題的思路:查看是否沒有設(shè)置Referer,和開發(fā)者后臺(tái)配置的域名是否生效
支付之后沒有返回APP
出現(xiàn)這種情況可能是如下兩個(gè)原因:
- Referer沒有設(shè)置成可靠域名://
-
scheme沒有設(shè)置成可靠域名
解決問題的思路:檢查Referer和scheme
多APP的問題
出現(xiàn)這種情況可能是如下原因:
一個(gè)公司多個(gè)app,而一個(gè)用戶安裝了多個(gè)同一個(gè)公司的app,如果使用同一個(gè)安全域名的話,那么在支付回調(diào)之后就會(huì)出現(xiàn)跳轉(zhuǎn)錯(cuò)亂的問題
解決問題思路:如果你有兩款以上APP,比如(App1,App2,App3),并都把referer設(shè)置成pay-test.61info.cn,scheme 設(shè)置成pay-test.61info.cn,那么用戶只如果只安裝一臺(tái)APP1,此時(shí)支付能夠成功,并能轉(zhuǎn)回原APP1,一點(diǎn)問題都沒有。如果通過安裝了App1,App2,就會(huì)發(fā)現(xiàn)支付能夠成功,但有可能不能跳回原來的App,嚴(yán)重影響了用戶體驗(yàn),所以想到的第一個(gè)解決方案,就是更改referer與scheme
| APP | APP1 | APP2 | APP3 |
|---|---|---|---|
| referer | pay-test.61inf.cn/App1:// | pay-test.61inf.cn/App2:// | pay-test.61inf.cn/App3:// |
| scheme | test.61inf.cn/App1:// | test.61inf.cn/App2:// | test.61inf.cn/App3:// |
經(jīng)過測試,發(fā)現(xiàn)根本不起任何作用,但是想深入一層,微信不至于不給用戶通道呀,登錄到開發(fā)者后臺(tái)觀察一下:

可以看到上面是可以配置5個(gè)支付域名的,咨詢運(yùn)營中心的同事,目前我們配置的是一個(gè)頂級(jí)域名61info.cn,既然這樣的話,我們能否自己偽造一個(gè)假域名來處理呢,經(jīng)過更改變成如下處理方式:
| APP | APP1 | APP2 | APP3 |
|---|---|---|---|
| referer | app1.61info.cn:// | app2.61info.cn:// | app3.61info.cn:// |
| scheme | app1.61info.cn | app2.61info.cn | app3.61info.cn |
通過postman 來驗(yàn)證一下:
-
能通過驗(yàn)證
postman驗(yàn)證.jpg -
不能通過驗(yàn)證
postman驗(yàn)證不通過.jpg
通過測試結(jié)果發(fā)現(xiàn),這樣設(shè)置,可以完美達(dá)到我們的交互要求!

