
微信小程序要接支付宝沙箱支付第一波劝退你的往往不是签名算法而是标题里“http请求”这四个字。很多同学拿着支付宝官方的 OpenAPI 文档准备在小程序里直接wx.request沙箱网关然后被域名校验、签名、回调轮番教育最后卡在“为什么我的请求一直报错”这一步。这篇就把链路彻底讲透从沙箱应用申请、RSA2 密钥、后端下单接口到小程序端wx.request怎么调、web-view怎么开收银台再到支付宝异步回调验签每一步的“为什么”都给你拆开。适合正在做小程序商城、想先走通沙箱再上生产的后端或全栈开发者阅读。1. 先理清一条链路小程序为什么不能直接“http请求”支付宝沙箱1.1 这里的“http请求”到底指什么很多人第一次看这类教程会误以为“http请求”是让小程序直接请求支付宝沙箱的网关地址。实际上微信小程序里的wx.request有两个硬性限制第一请求地址只能是 HTTPS第二这个域名必须在小程序后台配置成合法域名而且必须完成 ICP 备案。支付宝沙箱网关openapi-sandbox.dl.alipaydev.com是你没法在小程序后台配的原因很简单——它本身就不是备案域名。所以“http请求支付宝沙箱”这个说法准确的理解应该是小程序端用wx.request请求你自己后端的 HTTP 接口再由后端去和支付宝沙箱网关通信。1.2 微信小程序与支付宝网关之间的三堵墙即使你在开发者工具里勾上了“不校验合法域名”真正把请求发到支付宝沙箱网关还会撞上另外几堵墙wx.request的 Content-Type 和响应格式限制支付宝网关要求application/x-www-form-urlencoded或表单提交返回的是表单 HTML 或 Gateway 响应。小程序直接解析这类响应特别别扭。签名不能暴露在前端支付宝接口签名用的是你的应用私钥这个东西如果放在小程序里等于把支付安全直接送给别人。微信小程序无法唤起支付宝 App支付宝官方 SDK 在小程序环境里根本没法用微信也不会允许你跳转到支付宝客户端完成支付。这是生态层面的互斥技术上也绕不过去。1.3 最终选型后端中转 web-view 收银台基于上面这些限制实际可落地的方案基本是统一的小程序端wx.request调用自己后端的“创建订单”接口。后端拿着订单号、金额、商品标题按支付宝开放平台要求拼参数、做 RSA2 签名请求沙箱网关alipay.trade.wap.pay。支付宝沙箱返回一个 H5 收银台地址或一段自动提交表单的 HTML。小程序端用web-view加载这个收银台地址用户在沙箱收银台里输入虚拟买家账号密码完成支付。支付宝异步通知你的后端接口后端验签后把订单状态改成已支付。这套链路里“http请求”就是第一步和第五步真正核心的是后半段跟支付宝网关的签名通信。下面的内容全部围绕这条链路展开。2. 沙箱环境准备应用、密钥、虚拟买家一个都不能少2.1 创建沙箱应用打开支付宝开放平台登录后进入“控制台 - 沙箱环境 - 沙箱应用”。首次使用会让你创建一个沙箱应用创建成功后会给你一个 16 位的APP_ID类似2021000123456789这就是后面所有接口调用都要用的应用标识。沙箱应用默认会开通一系列产品能力其中和我们最相关的两个手机网站支付对应alipay.trade.wap.pay适合 web-view 加载 H5 收银台。电脑网站支付对应alipay.trade.page.pay适合 PC 端小程序里用不到但沙箱里顺手开着也没问题。这里要特别注意沙箱环境的入口和个人账号体系是独立的。你在沙箱控制台看到的APP_ID、网关地址、密钥全部只能在沙箱环境里用不要拿生产环境的支付宝开放平台账号去混淆。2.2 用支付宝密钥工具生成RSA2密钥对支付宝开放平台的接口签名方式推荐 RSA2即 SHA256withRSA。你需要一对密钥应用私钥、应用公钥。生成方式有两种这里说最省事的一种——下载支付宝官方的“支付宝开放平台密钥工具”。打开工具后选择“生成密钥”会自动生成应用私钥和应用公钥格式上有 PKCS1 和 PKCS8 两种选择。如果你是 Java 后端选 PKCS8如果你用 Go也建议选 PKCS8Go 标准库解析 PKCS8 更顺手。密钥工具生成的内容长这样应用私钥-----BEGIN PRIVATE KEY-----开头的一长串内容。应用公钥-----BEGIN PUBLIC KEY-----开头的一长串内容。拿到这两串之后回到沙箱应用的“接口加签方式”配置里选择“公钥模式”把应用公钥粘贴进去。保存后支付宝会返回一个支付宝公钥这个和你的应用公钥不是一回事验签时用的是支付宝公钥。2.3 关键配置项速查表配置项沙箱环境取值说明网关地址https://openapi-sandbox.dl.alipaydev.com/gateway.do沙箱专用生产环境是https://openapi.alipay.com/gateway.do应用 ID沙箱控制台里的APP_ID每个沙箱应用独立应用私钥密钥工具生成的私钥下单签名用只能放在后端支付宝公钥在沙箱应用里配置公钥后由支付宝返回异步通知验签用回调地址你后端可公网访问的接口地址在沙箱应用的功能配置里设置2.4 沙箱买家账号的拿法沙箱支付不能用你自己的真实支付宝账号去付款。在沙箱控制台“沙箱账号”区域支付宝会给你一个虚拟买家账号通常是一个手机号同时有登录密码和支付密码。支付时在 H5 收银台里输入这个虚拟账号和密码输入支付密码就能模拟完成一笔真实支付流程但不会产生任何资金流水。如果你发现收银台提示账号不存在或者支付密码错误大概率是拿成真实支付宝账号了。这个细节第一遍跑通时最容易踩后面避坑清单再细说。3. 后端下单接口把小程序的 HTTP 请求翻译成支付宝 OpenAPI 调用3.1 接口设计后端要提供给小程序的接口很简单我按 gin 框架写一个最小可用的示例接口路径叫/api/alipay/create支持 POST JSON。它接收两个参数商品标题subject和支付金额total_amount然后返回给前端一个pay_url。为什么后端不能直接替用户完成支付而是返回一个pay_url因为支付动作需要用户在支付宝收银台输入密码确认这是一个“人”的交互过程不是简单接口调用。后端能做的是生成一个能让用户去支付的地址或表单。3.2 请求支付宝沙箱网关的签名核心步骤支付宝开放平台接口的调用逻辑本质上是把业务参数放进biz_content再把公共参数和biz_content一起做签名最后用 HTTP 请求发到网关。签名是整条链路里最容易出错的一环拆开看其实就四步按参数名 ASCII 码从小到大排序。过滤掉值为空的参数用key1value1key2value2的方式拼接成待签名字符串。用应用私钥对待签名字符串做 SHA256withRSA 签名结果做 Base64 编码。把签名放进请求参数里的sign字段调接口时一起提交。这个签名逻辑几乎适用于所有支付宝 OpenAPI 接口。你可以直接用现成 SDK但搞懂原理后排查问题会快很多。下面代码展示的就是这个手写流程用 Go 实现。3.3 Go 代码实现gin 示例先定义配置项package main import ( crypto crypto/rand crypto/rsa crypto/sha256 crypto/x509 encoding/base64 encoding/json encoding/pem fmt net/url sort strings time github.com/gin-gonic/gin ) var ( appId 2021000123456789 // 沙箱应用APP_ID privateKey -----BEGIN PRIVATE KEY----- ...你的应用私钥... -----END PRIVATE KEY----- alipayPublicKey -----BEGIN PUBLIC KEY----- ...支付宝公钥... -----END PUBLIC KEY----- notifyUrl https://yourdomain.com/api/alipay/notify gatewayUrl https://openapi-sandbox.dl.alipaydev.com/gateway.do )然后是创建订单接口type CreateOrderReq struct { Subject string json:subject TotalAmount string json:total_amount } type CreateOrderResp struct { PayURL string json:pay_url } func main() { r : gin.Default() r.POST(/api/alipay/create, createOrder) r.POST(/api/alipay/notify, alipayNotify) r.Run(:8080) } func createOrder(c *gin.Context) { var req CreateOrderReq if err : c.ShouldBindJSON(req); err ! nil { c.JSON(400, gin.H{message: 参数错误}) return } // 生产环境这里应该先落库生成唯一订单号 orderNo : fmt.Sprintf(T%d, time.Now().UnixNano()/1e6) bizContent, _ : json.Marshal(map[string]string{ out_trade_no: orderNo, total_amount: req.TotalAmount, // 单位是元字符串类型 subject: req.Subject, product_code: QUICK_WAP_WAY, // 手机网站支付固定值 }) params : map[string]string{ app_id: appId, method: alipay.trade.wap.pay, format: JSON, charset: utf-8, sign_type: RSA2, timestamp: time.Now().Format(2006-01-02 15:04:05), version: 1.0, notify_url: notifyUrl, biz_content: string(bizContent), } sign, err : signParams(params, privateKey) if err ! nil { c.JSON(500, gin.H{message: 签名失败}) return } params[sign] sign // 构造支付宝收银台URL query : sortedQueryString(params) payURL : gatewayUrl ? query c.JSON(200, CreateOrderResp{PayURL: payURL}) }3.4 签名与排序细节签名函数单独拎出来看func signParams(params map[string]string, privateKeyStr string) (string, error) { // 1. 过滤空值 filtered : make(map[string]string) for k, v : range params { if v ! { filtered[k] v } } // 2. ASCII码排序 keys : make([]string, 0, len(filtered)) for k : range filtered { keys append(keys, k) } sort.Strings(keys) // 3. 拼接待签名字符串 var sb strings.Builder for i, k : range keys { if i 0 { sb.WriteByte() } sb.WriteString(k) sb.WriteByte() sb.WriteString(filtered[k]) } signStr : sb.String() // 4. 私钥签名 block, _ : pem.Decode([]byte(privateKeyStr)) if block nil { return , fmt.Errorf(private key decode error) } key, err : x509.ParsePKCS8PrivateKey(block.Bytes) if err ! nil { return , err } hashed : sha256.Sum256([]byte(signStr)) sig, err : rsa.SignPKCS1v15(rand.Reader, key.(*rsa.PrivateKey), crypto.SHA256, hashed[:]) if err ! nil { return , err } return base64.StdEncoding.EncodeToString(sig), nil } func sortedQueryString(params map[string]string) string { keys : make([]string, 0, len(params)) for k : range params { keys append(keys, k) } sort.Strings(keys) var sb strings.Builder for i, k : range keys { if i 0 { sb.WriteByte() } sb.WriteString(url.QueryEscape(k)) sb.WriteByte() sb.WriteString(url.QueryEscape(params[k])) } return sb.String() }代码里有两个点要特别说明。第一签名前拼接的字符串里biz_content是完整 JSON 字符串不能做 URL 编码签完名之后拼 URL 时才可以编码。第二timestamp格式必须是2006-01-02 15:04:05时区用本地时区即可。有很多人在这里格式写错导致支付宝返回isv.invalid-signature之类的错误码。sortedQueryString里用url.QueryEscape处理参数值因为biz_content里有 JSON 的特殊字符不转义的话拼到 URL 里会被截断或解析错乱。3.5 返回给小程序的是什么后端返回给小程序的是pay_url也就是一个可以直接在浏览器打开的支付宝收银台地址。小程序端拿到这个地址后不需要再发什么 http 请求直接把地址丢给web-view组件加载即可。用户支付完支付宝会跳转到return_url如果在接口里配了的话或关闭页面。这里注意支付宝手机网站支付在沙箱环境里用 GET 方式访问gateway.do?拼接参数是能弹出收银台的。如果你在测试时发现收银台一直白屏或报错可以退回去用响应表单的方式后端返回一个 HTML 页面里面放一个自动提交的 form 表单web-view加载这个页面地址。不过多数沙箱测试下直接拼 URL 是可行的先从这个简单方案开始。4. 小程序端 wx.request 与 web-view开发调试怎么绕过域名限制4.1 wx.request 合法域名规则小程序里的wx.request在正式环境要求非常严格域名必须已经通过 ICP 备案。域名必须是 HTTPS。域名必须在小程序管理后台“开发管理 - 服务器域名 - request 合法域名”里配置过。这意味着后端开发阶段如果只在本地起一个 8080 服务用http://localhost:8080去请求生产环境是百分之百不可用的。但在本地开发调试阶段为了不卡脖子微信开发者工具提供了一个开关接下来马上说。4.2 调试期勾选“不校验合法域名”在微信开发者工具里点击右上角“详情 - 本地设置”勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”。勾上之后wx.request就能请求http://localhost:8080或者局域网 IP 了。如果你要真机预览手机和电脑连同一个 Wi-Fi把接口地址改成电脑的局域网 IP比如http://192.168.1.100:8080/api/alipay/create同时手机上的小程序也需要是开发版本。沙箱调试阶段这个方案最直接。4.3 小程序端代码示例小程序端逻辑非常简单调用后端接口拿到pay_url然后渲染web-view。Page({ data: { payUrl: }, createOrder() { wx.request({ url: http://localhost:8080/api/alipay/create, method: POST, data: { subject: 沙箱测试商品, total_amount: 0.01 }, success: (res) { if (res.data res.data.pay_url) { this.setData({ payUrl: res.data.pay_url }) } else { wx.showToast({ title: 下单失败, icon: none }) } }, fail: () { wx.showToast({ title: 请求失败请检查后端服务, icon: none }) } }) } })页面里放一个按钮触发createOrder然后条件渲染web-viewbutton bindtapcreateOrder发起沙箱支付/button web-view wx:if{{payUrl}} src{{payUrl}}/web-view这里有个容易踩的细节web-view组件要求src是一个合法的 URL不能直接放一段 HTML 字符串。所以后端设计时一定返回可访问的pay_url而不是把收银台表单 HTML 拼在 JSON 里返回给小程序。4.4 web-view 打开支付宝收银台的注意事项web-view在小程序里本质是一个全屏的浏览器组件但它不是万能的。开发调试阶段你可能会遇到这几个现象开发者工具里web-view加载支付宝沙箱收银台可能显示正常也可能因为沙箱环境不稳定导致白屏。我建议优先在开发者工具里看 Network 面板确认pay_url是不是能正常打开再把同样的地址粘贴到电脑浏览器里访问两边对照。真机上web-view对支付宝 H5 收银台的支持整体而言是偏弱的尤其沙箱环境真机上经常会被支付宝风控拦截提示“当前环境不支持”之类的错误。这不是你代码的 bug而是沙箱环境和 H5 页面自身限制。我能给的可行建议是功能验证优先在开发者工具里做真机验证放到生产环境用真实支付宝账号去测。web-view的业务域名校验在正式环境同样存在。开发调试关闭校验后随便什么 URL 都可以加载但生产环境web-view加载的域名必须在“业务域名”里配置而且需要下载校验文件放到域名根目录。如果你的后端是给你自己的支付中间页用的要把这个域名配进去。5. 异步回调验签支付结果不能只看小程序返回5.1 为什么必须验签用户在小程序里完成支付后你后端怎么知道这笔钱已经付了支付宝的同步返回用户在收银台支付成功后跳转回return_url只是浏览器行为数据可以被伪造也可能因为网络原因丢失绝对不能作为订单状态更新的依据。支付宝真正的“结果通知”是异步通知支付成功或退款成功后支付宝服务器会主动 POST 请求你配置的notify_url通知里带着out_trade_no、trade_no、trade_status、total_amount等参数。因为这个通知可能被第三方恶意构造所以后端第一件事必须是验签——用支付宝公钥验证通知里的sign是否合法。验签通过后再核对订单号和金额才能把订单改成已支付。5.2 异步通知参数与验签流程异步通知的参数和请求签名方式类似区别在于验签用的是支付宝公钥不是应用私钥。参数中要剔除sign和sign_type两个字段后再排序拼接。签名算法还是 RSA2。验签通过之后还要做三项关键校验检查out_trade_no是不是你系统里真实存在的订单号。检查total_amount是不是和下单时一致防止有人篡改金额。检查trade_status只有TRADE_SUCCESS或TRADE_FINISHED才能算支付成功。处理完业务逻辑后接口必须返回纯文本success否则支付宝会按照策略多次重试通知直到你返回success为止。这个细节也是新手容易漏的。5.3 Go 验签代码下面是一个针对 gin 的异步通知处理函数func alipayNotify(c *gin.Context) { // 支付宝异步通知是 x-www-form-urlencoded 格式 c.Request.ParseForm() values : c.Request.PostForm // 1. 先验签 ok, err : verifyNotify(values, alipayPublicKey) if err ! nil || !ok { c.String(200, fail) return } // 2. 校验业务字段 outTradeNo : values.Get(out_trade_no) tradeStatus : values.Get(trade_status) totalAmount : values.Get(total_amount) // 这里要查你自己的订单库比对金额 // if totalAmount ! order.TotalAmount { ... } if tradeStatus ! TRADE_SUCCESS tradeStatus ! TRADE_FINISHED { c.String(200, success) return } // 3. 业务处理修改订单状态 // updateOrderStatus(outTradeNo, paid) c.String(200, success) } func verifyNotify(values url.Values, alipayPublicKeyStr string) (bool, error) { sign : values.Get(sign) values.Del(sign) values.Del(sign_type) keys : make([]string, 0, len(values)) for k : range values { keys append(keys, k) } sort.Strings(keys) var sb strings.Builder for i, k : range keys { vs : values[k] if len(vs) 0 { continue } if i 0 { sb.WriteByte() } sb.WriteString(k) sb.WriteByte() sb.WriteString(vs[0]) } hashed : sha256.Sum256([]byte(sb.String())) decodedSign, _ : base64.StdEncoding.DecodeString(sign) block, _ : pem.Decode([]byte(alipayPublicKeyStr)) if block nil { return false, fmt.Errorf(alipay public key decode error) } pub, err : x509.ParsePKIXPublicKey(block.Bytes) if err ! nil { return false, err } err rsa.VerifyPKCS1v15(pub.(*rsa.PublicKey), crypto.SHA256, hashed[:], decodedSign) return err nil, nil }这里需要提一句支付宝异步通知的参数里有passback_params之类的字段如果值带特殊字符拼接验签时要用原始值不能先做 URL 解码。按上面的代码从PostForm里取出来的就是解码后的值和支付宝官方验签逻辑一致可以正常工作。5.4 回调地址怎么配置公网可访问沙箱应用里要配置“接口加签方式”和“回调地址”其中notify_url必须是你后端能收到的地址。本地开发时一个比较实用的做法是用内网穿透工具把localhost:8080映射成一个临时公网地址比如https://yourname.free.ngrok.cn/api/alipay/notify把这个地址填到沙箱应用的授权回调地址里。我用过几次之后发现支付宝沙箱对回调地址的域名要求比生产环境宽松但要求必须是能公网访问的地址不能是localhost或内网 IP。配置完记得点保存然后在下单参数里也填一样的notify_url两边一致才能收到通知。6. 沙箱调试避坑清单6.1 签名相关问题现象可能原因解决办法返回isv.invalid-signature签名参数拼错、排序不对、私钥格式不对按本文 3.4 节重新检查Go 后端统一用 PKCS8 私钥返回sign check fail验签用的公钥配错确认用的是支付宝公钥不是应用公钥也不是应用私钥返回Required argument missing公共参数缺失检查app_id、method、timestamp、version是否都有签名这一块最容易出问题的其实是换行符。粘贴私钥时前后多了空格、换行或者Begin Private Key和End Private Key没保留pem.Decode解析出来就是nil然后各种报错。建议直接把密钥放在独立配置文件里从文件读取减少手工粘贴带来的问题。6.2 金额/订单号相关total_amount必须是字符串单位是元不能传整数类型的1应该传1.00。很多语言里 JSON 序列化会自动把1.00变成1所以下单接口里我建议统一用字符串接收金额。out_trade_no必须是商户系统唯一订单号同一个订单号不能重复下单。沙箱测试时如果你一直用一个固定订单号第二次请求会报ACQ.TRADE_HAS_SUCCESS之类的错误。我测试时会在前面拼毫秒时间戳来保证唯一。订单号长度不能超过 64 位纯数字和字母组合即可。6.3 沙箱环境特有坑沙箱支付最大的坑就是账号。你必须在沙箱控制台找到虚拟买家账号用这个账号在收银台登录。如果用真实支付宝账号会直接提示无法支付或者账号不存在。另外沙箱买家账号登录时网站一般会要求你用“沙箱工具”或扫码方式登录实际用下来最简单的方式是在收银台页面直接输入沙箱账号和密码不用走扫码。另一个常见问题沙箱收银台在web-view里加载后支付密码输对了也可能一直转圈。这种情况多半是支付宝沙箱环境对 web-view 的兼容性问题。先不要急着改代码把pay_url复制到普通浏览器里重试一次如果普通浏览器能正常支付说明链路没问题问题在 web-view 环境本身。6.4 环境切换时容易漏的项我在从沙箱切生产环境时反复漏过几样东西列成清单提醒你网关地址openapi-sandbox.dl.alipaydev.com换成openapi.alipay.com。APP_ID沙箱应用 ID 换成真实应用 ID。应用私钥换成生产环境密钥最好重新生成一对不要用沙箱的。支付宝公钥换成生产环境应用的支付宝公钥。回调地址沙箱里临时内网穿透地址换成生产 HTTPS 域名。微信小程序后台request合法域名、web-view业务域名都要同步更新。这些配置任何一个没换都会导致支付流程断在半路。而且报错往往不太直观比如支付成功但回调不到、下单时一直签名失败排查起来很费时间。7. 从沙箱切到生产环境需要过的一关又一关7.1 需要替换的配置项生产环境和沙箱环境之间不是改一个开关那么简单。我把常用的参数整理成下面这张对照表每上线一个新项目都会对着它检查配置项沙箱环境生产环境网关地址https://openapi-sandbox.dl.alipaydev.com/gateway.dohttps://openapi.alipay.com/gateway.do应用 ID沙箱应用APP_ID真实应用APP_ID应用私钥沙箱密钥生产密钥妥善保管支付宝公钥沙箱应用配置后返回真实应用配置后返回回调地址允许公网 http/内网穿透必须是 HTTPS 备案域名支付账号沙箱虚拟买家真实支付宝用户7.2 生产环境小程序端的域名与合规检查小程序端的web-view有一个容易被忽略的点业务域名要求文件校验你需要把平台生成的校验文件放到域名根目录确保通过 HTTPS 能访问到。如果你准备用“后端生成收银台 URL小程序web-view直接加载支付宝域名”那还得把支付宝 H5 支付的域名加入业务域名。但支付宝的域名你没法放置校验文件所以这种方案在生产环境基本走不通。实际在生产项目里更稳的路径是后端生成一个属于自己的支付中间页 URL这个中间页通过服务端渲染或前端跳转把用户带到支付宝收银台。这样小程序web-view加载的是你自己已校验的域名域名跳转到支付宝 H5 页面时因为是从 web-view 内部发起的顶层跳转相对不容易被拦截。这个方案需要后端多渲染一个页面代码量不大但能避开一堆线上问题。7.3 一个实测稳定的上线顺序建议按下面顺序做能最大程度减少上线时的“最后一跳”问题先用本文的沙箱链路把开发者工具里的pay_url跑通。在内网穿透环境下把异步回调验签完整跑通确认success返回和订单状态更新正确。申请并配置生产环境的支付宝应用、密钥、回调地址。后端加一个环境配置开关沙箱和生产配置分离避免手误改错。小程序后台配置真实request合法域名和业务域名正式包发布前用体验版完整支付一单。全部通了之后再用真机支付 0.01 元商品做最终验证。我个人强烈建议在第 5、6 步之间留出时间因为真机上的 H5 收银台表现和开发者工具差别很大尤其是 web-view 的兼容性需要提前确认。最后再分享一个我自己的习惯沙箱联调时不要一上来就写完整业务代码先写一个最小可用的下单接口和一个最简单的回调打印手动curl一下pay_url再把支付宝异步通知打出来的所有参数存到日志里。日志里的参数和签名就是最好的排查依据比任何文档都直观。这个思路帮我少走了很多弯路你也可以试试。