在支付宝扫码支付开发中遇到的一个由很简单的原因引起的但是很难排查出来的错误

背景是这样的,支付宝有一个扫码支付接口。网站可以通过支付宝 API 申请一个二维码,二维码包含了商品信息和价格,用户可以使用手机支付宝扫描这个二维码,然后在手机上直接完成支付。

整个支付流程是这样的:

  1. 网站向支付宝提交信息,得到一个二维码
  2. 用户使用手机支付宝扫描二维码,并确认商品信息准备支付
  3. 支付宝调用网站提供的 return_url,通知网站用户已经扫码成功,并准备支付
  4. 用户进行支付
  5. 支付宝调用网站提供的 notify_url 将支付结果通知网站

问题出在第三歩上,按照正常情况,用户在支付宝上点击确认支付之后,支付宝应该会访问网站的 return_url。我在测试时,当点击支付宝上的确认支付按钮后,支付宝却弹出了这样的错误信息:

Screenshot_2015-05-26-14-21-11-public

接着,我查看了服务器日志,发现支付宝并没有调用网站的 return_url 页面。

于是我只能求助支付宝的技术支持了。

在这里不得不吐槽一下支付宝的技术支持,我前后共遇到了 3 位技术支持人员,但是前两位都不怎么靠谱,只有第三位靠谱,同时也是第三位技术支持给出的建议才帮助我找到了问题所在。第一位技术支持的水平我不想多说了,似乎他根本不知道我在说什么,总是顾左右而言它。

举例来说,我把网站向支付宝发送的经过了 URL 编码的完整请求发了过去,但是他却表示这段内容是加密的我看不到啊,是的,我这段内容确实包含了一个 MD5 参数,但是 MD5 算法不是用来加密的,而是用来做签名的好不好,请搞清楚加密和签名的区别。再者,难道你看到一大堆百分号还认不出这内容明显是经过了 URL 编码的吗?

最后,我不得不手动将这段内容进行了 URL 解码,然后再给他发过去。好,这回对方不抱怨内容是加密的看不到了,他开始抱怨你这样把内容分成这么多段发过来我怎么看(支付宝的聊天窗口每次只能发送 200 字节的内容,所以我只能分段发送,并且我也解释了分段发送的原因)。好吧,那我只能把这段内容截图了,这回总该没有没有什么借口了吧。

是的,这回这位技术支持终于没有借口了,于是他直接说:你的参数有问题,用我给你的这段示范参数试试看。事实上,我在寻求技术支持之前,就已经对照文档检查过了我的参数内容,要有问题的话我就不用寻求技术支持了。我一再强调我已经检查过参数没有问题,并要求对方指出是哪个参数有问题,但对方一直避开我的问题,而是一直要求我用他给出的示范参数来试试看。

但是,他给出的示范参数和我的业务有很大差别,在我的业务中,我没有商品详情和规格,但他给出的示范参数中包含了商品详情和规格,这样就算使用示范参数能够成功支付,对解决我的问题估计帮助也不大。

在和这位技术支持沟通的过程中,我多次提到我没有收到支付宝向 return_url 发送的请求,这才是问题的关键所在,但对方似乎不知道我在说什么,并没有向这方面展开调查的意图。不过好在使用他给我的示范参数进行测试时,我总算得到了支付宝给出的更多的错误提示:

Screenshot_2015-05-26-15-06-58-public

看来支付宝确实是在访问 return_url 的时候遇到了问题,但是遇到的是什么问题呢?我可以确认我的 return_url 是工作正常的,那么应该是支付宝那边出现问题的可能性比较大。

另外从这里还可以看出另一些问题,访问相同的支付宝接口,提交了不同的参数,竟然会走到似乎两个不同的业务逻辑中(第一张图中的错误提示没有包含详细的错误信息,第二张图中的错误提示包含了更多的错误信息),看来支付宝的内部代码应该是不够规整的。

在这之后,联系到了第三位技术支持,总算遇到一个比较靠谱的技术支持了。不像前两位技术支持,当我把问题重新描述一遍之后,这位技术支持就立刻想到去查询支付宝服务器的日志,并迅速给了我反馈:支付宝的服务器确实发起了对 return_url 的请求,不过由于日志信息有限,他也没法查询到支付宝请求 return_url 之后发生了什么。他建议我检查网络上是否有防火墙,同时换用 HTTP 进行测试,因为 HTTPS 握手可能有问题。

我能够确认网络路径上没有任何防火墙或类似的东西拦截了支付宝的请求,那么现在只能使用 HTTP 进行测试了。令人惊奇的是,将 return_url 的地址换成 HTTP 之后,问题立刻就解决了。那么,看来问题很有可能出在 HTTP 握手上了。但是具体是什么原因呢?我们的网站使用了商业的 SSL 证书,在浏览器中进行测试的时候也是一切顺利,会是什么原因导致握手失败呢?

看来我需要抓包。

其实在第三位技术支持提到可能是 HTTPS 握手出问题的时候,我就想到可能需要抓包了,于是顺便询问了支付宝用于访问 return_url 的服务器的 IP 地址。虽然因为权限原因,技术支持没法查询到服务器的 IP 地址,不过他给出的答复是迅速、明了而专业的,这点值得称赞(在和前两位所谓的技术支持扯皮了一周后,我已经做好随时看到技术支持一本正经地胡说八道的准备了)。

好在刚刚通过 HTTP 测试的时候,我已经在 Nginx 的日志中看到了支付宝用于请求 return_url 的服务器的 IP 地址,那么我的抓包就可以很顺利地进行了。抓包之后,结果是这样的:

截图_2015-06-01_12-51-18

从 TCP 握手,到 TLS 握手,一切看起来都很正常……等等,SSLv2?啥?支付宝竟然使用 SSLv2 发起请求?要知道,这年头可是连 TLSv1.0 都已经不安全了,支付宝竟然还在使用老掉牙的 SSLv2?也许你要说,支付宝是为了保持向下兼容,嗯,这点我理解,但是我的服务器是支持到 TLSv1.2 的,一个正常的客户端都会首先选择最安全的协议来尝试连接,连接失败的话才降级到更老的协议上。支付宝一上来就直接要求使用 SSLv2,这样做真的没问题吗?

以后我看到新闻报道支付宝遭到严重攻击损失惨重时,我可以不用表示惊讶了,因为现在我已经惊讶过一次了。

接下来我在抓包中看到了 SSL 握手失败的原因:证书不被信任。原因很简单,因为 SSLv2 是不支持 SNI 的,所以 Nginx 只能向支付宝发送默认的 SSL 证书,而这个默认的 SSL 证书并不是我的 API 域名使用的证书。

我觉得让支付宝修改他们的程序首选使用 TLS 协议发起连接可能困难重重,还是我自己修改比较靠谱,毕竟现在这个默认证书是没用的,我只要将我的网站使用证书作为默认证书就可以了。经过一番枯燥的 Nginx 配置文件修改操作后,支付宝扫描支付接口,支付顺利成功。

另外发现一个有趣的东西,支付宝访问 return_url 时发送的 UserAgent 字串是 Jakarta Commons-HttpClient/3.1,而在访问 notify_url 时发送的 UserAgent 字串是 Mozilla/4.0。看来支付宝内部的代码确实比较混乱。

本文发表于 乱七八糟, 技术向,并添加了 , , , , , 标记。保存永久链接到书签。

发表评论

电子邮件地址不会被公开。