ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

H5页面不集成SDK也能拉起支付宝与微信支付的完整方案

H5页面不集成SDK也能拉起支付宝与微信支付的完整方案 最近又有同行来问我“H5页面嵌在别人的App里宿主不给开SDK我还能不能直接把微信、支付宝支付拉起来”这个问题我在项目里被问过太多次也是很多做Hybrid开发的人卡得最狠的地方。实话说能拉但不是一个“都能”的答案而是两条通道、一堆前置条件、以及反复验证回调的坑。先说结论给急着上线的人支付宝侧用手机网站支付alipay.trade.wap.pay最稳妥纯网页跳转就能完成基本不依赖宿主App微信侧只有“H5支付”MWEB在理论上有机会走通但它要求WebView环境不干扰UA和referer否则报错卡死。公众号支付JSAPI在这种“别人家的App”内嵌场景里基本不用考虑。适合看这篇文章的人做H5业务页的前端、混合应用开发、或者正在接支付通道的独立开发者尤其适合那些“页面不是自己家的原生壳而是嵌到合作方App里”的生态开发场景。下面我把整个链路拆开来讲包含原理、实操代码、参数踩坑、以及宿主WebView环境里的专属问题。没有标准答案但有最佳实践建议顺着读完整再动手。1. 先把问题拆清楚这个需求背后到底卡在哪里1.1 为什么“不集成SDK”会让很多人以为没法做很多开发者的第一反应是支付不都要调App的SDK吗支付宝需要PayKit、微信需要WXPayEntryActivity没有这些原生能力H5能干什么这个认知只对了一半。支付SDK的本质是把“唤起支付客户端并拿到支付结果”这件事封装成了原生接口方便原生开发者在App里调用。但支付平台真正的核心链路并不一定要求SDK必须在场。以支付宝为例手机网站支付方案就是一套纯网页流程你的服务端拿着订单信息向支付宝下单支付宝返回一个收银台URLH5把用户带到这个收银台用户完成支付后支付宝再通过同步跳转和异步通知把结果送回服务端。SDK只是帮你在原生环境里省掉“打开网页收银台”这一步并没有改变支付平台的结算机制和回调机制。生活里可以类比成你开了一家店不一定要自己装一台收银机可以让店员把客人带到商场公共收银台商场柜员负责收钱、打小票之后再跟你对账。H5页面此时就是那个“带路的店员”。SDK是“店里自带的收银机”没有它店照样能收钱只是收银过程交给了商场公共系统。所以“不使用App集成的SDK”并不等于“没法支付”只是你选择的支付产品形态变了从原生SDK方案变成网页支付产品方案。这个认知先转过来后面所有操作才顺理成章。1.2 三条可行路线和一条歪路把可行方案放在一张表里处理问题之前先对着表选型。路线适用的支付产品内嵌宿主App WebView场景关键限制支付宝手机网站支付alipay.trade.wap.pay可用最稳优先选域名需可访问并完成备案校验收银台会跳到支付宝App或支付宝网页微信H5支付MWEB / APIv3 transactions-h5有条件可用非微信浏览器、referer域名必须在白名单、宿主不能改UA微信JSAPI公众号支付JSAPI下单基本不可用需要微信内置浏览器环境、微信公众号网页授权、JS-SDK安全域名所谓的“扫码枪/付款码直连”商家付款码不建议走这种方案本质是线下扫码在线上H5里用容易被风控且体验非常割裂第一次做支付的人容易问“我用一个静态二维码页让用户自己扫算不算拉起支付”严格说这不是H5拉起支付是“把支付工具扔给用户”。在产品上可行但风控角度不推荐尤其在陌生网络环境下容易触发异常交易拦截而且订单状态联动会非常被动。选型的本质逻辑是支付平台会优先信任“它自己托管收银台的网页链路”。支付宝的WAP支付、微信的H5支付都是让用户进入平台自己的收银台页面去完成敏感操作H5只负责“递话”。这条路最顺。2. 支付链路核心概念收银台托管、同步跳转与异步通知2.1 收银台托管你的H5页只是“带路人”理解支付能不能拉起来要先理解支付平台把你的“信任边界”画在了哪里。不管是支付宝WAP支付还是微信H5支付你的H5页面都不会直接处理银行卡、密码、指纹这些敏感信息。流程是用户在你的H5页面点了“去支付”你的服务端向支付平台下单拿到一个收银台地址H5把用户跳转到这个收银台地址用户在收银台完成支付如果装了支付宝/微信App通常会被唤起没装则在平台网页收银台完成支付平台把结果告诉你的服务端。这整个过程中“能否唤起支付App”其实是由支付平台和操作系统共同决定的不是你的H5能控制的。你的H5只能控制“把用户送到哪个门”至于送到之后用户是用支付宝App、还是网页账号密码登录是支付平台在前端环境里做降级处理的。所以做H5支付的人必须接受一个事实你没办法100%保证支付App一定被唤起。手机没有安装支付宝App时支付宝会自动落到网页收银台微信H5支付遇到某些浏览器环境也可能只让你在网页上输密码。这也是为什么“拉起App支付”和“拉起网页收银台”本质上是一件事的两面。2.2 return_url与notify_url谁才是订单结算依据支付平台的回调设计是整套方案里最容易被忽略、却最不能出错的部分。return_url是同步跳转用户在收银台支付完后浏览器会带着支付结果参数GET跳回这个地址。这个东西的定位是“用户体验层”只能用来展示“你回来了”或“支付已完成”这类提示不能作为订单结算的唯一依据。原因很简单用户支付完手滑直接关了收银台页面或者App切换导致浏览器标签被杀同步跳转可能根本不会发生。更极端的情况是用户支付成功后停留在收银台没有触发任何跳转你的页面永远收不到return_url。notify_url是异步通知支付平台在交易状态变化后主动用服务端对服务端的POST请求把你的服务端地址戳一遍。这个通知才是支付平台的“结算存证”。你必须在服务端对异步通知做验签、金额校验、幂等处理再更新订单状态。记住一句话页面可以假参数可以被伪造异步通知也要验签但只有经过验签的异步通知才能真正驱动订单流转。另外提醒一点支付宝的同步跳转是GET请求会附带回参但其中的sign参数在部分老版本SDK里可能会因为URL编码问题被改坏解析的时候一定先做URL解码再做验签。微信H5支付基本没有同步return_url的入参它靠的是支付完成后跳回你预先指定的redirect_url这个页面更像“落地页”是否支付成功同样要以异步通知为准。2.3 为什么不能靠前端判断支付结果很多H5项目把支付结果判断放在前端“监听页面可见性”或者“轮询订单接口”上。这个思路在体验层没问题但业务正确性不能依赖它。试想这个场景用户点击支付支付宝被唤起这时用户去了一趟后台回来时支付流程已经走完手机上的支付宝App被系统回收了进程同步跳转丢失。此时你的H5页面还停留在“支付中”状态怎么处理靠前端是等不到结果的必须由服务端声明“这笔订单已经支付”前端再通过轮询或WebSocket刷新状态。另一个反向场景更危险用户支付完成后网络发生切换前端请求丢失但异步通知已经到了服务端。如果你前端只依赖自己的接口去查状态接口没通就提示“支付失败”用户会重复支付。所以我在项目里都会把前端支付结果页做成“展示性”的真实状态一律以后端查询接口为准。3. 支付宝手机网站支付纯H5拉起支付的主干方案3.1 从头到尾的完整交互流程先给一套已经在生产环境验证过的流程。这套流程对“内嵌在宿主App WebView中的H5”同样适用因为支付宝WAP支付对WebView环境的容忍度比较高。用户在H5页提交订单前端把订单信息发给自己的服务端。服务端校验价格、库存、用户身份生成唯一订单号out_trade_no。服务端调用支付宝接口alipay.trade.wap.pay传入订单金额、商品名、同步跳转地址、异步通知地址。支付宝返回一个收银台URL或返回一组前置参数。服务端把收银台URL返回给H5前端。前端跳转可以是location.href跳转也可以表单POST提交到支付宝收银台。支付宝处理支付若手机装了支付宝App则尝试唤起若没有则加载支付宝网页收银台。支付完成后浏览器被带回return_url同时支付宝服务端向notify_url发送异步通知。服务端验签、校验金额和订单号、处理订单状态。前端通过查询接口刷新结果页。这十步里第8步两个动作是并行且互相独立的这也是很多新手的坑以为同步跳转收到成功参数异步通知就一定也到了其实两边完全可能一个成功一个延迟。3.2 后端签名与下单参数实操现在动手写代码。后端语言我用Node.js做示例其他语言思路完全一致。强烈不建议手写加签逻辑直接用官方SDKSDK内部把参数排序、拼接、加签、请求都封装好了不容易错。安装SDKnpm install alipay-sdk初始化并创建WAP支付订单const fs require(fs); const AlipaySdk require(alipay-sdk).default; const alipaySdk new AlipaySdk({ appId: 你的支付宝应用APPID, privateKey: fs.readFileSync(./private-key.pem, utf8), // 正式环境用支付宝公钥沙箱环境必须用沙箱的公钥别混 alipayPublicKey: fs.readFileSync(./alipay-public-key.pem, utf8), // 沙箱环境请替换为 https://openapi.alipaydev.com/gateway.do gateway: https://openapi.alipay.com/gateway.do, }); const orderNo H5 Date.now() Math.random().toString().slice(2, 8); // 返回的是支付宝收银台跳转URL也可以用于表单自动提交 const alipayPage await alipaySdk.exec(alipay.trade.wap.pay, { notifyUrl: https://yourdomain.com/api/alipay/notify, returnUrl: https://yourdomain.com/order/payResult, bizContent: { out_trade_no: orderNo, total_amount: 0.01, // 单位是元字符串类型 subject: 测试商品, product_code: QUICK_WAP_WAY, quit_url: https://yourdomain.com/order/cancel }, }); // 把 alipayPage 返回给前端这里几个参数一旦配错结果非常隐蔽。total_amount是字符串而且单位是元不是分。很多从微信支付转过来的同学在这里单位踩坑。subject是中文的话官方SDK内部会自动做URL编码如果你手写拼接跳转地址一定要记得encodeURIComponent。quit_url是用户主动取消时返回的地址它在支付宝App环境下通常表现正常但在部分浏览器环境下可能不会触发所以不能依赖它作为“取消订单”的唯一入口。顺带提一句appId在沙箱和正式环境不是同一个创建应用的时候看清楚环境标签。3.3 前端拉起收银台的两种写法后端返回alipayPage后前端有两种方式把用户送过去。第一种直接跳转// 适用于参数较少、URL较短的情况最直观 window.location.href alipayPage;第二种表单POST自动提交!-- 当参数太长时部分老版本Android WebView对超长URL支持不好表单提交更稳 -- form idalipayForm actionhttps://openapi.alipay.com/gateway.do methodPOST input typehidden nameapp_id value... input typehidden namemethod valuealipay.trade.wap.pay input typehidden namecharset valueutf-8 input typehidden namesign_type valueRSA2 input typehidden namesign value... input typehidden nametimestamp value... input typehidden nameversion value1.0 input typehidden namenotify_url valuehttps://yourdomain.com/api/alipay/notify input typehidden namereturn_url valuehttps://yourdomain.com/order/payResult input typehidden namebiz_content value... /form script document.getElementById(alipayForm).submit(); /script表单方式在宿主App的WebView里往往更可靠因为有些WebView对location.href的外跳做了一层自定义拦截而POST表单通常只会被当作正常的导航请求。如果宿主WebView连POST都拦截那就只能走后面的“降级到浏览器”方案了。3.4 必须守着的那几个坑这块都是生产环境踩出来的经验单独列一下。第一沙箱环境和正式环境的gateway地址不同。沙箱是https://openapi.alipaydev.com/gateway.do正式是https://openapi.alipay.com/gateway.do。最坑的是公钥也分环境沙箱应用的后台里下载的支付宝公钥不能往正式环境里填否则通知验签阶段会直接挂掉。第二同步跳转的参数不可信但必须验签。return_url里会带着out_trade_no、trade_no、total_amount等参数这些是可以伪造的。如果你在页面里直接展示“支付成功”至少要做一次服务端验签并查询订单状态再做展示。哪怕只是展示层也不要把未经校验的total_amount直接显示给用户。第三notify_url必须是一台公网可访问的地址且不能被重定向。有些同学在本地联调时用内网穿透工具临时调试没问题但正式环境一定要确保域名解析正常、https证书有效。支付宝异步通知会重试如果地址临时挂了后面还有机会补发不要慌但也不要长期不处理。第四URL长度约束。手写拼接参数时整条URL不能无限加长。WebView的导航请求在部分机型上有URL长度限制超长会静默失败。这就是我为什么推荐表单POST的原因。4. 微信侧的两条路H5支付与JSAPI支付的适用边界4.1 微信H5支付MWEB到底能不能在别人的App里用微信H5支付逻辑上和支付宝WAP支付类似也是服务端下单、前端跳转到微信收银台。但微信对“发起支付的网页环境”审查严格得多所以在“嵌套在别的App”这个场景里能不能用完全看宿主WebView做了什么。核心条件有三个第一发起支付的浏览器/WebView必须不是微信内置浏览器因为微信内部网页只能用JSAPI或小程序支付第二页面的referer域名必须在微信商户平台配置过“域名授权”或“支付目录”第三页面UA不能是明显被篡改过的特殊环境标识比如某些超级App会把WebView的UA改成自定义名称微信会直接拒掉。用新版APIv3下单的代码示意const result await wxpay.transactions_h5({ appid: 你的AppID, mchid: 你的商户号, description: 测试商品, out_trade_no: orderNo, notify_url: https://yourdomain.com/api/wxpay/notify, amount: { total: 1 }, // 单位是分和支付宝不同 scene_info: { payer_client_ip: userRealIp, h5_info: { type: Wap, wap_url: https://yourdomain.com, wap_name: 某某商城 } } }); // 返回 result.h5_url也就是微信收银台中间页地址 const redirectUrl encodeURIComponent(https://yourdomain.com/order/wxpayResult); window.location.href result.h5_url redirect_url redirectUrl;这里必须注意三件事。第一payer_client_ip要传用户的真实IP不是服务器IP。第二amount.total单位是分整数类型别把元和分搞混。第三h5_url每次下单都是新的有效期短服务端返回后前端要立刻跳转不要把它存起来第二天再用。跳转之后用户看到的通常是wx.tenpay.com下的一个中间页微信会根据客户端类型决定唤起微信支付App还是展示网页收银流程。支付完成后页面会尝试带你回到redirect_url这个地址只能算落地页订单是否成功还是要等异步通知。4.2 JSAPI公众号支付为什么在这里基本不可用有些开发者在H5页面里看到“微信支付”几个字就想着先把JSAPI调通再说。JSAPI支付需要三个条件用户的浏览器是微信内置浏览器能执行微信注入的WeixinJSBridge已经完成网页授权并拿到openid页面域名在公众号的JS接口安全域名和支付目录里。这三个条件在“别人家的 App 里的 H5”这个场景里几乎一个都不满足。具体说你在宿主App的WebView里打开网页这个WebView没有微信注入的JS接口WeixinJSBridge对象根本不存在。就算你拿到了用户的openid也无法唤起微信支付的确认页。所以别在这个方向上耗费精力如果你发现页面确实是在微信内置浏览器里打开的那就说明用户本身已经身处微信环境这时候要换产品形态了不是H5支付能解决的。4.3 被问最多的边界场景微信内、小程序内、企业微信内顺手把边界场景理清楚很多人是因为页面同时被多个渠道加载导致支付方案左右矛盾。用户在微信内打开H5页面想做微信支付那要对接的是公众号支付JSAPI。用户在小程序内不能直接做传统H5支付要用小程序的支付组件钱走虚拟支付规则尤其涉及苹果还有IAP结算的问题。用户在企业微信内加载H5支付需要看企业微信是否在白名单以及支付产品的兼容情况通常限制更多我会在需求评审时直接建议业务方换渠道。这些场景和你当前“嵌套在其他App的WebView里”不是一个问题。所以在接微信支付前先让产品确认用户到底从哪里进来如果用户会从多种渠道进来正确的做法是不同入口走不同支付方案而不是一套H5支付通吃。5. 回调与订单状态管理的连锁反应5.1 异步通知是订单结算的唯一信源支付宝和微信在异步通知处理上略有差异但结论一致服务端异步通知是驱动订单状态流转的唯一信源。所有“前端显示支付成功”“后台标记已支付”“触发发货”等动作都要在异步通知验签通过之后再去执行。支付宝通知是普通POST表单格式微信APIv3的通知则是加密JSONbody里的resource字段要先做AES-GCM解密才能拿到transaction_id和out_trade_no。这里注意微信APIv3签名验证和支付宝不同支付宝用公钥验签相对直观微信v3需要对Wechatpay-Timestamp、Wechatpay-Nonce、Wechatpay-Signature这几个header做验签新手很容易在这里绕晕直接看官方示例代码或使用官方SDK的notify处理函数。5.2 验签、幂等、金额校验一个都不能少处理异步通知有三件事是必须做的漏一个都可能埋雷。第一验签。伪造异步通知的成本很低如果有人知道你的回调地址构造一个“支付成功”的POST请求订单就会被误发放。服务端必须用支付平台公钥验签验签不通过直接丢弃并返回错误。第二幂等。支付平台的通知会重试同一笔订单的“成功通知”可能收到好几次。如果每次都执行业务逻辑会导致重复发货、重复加余额。标准做法是收到通知后先查订单当前状态如果已经是“已支付”直接返回成功不再执行后续动作。第三金额校验。异步通知里的total_amount或amount.total要和订单表里的金额完全一致。有些攻击场景是请求被篡改金额改小或改大验签拦不住的时候就能被金额校验拦下来。我在生产里还遇到过商户后台配置和订单币种不一致导致跨境订单被错误回调的场景金额校验属于家常便饭但必须做。下面是一个简化的状态机写法供参考订单状态CREATE已创建 → PAYING支付中 → PAID已支付 → DELIVERED已发货 伪代码 if 通知验签不通过: return error if 通知中的订单号不存在: return error if 订单状态已经是 PAID: return success if 通知金额 ! 订单金额: return error update 订单状态为 PAID 执行发货/发放等业务动作 return success5.3 掉单问题排查清单我整理了一张在实际项目里反复用到的掉单排查表。现象可能原因排查方向用户付了款订单还是待支付异步通知没有收到或幂等判断被反复拦截看服务端日志有没有通知请求看回调地址是否可访问看订单状态机是否提前判断已支付页面跳回结果页显示未支付实际已支付前端拿同步跳转结果当最终状态改为服务端查询接口异步通知为准支付宝异步通知验签失败配置的是沙箱公钥或串了公钥检查后台公钥是否与当前环境一致微信通知报“解密失败”APIv3密钥与证书不匹配或加了无关字符检查APIV3Key是否配置正确证书序列号是否匹配支付成功但金额校验失败单位混淆元/分不一致统一在服务端将回调金额与订单金额做精确比对掉单问题的本质是回调链路某一环断了。排查时不要盯前端先看服务端日志确认有没有收到支付平台的请求。如果完全没有收到再看回调地址和防火墙如果收到了但没处理成功再看验签和幂等逻辑。6. 嵌套宿主App的专属避坑与调试方式6.1 宿主WebView哪些改造会直接干死支付H5嵌进别人家的App等于把命运交给了对方的WebView实现。我见过太多“在浏览器里好好的一嵌进去就不行”的支付问题。常见的原因就几类提前识别比事后排查强。第一UA被改写。有些超级App会在WebView的UA尾部追加自己的平台标识这本身通常不影响支付宝但会影响微信H5支付的判断。遇到微信侧报“当前页面环境不允许支付”时优先怀疑UA。H5可以在页面里打印navigator.userAgent和正常浏览器UA做对比。第二referer被改或丢失。微信H5支付依赖referer白名单校验如果宿主App的WebView在发起请求时会把referer置空或改成App内部的固定值微信侧基本直接挂。这个在H5侧几乎没有绕过的办法只能让宿主App配合去掉改写逻辑。第三外跳拦截。部分WebView把“跳转到外部App”的行为看成危险操作会弹出确认框甚至直接拦截。你在H5里执行location.href alipayPage如果是支付宝的alipays://协议或https://地址被拦截表现就是点击按钮没反应。这种情况需要宿主App在原生侧的WebViewClient.shouldOverrideUrlLoading里放行H5侧无能为力只能做好降级提示。第四本地文件域。如果宿主App是用file://协议加载H5页面的很多浏览器的安全限制会导致支付跳转失败。正确做法是宿主用https://协议加载或者至少配置一个虚拟域名指向本地资源。6.2 页面打不开、跳转被拦时的调试步骤真机调试时我给H5页面预埋一套轻量调试方案避免每次都要连电脑看日志。在页面里直接集成vConsole这个库体积不大生产环境记得先关闭。script srchttps://yourcdn.example.com/vconsole.min.js/script script // 只有测试模式才初始化 if (location.href.indexOf(debug1) -1) { new VConsole(); } /script然后按下面顺序排查。第一步打开H5页面点击支付按钮看vConsole有没有打印“收到收银台URL”的日志。如果这步就卡住问题在服务端下单接口。第二步执行跳转时观察vConsole的Network面板里有没有出现openapi.alipay.com或wx.tenpay.com的请求。如果没有说明WebView拦了外跳。如果有但页面白屏或返回说明收银台加载过程中出了问题再去抓收银台页面的报错。第三步配合抓包工具看HTTPS请求比如Charles或whistle。注意抓包时手机会把证书信任给代理生产环境不要长时间开着调试完就关。抓包重点看三个值referer、User-Agent、Cookie。这三个值就是支付平台判断环境的主要依据。第四步如果在WebView里实在无法完成做一个降级入口。给用户一个“在浏览器中打开”的按钮通过通用跳转接口唤起系统浏览器继续支付。这个降级入口能救一大半“宿主环境不配合”的场景。6.3 兜底方案和合规提醒有时候支付平台的两条路都被宿主环境卡死那就只剩两条现实路径要么跟宿主App协商在它的原生壳里加一个支付中转要么把用户引导进系统浏览器完成支付。引导到浏览器不算坏事很多大型电商H5在App内也保留了这个降级通道因为收银台的强环境校验在原生浏览器里成功率更高。我不推荐的另一条路是接第三方“聚合支付”或来路不明的第四方支付工具。这类服务通常打着低费率、快速进件的旗号实际资金流向不受监管一旦平台跑路商户款追不回来还涉及严重的合规风险。做技术的人不要在支付通道上省成本能用官方通道就用官方通道哪怕多写一点适配逻辑。7. 实操总结与我的踩坑手记最后分享一段实际经历。去年有个合作项目H5页面嵌在一款办公App里对方只给了一个WebView容器原生SDK集成要排期三个季度业务等不了。我最后选的就是支付宝手机网站支付把服务端下单、前端表单POST跳转、异步通知验签三件事做通之后整条支付链路就稳了。微信H5支付当时也没有放弃但因为宿主App的WebView会在请求里注入自定义UA微信侧一直报“当前页面环境不允许支付”最后靠引导用户到系统浏览器打开才解决。这段经历里有三个心得供后来人参考。第一先接服务端异步通知再接前端页面顺序反了你会被假回调弄得焦头烂额。第二能做支付宝就优先支付宝它对WebView环境的包容度确实比微信好很多。第三支付联调要用真机不要只在浏览器模拟器里确认因为宿主App的WebView策略只能真机复现桌面浏览器模拟不出来。如果你正在做同样的需求别被“不集成SDK”这句话吓住HTTP和HTTPS本身就能完成大部分工作。把收银台托管、同步跳转、异步通知、验签幂等这四个概念吃透再对照自己的宿主环境一项项确认这个支付通道是一定能跑通的。
返回列表