本文转载自微信公众号《程序通识》,作者楼下小黑哥。转载请联系项目总监公众号。现在的生活离不开微信/支付宝电子支付。平时出去吃饭或者逛街,只需要带个手机就可以解决一切,以至于好久没碰过真机了。有一次出去吃饭,排队付钱,等的过程很无聊。正想掏出手机去打野,却发现这里连网都上不了。看手机,明明信号满满的,却只是显示没有连上网络。苹果手机用户之痛,谁用过就知道。画外音:好想diss一下英特尔基带的iphone,???Net,没办法用支付宝扣钱。正在想的时候,我已经在排队了。不管发生什么,我都会先试试支付宝。如果它不起作用,我不会吃它。没想到,当商家在支付宝上使用二维码扫码支付时,我的手机虽然没有弹出支付成功的页面,但是商家却显示支付成功,并成功打印出收据。过了一会儿,我的手机收到了支付宝扣款短信。因为我最近的工作对都是和微信/支付宝相关的,所以整体支付流程比较清楚,但是对于支付码为什么可以线下支付确实不是很清楚,所以做了一些研究,才有了今天的这篇文章。科普支付方式在说支付码离线原理之前,先给不熟悉支付宝/微信支付方式的同学普及一下两种常见的支付方式。常见的支付方式有微信和支付宝线下支付两种。一是我们打开手机,主动扫描商家提供的密码卡。这种支付方式一般称为主扫码支付(用户主动扫码)。以支付宝为例,支付流程如图所示:图片来自支付宝官网。第二种方式是我们打开手机显示我们的支付码,然后商家使用扫码器等工具获取支付码完成支付。这种支付方式一般被称为扫码支付(用户扫一扫)。以支付宝为例,支付流程如图所示:图片来自支付宝官网。第一种方式,需要在手机APP上扫码,然后弹窗确认支付。这种方式在没有手机网络的情况下是无法完成支付的。所以我们上面说的无网专指支付码支付场景。支付码的支付过程在讲支付码离线支付之前先说一下。我们先来看一下支付代码的整体流程。以超市购物为例,一个支付码的支付信息流如图所示:参考知乎@天顺这个流程商户的后台系统是需要调用支付宝条码支付接口完成支付。“由于商户后台需要通过线上联网与支付宝后台进行通信,支付码离线支付是指客户端没有网络的情况,商户实际上必须实时联网。”支付码接口调用流程如图:来自支付宝官网通过上面两张图,我们对支付码交互流程有了一个整体的认识。支付码的技术方案其实可以分为客户端在线和离线两种情况。下面我们来看看两种方案的具体实现方法。在线代码方案客户端在线代码方案应该比较容易想到。只要登录支付宝/微信,点击支付按钮,客户端就会调用后台系统的应用支付代码接口。后台系统收到请求后生成支付码,然后将支付码与用户的关系保存在数据库中,返回给客户端。客户端只要在有效期内出示支付码即可完成支付,否则二维码将失效。使用这种方案是比较安全的,因为代码每次都是服务端生成的,服务端可以控制幂等性,没有客户端伪造的风险。另外,即使支付码规则需要调整,比如支付码加一位,我们只需要调整服务端代码,客户端不需要升级。“不过,这种方案的缺点也很明显,客户端必须实时在线,没有网络无法获取支付码。”此外,现在部分智能设备已经支持支付宝支付,而这些设备大多不具备联网功能。(如小米手环4),遇到这种情况,使用网上对码解决方案是没办法的。基于这种情况,离线编码方案开始存在。离线代码方案你可能对离线代码比较陌生,但其实仔细观察的话,其实有很多场景都会用到离线代码。比如我以前在黑网吧玩梦幻西游的时候,账号总是被盗号。没办法,花大价钱买了个网易总单。每次登录,除了输入用户名和密码外,还需要输入动态密码。从那以后,账户就很少被盗了。再比如每次网易支付,除了输入银行卡密码外,我们还需要输入网银盾上的动态码,才能完成支付。画外音:这里又要吐槽一下,网银盾以前真的不好用,动不动就驱动不兼容。还记得我用网银给黄钻充值,结果一下午都没成功--!当然,以上这些可能都是老古董了,很多人可能都没用过。现在比较流行的“手机验证器APP”,比如“GoogleAuthenticator”等,这种tokenizer动态生成一次性密码(“OTP,One-timePassword”),可以防止密码带来的安全隐患盗窃。其实支付码离线方案的技术原型就是基于这个方案,那么我们先来看看基于GoogleAuthenticator的原理。动态密码技术原理首先,如果我们需要使用“GoogleAuthenticator”,就需要在网站上开启二次验证功能。以谷歌账号为例,在设置两步验证的地方可以找到如下设置:当我们点击设置时,会弹出一个二维码,然后使用“谷歌验证器”APP进行扫描要绑定的代码。我们绑定后,“GoogleAuthenticator”APP会显示动态验证码。我们来分析一下这个二维码,它对应的字符串是:otpauth://totp/Google%3Ayourname@gmail.com?secret=xxxx&issuer=Google上面的字符串中,最重要的就是这串秘钥,这是一个由“BASE32”编码的字符串。实际使用时,需要使用“BASE32”进行解码。伪代码如下:original_secret=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxsecret=BASE32_DECODE(TO_UPPERCASE(REMOVE_SPACES(original_secret)))》这个密钥客户端会和服务端同时保存一份,两端用相同的算法计算到比较动态代码的正确性。”我们以客户端为例,生成一个动态码,首先需要经过一个签名函数,这里**GoogleAuthenticator**采用的是“HMAC-SHA1”,是一种基于哈希的消息认证码,还有一个可以使用相对安全的单向哈希函数(如SHA1)来生成签名,签名函数的伪代码如下:hmac=SHA1(secret+SHA1(secret+input))上述函数中,input使用当前时间除以30的值。input=CURRENT_UNIX_TIME()/30这里的时间作为一个动态可变参数,从而可以不断生成动态代码。”另外,这里除以30是为了验证codea30-secondvalidityperiod."这样对于用户输入来说,有足够的时间准备输入动态代码。另外,客户端和服务器之间可能存在时间差。间隔可以屏蔽这个大概率的差异。画外音:这个有效时间其实很贴心。如果时间更长,安全性会很差。如果较短,则用户体验差,不易进入和准备。经过“HMAC-SHA1”签名函数,我们得到一个长度为40的字符串,我们需要将其转换成6位数字供用户输入。处理的伪代码如下:four_bytes=hmac[LAST_BYTE(hmac):LAST_BYTE(hmac)+4]large_integer=INT(four_bytes)small_integer=large_integer%1,000,000完整的算法伪代码如下:original_secret=xxxxxxxxxxxxxxxxxxxxxREMOVEUPPPER_DECODE(original_secret)))input=CURRENT_UNIX_TIME()/30hmac=SHA1(secret+SHA1(secret+input))four_bytes=hmac[LAST_BYTE(hmac):LAST_BYTE(hmac)+4]large_integer=INT(four_bytes)small_integer=large_integer%1,000,0当客户端上传动态码到服务器,服务器查询数据库得到用户对应的key,然后用相同的算法处理生成动态码,最后比对客户端上传的动态码是否一致与服务器生成的。支付码离线解决方案上面我们了解了动态密码的实现,支付码的生成原理大致相同。而支付码离线方案采用动态密钥(“全球唯一”),定期请求服务器更改密钥,保证更高的安全性。另外,在一次性动态口令方案中,双方需要基于同一个秘钥,所以服务器端需要清楚地知道“背后的正确用户”。以上面的登录场景为例,在登录过程中输入用户名,服务器可以根据此在数据库中查询对应的key。但是在支付码的支付场景下,支付过程只需要传递一个支付码,对应的用户就可以扣款了。别想了,这个支付码中的一串数字,肯定包含了对应的用户信息。因此,支付码对应的算法会比动态码复杂,从而有效保证安全性。看到这里,不知道你是不是迫不及待想要了解这个算法呢?哈哈,开玩笑的,这个算法怎么可能被我们掌握。支付宝的核心算法我们不得而知,但从别人公开的设计方案中可以窥见一斑。在这里,小黑哥给大家提供一个网友@反方的钟解答的离线二维码实现方法,给大家看看。出自:https://www.zhihu.com/question/49811134/answer/135886638支付码离线码的缺点最后我们来看一下支付码离线方案的缺点:第一,算法调整不灵活,如果相关算法较大调整,客户端可能需要升级,服务端也需要兼容这期间新旧算法生成的支付码。第二,安全问题。一般情况下,相关密钥是普通用户拿不到的,但也经不起有心人。他们可能利用恶意程序通过获取手机用户root权限或越狱手机获取密钥,然后随意生成支付码。看到这里,大家可能会担心自己钱包的安全。但在这一点上,我认为不必担心太多。蚂蚁集团的高手实在是太多了。第三个数据碰撞问题,计算出用户A生成的支付码与用户B一致,这个和Hash算法是一样的。再优秀的算法,也有可能产生等量的Hash值。这导致用户A的钱被扣了,但最终用户B被扣了。这样一来,确实是乌龙了。对于用户B,钱被莫名其妙地扣掉了。不过你不用担心,我觉得这种事情还是比中彩票低,所以不要太担心这种事情。就算误扣了,也不用担心,这么大的额度,支付宝肯定会亏本给客户的。最后总结一下,我们一般都是使用支付码来支付。其实原理就是商户获取到我们手机APP的支付码(“其实是一串数字”),然后在后台调用支付宝支付接口完成扣款。在这个过程中,商家端的后台程序必须要联网,但是对于我们客户端来说,可以是在线的,也可以是离线的。如果我们的客户端在线,我们可以通过服务器将支付码发送给客户端。这种方式比较安全灵活,但是在弱网环境下,体验很差。如果我们的客户端没有联网,那么客户端通过一定的算法生成支付码,服务端接收后通过相关验证,确认是哪个用户,确认码的有效性,完成扣款。这种方式适用于客户端没有网络的情况,但相对不灵活,安全性较低。嘿嘿,明白了原理,是不是觉得挺有意思的~
