作者:Xie, James
支付结果通知看似一个很简单的需求,但是做到一个安全高效可靠的架构和设计也是值得研究和探讨的一个问题。本人通过总结实战中不同的通知机制的研究分析,希望从中找到需求的本质,从而启发我们如何更好的在今后的工作中设计通知机制。
本人分析了四种通知机制,ReviseCheckoutStatus,Instant Payment Notification,Payment Data Transfer和WebHooks方式。然后从安全性,高效性和可靠性三个方面分析了各个通知机制。最后由现行的通知方案中得到一些启发。比如如何权衡安全性高效性和可靠性,如何设计UID使得交易处理匹配更顺畅。
因此,本文即讲了我们在通知机制设计中怎么做的,也讲了为什么采取这种通知机制的设计,希望给自己和读者的今后的工作一定的启发。
在电子商务交易过程中,第三方支付的出现解决了各种银行接口和消息格式不统一的问题,清结算更加灵活方便,手续费更加优惠。
但是与此同时,由于电子商务平台和第三方支付平台分属于不同的网络,一方面支付平台需要知道订单信息作为支付的上下文,以便更好的实现金融风险控制。另一方面电子商务平台也需要知道任何支付结果或者原支付交易的变动,才能在自己的订单系统反应订单状态,避免误解和减少纠纷。
关于订单信息如何安全简单高效的流转到第三方支付公司,本人的另外一篇《基于OAUTH的电子商务支付集成研究与实现》有详细的研究。这里我想讨论的是关于支付结果和变动如何通知回电子商务平台的问题。
对于支付系统的通知需求来说,主要有以下几个方面,
1. 安全是保障
在互联网的环境下,如何验证发送通知是首要考虑的问题,如果不能很好的验证身份,那么伪造支付结果将成为可能。欺诈性销售等一系列问题随之而来。所以安全性是任何支付系统互联首要考虑的问题。
2. 高效是手段
支付交易在大型的电子商务平台上流量会非常大,并发的通知需要我们很好的设计我们的处理机制,否则很可能造成服务器拒绝服务。
3. 可靠是目的
如何保证通知准确可靠的送达电子商务平台,然后更新相应的订单状态是支付结果通知的意义和目的所在,任何的通知都需要尽可能的准确无遗漏的正确处理。否则不仅可能造成电子商务平台本身的财务对账系统账不平,而且会使得用户的购物体验造成影响。
下面举eBay平台为例,分析各种通知机制,以启发我们对于如何建立更好的支付结果通知机制的思考。
对于支付结果来说,最终的目的是更新订单的状态,因此,eBay开放平台中设计了一个接口--- ReviseCheckoutStatus。
我们来看看大致是怎么工作的,见下图2-1,
主要处理流程如下,
1. 每当有一笔支付结果产生或者原交易支付状态变化,PayPal都会调用eBay的ReviseChecoutStatus发送订单的支付状态变化
2. eBay的ReviseCheckoutStatus接口接收到支付结果以后,首先根据orderId匹配原始order,并做相应的明细调整
3. eBay的ReviseCheckoutStatus接口接收到支付结果以后,还会把支付信息发送给Payment系统,更新相应的财务账目。
4. eBay的ReviseCheckoutStatus接口在完成上述操作后把更新结果返回给PayPal
5. PayPal在接收到失败响应后会放到retry队列,安排下次再次发送。
接下来我们来看看ReviseCheckoutStatus接口明细,
<?xml version="1.0" encoding="utf-8"?> <ReviseCheckoutStatusRequest xmlns="urn:ebay:apis:eBLBaseComponents"> <RequesterCredentials><eBayAuthToken>ABC...123</eBayAuthToken><RequestUserId>paypal</RequestUserId> <RequestPassword>******</RequestPassword> </RequesterCredentials> <AdjustmentAmount currencyID="CurrencyCodeType">AmountType (double) </AdjustmentAmount> <AmountPaid currencyID="CurrencyCodeType">AmountType (double) </AmountPaid> <BuyerID>string </BuyerID> <CheckoutStatus>CompleteStatusCodeType </CheckoutStatus> <CODCost currencyID="CurrencyCodeType">AmountType (double) </CODCost> <EncryptedID>string </EncryptedID> <ExternalTransaction>ExternalTransactionType <ExternalTransactionID>string </ExternalTransactionID> <ExternalTransactionTime>dateTime </ExternalTransactionTime> </ExternalTransaction> <InsuranceType>InsuranceSelectedCodeType </InsuranceType> <ItemID>ItemIDType (string) </ItemID> <MultipleSellerPaymentID>string </MultipleSellerPaymentID> <OrderID>string </OrderID> <OrderLineItemID>string </OrderLineItemID> <PaymentMethodUsed>BuyerPaymentMethodCodeType </PaymentMethodUsed> <PaymentStatus>RCSPaymentStatusCodeType </PaymentStatus> <SalesTax currencyID="CurrencyCodeType">AmountType (double) </SalesTax> <ShippingAddress>AddressType </ShippingAddress> <ShippingCost currencyID="CurrencyCodeType">AmountType (double) </ShippingCost> <ShippingIncludedInTax>boolean </ShippingIncludedInTax> <ShippingInsuranceCost currencyID="CurrencyCodeType">AmountType (double) </ShippingInsuranceCost> <ShippingService>token </ShippingService> <TransactionID>string </TransactionID> <!-- Standard Input Fields --> <ErrorLanguage>string </ErrorLanguage> <InvocationID>UUIDType (string) </InvocationID> <MessageID>string </MessageID> <Version>string </Version> <WarningLevel>WarningLevelCodeType </WarningLevel> </ReviseCheckoutStatusRequest>
|
分析上述接口,可知这个接口主要包括两组关键信息,
1. eBay的标识符:ItemId/TransactionId/OrderId,可以匹配回原始订单。
2. PayPal标识符:ExternalTransactionID,可以匹配回原始支付流水,如果不存在就是一笔新的交易流水,这笔交易可能是在做真实支付时连接超时或用户直接在PayPal那里发生了一笔支付。
3. RequesterCredentials:标明request的身份,防止造假。
接下来,我们回到开头提出的支付系统的通知需求来分析一下ReviseCheckoutStatus通知机制的优劣
安全性方面,ReviseCheckoutStatus接口是eBay开放平台TradingAPI的一部分,所以文中的RequesterCredentials可以用户名/密码,也可以是AuthToken,相对来说AuthToken安全性更高,唯一的缺点就是要维护Token的生命周期,而token的维护又是依赖于TradingAPI的另外一个接口FetchToken来实现,本身也是通过用户名/密码换取的。但是由于AuthToken本身并没有绑定任何scope,因此是一个超级权限。因此如果能进一步缩小token本身的scope和生命周期,会更好的实现安全性。
高效性方面,ReviseCheckoutStatus接口需要完全更新eBay的order系统和payment系统才能返回更新结果,也就是说如果任何的更新遇到长延迟,也就意味着当前线程将被阻塞等待。这对于高并发的情形下,服务器将是一个考验。很容易造成拒绝服务的情况。
可靠性方面,由于有两个标识符,系统首先匹配eBay标识符,接口当且仅当order找到的情况下才继续操作。也就是说PayPal系统需要保存eBay电子商务平台上的信息,即使在我们看来order/item/transactionId这些概念对于标示一笔支付来说非必须。这个也在生产环境中经常发现找不到原始订单匹配的情况。另外一方面,对于那些非Order的支付,这个接口就显得无法使用了。不过对于message发送失败后,这个接口有retry机制,一定程度保证了。
总结以上分析,ReviseCheckoutStatus依赖于电子商务平台的开放接口,所以接口本身带有电子商务信息的特殊性,对于一个支付结果来说无法构建一个统一的通知机制。因此扩展性上极差,这在实战中也感受到了增加一个新功能的不易。
上面讲到电子商务平台上基于order的支付结果通知机制无法满足现实的需求,那么纯粹一些,支付平台本身如果具备任何账户的变动都能够一个即时通知,那样,不管是什么样的电子商务平台,也不管是订单系统还是物流运费系统,或者是会员系统,都能根据账户变动实时得到账户变动。进而根据支付结果更新自己的状态。
PayPal的InstantPayment Notification (IPN) 就是这样一种方案。IPN通知机制如下图2-2所示,
主要工作流程如下:
1. 每当有一笔支付结果产生或者原交易支付状态变化,PayPal所属账户都会调用该账户配置中指定的HTTP endpoint,发送的数据是纯粹的支付信息,后面会详细分析格式。
2. eBay接收到支付结果的第一件事情就是原封不动的发送回PayPal,以便能验证支付信息的完整性。
3. PayPal验证服务器接收到原始报文后会比对各个字段是否被修改过,并返回验证结果。(这里的比对可以是比对MAC值)
4. eBay得到验证通过后才继续逻辑处理,主要的工作就是找到Payment原交易,并更新。
5. eBay处理完后返回PayPal处理结果。httpstatus=200是成功,其他400,500为失败。
6. PayPal在接收到失败响应后会放到retry队列,安排下次再次发送。
接下来我们来看看IPN的具体格式,
mc_gross=19.95&protection_eligibility=Eligible&address_status=confirmed &payer_id=LPLWNMTBWMFAY&tax=0.00&address_street=1+Main+St&payment_date= 20%3A12%3A59+Jan+13%2C+2009+PST&payment_status=Completed&charset=window s-1252&address_zip=95131&first_name=Test&mc_fee=0.88&address_country_co de=US&address_name=Test+User¬ify_version=2.6&custom=&payer_status=ve rified&address_country=United+States&address_city=San+Jose&quantity=1&v erify_sign=AtkOfCXbDm2hu0ZELryHFjY-Vb7PAUvS6nMXgysbElEn9v-1XcmSoGtf&pay er_email=gpmac_1231902590_per%40paypal.com&txn_id=61E67681CH3238416&pay ment_type=instant&last_name=User&address_state=CA&receiver_email=gpmac_ 1231902686_biz%40paypal.com&payment_fee=0.88&receiver_id=S8XGHLYDW9T3S& txn_type=express_checkout&item_name=&mc_currency=USD&item_number=&resid ence_country=US&test_ipn=1&handling_amount=0.00&transaction_subject=&pa yment_gross=19.95&shipping=0.00 |
分析上面的信息,可知IPN支付结果主要包括以下几个部分:
1. 收款人信息:receiver_email/receiver_id/receiver_country, 这些信息都是支付系统的账户标识符,对于电子商务平台来说用处不大。
2. 付款人信息:payer_email/payer_id/first_name/last_name, 这些信息都是支付系统的账户标识符,对于电子商务平台来说用户也不大。
3. 支付概要信息:txn_id/txn_type,用来唯一标示这笔支付,电子商务平台可以用它来匹配订单。
4. 支付详细信息:custom/payment_date/payment_status/payment_type, 这些字段都是描述了这笔支付的关键信息。这里不得不提到一个关键的字段,custom是eBay在做真实支付时通过API传过去的,好处就是在系统超时的时候,eBay无法通过上述的txn_id匹配原交易的情况下,custom可以作为辅助匹配条件。
接下来,我们还是回到开头提出的支付系统的通知需求来分析一下IPN通知机制的优劣。
安全性方面,IPN callbackverification能够防止消息的伪造的可能性,握手过程跟TCP/IP的握手过程类似。我们先看看TCP/IP的握手过程:
第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
PayPal IPN的握手方式也很像,
第一次握手:建立连接,PayPal客户端发送完整的报文到eBay服务器;
第二次握手:eBay服务器收到报文后,并不知道是否来自PayPal,所以把完整的报文又重新发送回了PayPal做确认;
第三次握手:PayPal在得到第一次握手时发送的完整报文后,对原始报文再次做MAC签名,然后跟报文中的verify_sign 字段比对确认是否被篡改过。并把VERIFY结果返回给eBay,eBay在得到成功验证后也完全确认此消息来自于PayPal。
上面的三次握手中,原始报文中的verify_sign是验证消息完整性的关键,而唯一可以生成这个签名的只有PayPal自己的私钥。
高效性方面,真是由于上述的三次握手的安全性考虑,造成了消息接收并不是那么可靠,三次握手中的任何一次失败都可以造成此次消息发送失败。另外一方面,IPN基于Account设计,不管是支付成功失败的消息还是退款的消息,或者任何非购物的账户变动都通过这个接口发送,造成了消息处理的时候需要区分消息的类型,而区分消息类型一开始时可以根据商定简单区分,比如金额的正负来区分正向消费交易和反向退款交易,但是随着业务的发展,越来越多的特例,另外需要再区分逻辑中添加维护,最后照成区分逻辑相当复杂。生产环境也经常发生数据无法区分的情况。而且对于那些非购物的账户变动,eBay根本就不关心,不如账户billing agreement的变动,账户在非ebay网站上的购物支付行为。eBay往往是需要在完成上述三次握手后又丢弃掉,对于服务器资源的一种浪费。
可靠性方面,IPN本身的payload包含的支付信息里面即有来自PayPal的唯一标识符,也有来自eBay的唯一标识符。因此对于交易匹配带来了极大的便利性。对于message发送失败后,PayPal发送者维护了retry机制,在发送失败的情况下能够保证消息发送的不遗漏。
总结以上分析,IPN的机制主要建立在支付消息中,在安全性和可靠性方面都有独到的设计。美中不足的是高效性方面,由于消息基于Account设计,对于消息的订阅是个问题,尤其对于eBay这种作为交易市场而不是交易双方来说,对于取得用户权限订阅消息显得非常困难,往往需要再业务上取得合作授权后才能对相应的账户的IPN进行订阅。另外一方面三次握手的过程过于繁琐,减少系统交互是以后的发展目标。
为什么有个2.5代呢?肯定是IPN本身有不能满足需求的地方。比如,IPN的发送总是在交易发生之后,通过一个PayPal的内部的消息平台依次发送到一个固定的地址。首先在时效性方面至少会存在延迟,也就是在ebay支付交易发生的时刻到eBay平台接收到支付确认的时刻之间存在着延迟,如果PayPal的消息过多,由于IPN没有消息类型,等于说所有类型的消息都一起在排队,一个队伍必然造成延迟,对于大型的merchant来说客户的投诉和咨询量必然会增加。
因此,一种叫PDT(Payment DataTransfer)的支付信息通知机制诞生了。
什么是PDT?它跟IPN的不同之处在于,一旦ebay发生了一笔支付交易,PDT会立即发送回eBay一个支付确认消息。而IPN就不行,IPN就会存在一种叫做materiallag的延迟。这个延迟的避免使得一些销售电子券的商店能够使客户在完成付款后理解下载电子券,因为支付确认信息在完成付款后PDT已经立即返回eBay。
PDT的流程如下:
1. eBay消费者在PayPal网站完成支付
2. PayPal立即发送一个HTTPGET消息回eBay,内容包括Transaction Id
3. eBay把TransactionId和Identitytoken(PayPal user token)发送回PayPal
4. PayPal验证完token和transactionId后发送成功消息,消息包括了交易的详细信息,内容同IPN payload。
5. eBay解析消息并显示给用户。
分析PDT,可以看出它跟IPN基本类似,三次握手得到可靠的消息,所不同的是消息的内容在确认身份后再发送,比起IPN的重复发送过程省去了很多网络开销。
安全性方面,三次握手中,消息是pull的方式得到,不可能被伪造。唯一值得考虑的是paypal user的Identity token是人工拷贝得到的,对于编码和维护来说是个问题,也有可能被泄露的风险。因此消息有可能被窃取,但是不可能被篡改。
高效性方面,类似于IPN,三次握手本身造成了效率不高,但是比IPN的重复消息内容来回传递明显效率高一些。
可靠性方面,PDT有个弱点,它只尝试发送消息有且仅有一次。所以,如果eBay作为接收者,如果服务暂时不可达,那么PDT消息就丢失了。所以可靠性还是不高的。 还有一点就是PDT本身的消息只有一次,而这一次的消息就是订单支付成功的消息,至于交易被退款或者chargeback就无能为力了。因此如果eBay系统如果在幂等处理方面做得好的话,一般会让IPN和PDT消息同时接受。
上面描述了各种通知方式,各有优缺点,下面我们介绍一下新的通知理念。Webhooks,这个词在github上经常听到,也就是github的任何操作都可以被当做事件来发送到一个自定义的地址。比如一个commit,一个push操作,这对于CI持续集成来说是非常有用的,CI可以在有任何代码checkin的情况下自动发起build,对于build失败的情况可以及时的通知相应的checkin代码的programmer。
因此对于支付消息来说,对于支付交易的每一个操作本身也是一个事件,为什么不能用同样的原理来实现呢?答案是肯定的。
首先讲一下一些定义:
Webhooks,翻译过来叫做钩子,它是用户自定义订阅事件类型后的HTTP回调,内容是订阅的消息内容。它是异步的,消息顺序无法保证,而且是幂等的,因此可能导致同样的事件消息发送不止一次。通常对于webhooks的配置主要包括:创建webhook,列出已订阅的webhook,更新webhook回调地址,删除webhook订阅。
Events,事件被分类成不同的事件类型。事件的发生时因为某种资源的状态改变,比如一个支付状态从完成到退款。如果一个事件放生了,被注册的应用就会被webhook用HTTP POST的方式得到通知。这个POST内容包含了事件的所有细节和所在资源的内容,还包括了引起该事件的事件类型。对于事件的操作包括:检索事件, 查找事件,重发事件。
Event Types,事件类型是指对于Paypal账户的不同类型的触发,比如一个支付退款事件类型就是对于支付这种资源来说,从已支付状态到已退款状态的转变过程。对于事件类型的操作包括:得到所有支持的事件类型,列出已经订阅的事件类型。
PayPal的事件类型有:
l PAYMENT.AUTHORIZATION.CREATED
l PAYMENT.AUTHORIZATION.VOIDED
l PAYMENT.CAPTURE.COMPLETED
l PAYMENT.CAPTURE.PENDING
l PAYMENT.CAPTURE.REFUNDED
l PAYMENT.CAPTURE.REVERSED
l PAYMENT.SALE.COMPLETED
l PAYMENT.SALE.PENDING
l PAYMENT.SALE.REFUNDED
l PAYMENT.SALE.REVERSED
l RISK.DISPUTE.CREATED
再举一个具体的事件内容:
{ "id": "WH-8PT597110X687430LKGECATA", "create_time": "2013-06-25T21:41:28Z", "resource_type": "authorization", "event_type": "PAYMENT.AUTHORIZATION.CREATED", "resource": { "id": "2DC87612EK520411B", "create_time": "2013-06-25T21:39:15Z", "update_time": "2013-06-25T21:39:17Z", "state": "authorized", "amount": { "total": "7.47", "currency": "USD", "details": { "subtotal": "7.47" } }, "parent_payment": "PAY-36246664YD343335CKHFA4AY", "valid_until": "2013-07-24T21:39:15Z", "links": […] } |
接下来我们来分析一下交互过程,如下图:
交互的流程是:
1. eBay首先在PayPal上注册需要订阅的事件类型并指定通知接收地址,PayPal会返回一个WebHookId,eBay把WebHookId保存起来,这样在得到消息的时候就可以作为一个因子来验证消息的真实性。
2. 注册完成后,每当eBay用上述注册developer账号发起支付交易的时候,PayPal都会把该笔支付交易的状态变化作为一个事件发送到上述注册时的通知地址。
3. eBay的Webhook Listener接收到PayPal发送过来的事件内容后,需要验证消息,验证消息的关键信息包括:webhookId,PayPal的公钥证书。前者通过第一步保存的地方可以拿到。后者是通过PayPal指定的地方拿到CA证书。
4. 在验证签名通过后,相应的event就被drop到内部的QueueStorage去处理了。这里采用的是异步的方式。
5. PayPal在接收到2xx的http status的时候就停止发送改事件。
6. PayPal在接收到非2xx的http status的时候会加入到retry队列继续发送事件。(这个貌似利用了IPN的发送方式,可以保证每个事件消息至少能被成功deliver一次。)
下面还是从支付结果通知的三大需求分析Webhooks的特点:
安全性方面,从第三步看出,webhook并没有像IPN那样通过原样返回报文的方式让PayPal帮助验证,而是采用约定的一种方式实现的。验证签名本身也是在信息安全中最常用的方式。
理论上,任何人都可以做HTTPPOST到我们的Webhook Listener,因此我们必须验证:
l Webhook 事件来自PayPal
l Webhook事件在传输过程中没有被篡改或者损坏。
l Webhook事件确实是发送给eBay的。
验证签名,本身可以解决上面的第二个问题,如果验证签名时用的是PayPal的公钥,那么就可以解决上面的第一个问题,因为只有私钥才能签名,而PayPal是唯一一个拥有私钥的实体。还有,如果签名验证的时候还需要一个标示eBay的一个因子,那么自然而来解决了第三个问题。当然这个标示eBay的一个因子必需是唯一性的,这里我们用到了WebHookID。
我们来看看具体的验证过程,
String inputString =String.format("%s|%s|%s|%s", transmissionId, timeStamp, webhookId, crc32(eventBody)); //Get the signatureAlgorithm from the PAYPAL-AUTH-ALGO HTTP header Signature signatureAlgorithm =Signature.getInstance("signatureAlgorithm"); //Get the certData from the URL provided in the HTTP headers and cache it Certificate certificate = X509Certificate.getInstance(certData); signatureAlgorithm.initVerify(certificate.getPublicKey()); signatureAlgorithm.update(inputString.getBytes()); //Actual signature is base 64 encoded and available in the HTTP headers byte[] actualSignature =Base64.decodeBase64(actualSignatureEncoded.getBytes()); boolean isValid = signatureAlgorithm.verify(actualSignature); |
仔细研究代码我们有两个发现和我的理解:
1. TransmissionId/TimeStamp
为什么标示eBay的webhookId和标示PayPal的公钥还需要一个关于每笔交易的标示Transmistion信息?我想这跟MD5加密过程中加入的盐值类似,盐值的原理非常简单,就是先把密码和盐值指定的内容合并在一起,再使用md5,SHA或者CRC32时对合并后的内容进行演算,这样一来,就算密码是一个很常见的字符串,再加上用户名,最后算出来的md5值就没那么容易猜出来了。因为攻击者不知道盐值的值,也很难反算出密码原文。
2. CRC32
数据摘要算法是密码学算法中非常重要的一个分支,它通过对所有数据提取指纹信息以实现数据签名、数据完整性校验等功能,由于其不可逆性,有时候会被用做敏感信息的加密。数据摘要算法也被称为哈希(Hash)算法或散列算法。目前常用的主要有三种类型:
l CRC系列:主要用于winzip等文件完整性校验,优点就是简单,速度快。常用CRC32,hash结果是32bit也就是8个16进制值
l MD系列:主要用于大文件完整性校验或者密码校验,常用的是MD5, 码长128位也就是32个16进制值,因此安全性更高,速度也挺快,不过比起CRC32来说还是慢一些;
l SHA系列:一种安全性更高的hash算法,常用SHA1用于CA证书中;
上述算法几乎都可以满足签名的要求,我想本次采用CRC32签名算法的原因应该是权衡了吞吐量和保密性要求不太高的情况下选择的。
高效性方面,相对于IPN的三次握手,Webhook明显简化了握手的过程。消息完整性验证放到了客户端,唯一需要交互的是CA证书的下载,而证书的下载只要做一次缓存起来就可以了。因此吞吐量会明显增加。在加上客户端的异步处理机制,而且webhook本身是基于api的account订阅的,而不是IPN的user account,因此对于eBay这种第三方交易平台来说,不管user account权限是否能否拿到,如果交易是通过eBay开立的developer账户做的交易,eBay都可以得到事件消息。因此高效性方面还是做得非常好的。
可靠性方面,从通讯层看,webhooks借鉴了IPN的retry 队列的重试机制,对于网络不稳定或者服务器维护的情况下,这种重试机制能保证消息deliver不遗漏。
从通讯过程来看,我们可以看出Webhooks的方式各方面表现都非常好。简单可靠的公钥私钥的签名能保证消息完整不可伪造。同时也实现了消息递送的高效性。与此同时重试队列的继承也实现了消息递送不遗漏。如下表3-1所示:
通知机制 |
安全性 |
高效性 |
可靠性 |
ReviseCheckoutStatus |
高, 依赖于eBay开放平台的token架构 |
低 处理逻辑复杂,容易出错,升级困难 |
低 内部处理逻辑和验证复杂 |
IPN |
高 依赖于PayPal消息验证服务器 |
低 三次握手的过程复杂 |
高 重试过程保证消息不遗漏 |
PDT |
高 依赖于PayPal消息服务器验证 |
低 三次握手的过程复杂 |
低 消息发送失败不重试 |
Webhooks |
高 依赖于公私钥的签名 |
高 一次握手client端验证 |
高 重试过程保证消息不遗漏 |
表3-1 各种支付消息通讯机制比较
另外一方面从支付结果通知处理过程来看,我们始终离不开两个平台之间的交易匹配问题。这就让我引发我对于任何平台或者系统之间通知交互匹配的思考。任何一个系统往往都有自己的主表ID,而这个ID一般都能保证在系统内的唯一性,但是对于跨平台,尤其是分布式环境下未必是一个好的解决方案。如下图3-1所示,
如图3-1所示,首先平台A有自己的唯一值A_UID,平台B有自己的唯一值B_UID,平台C有自己的唯一值C_UID。而且平台两两交互的过程中都需要传递自己的唯一值或者对方的唯一值,比如平台A和平台B交互需要互相交换<A_UID, B_UID>, 因此平台A必须在自己的数据库里面存放< A_UID, B_UID >的对应关系。以此类推,所以不管哪个平台都需要维护<A_UID,B_UID,C_UID>这样的对应关系,否则在有二次交互的过程中就无法匹配原始交易信息。
对于一个优秀的架构师或者有洁癖的程序员来说,我们是否可以有另外一种更加优美的设计呢?下图3-2是我的启发,
如上图3-2所示,每个平台不需要维护各个平台之间的UID对应关系图。平台A在跟平台B交互前申请一个或者基于Spec生成UID_ABC,然后在交互的过程中只要传输UID_ABC,对方就能够知道这个ID代表的是什么样,因为UID_ABC在整个环境中都是唯一的。如果是一个公司内或者一个domain内,一个通用的UIDService的建立可以减少很多不必要的系统和通知的麻烦。
比如对于一个支付平台来说,支付子系统包含了会话ID,记账ID,网关ID等。每个系统又是独立的Service。一般对于孤立的系统来说各个系统的设计者往往会为自己系统生成一个唯一值UID,让后在接口中要求调用者记住这个值,而在异步结果通知的时候又可能需要知道对方的唯一值UID,命名为externalId,因此系统之间的唯一值满天飞,到头来缺了这个ID少了哪个ID,往往不得不定义一个IDType这种奇怪的字段来区分不同的系统UID,交互过程相当复杂。
如果换做上图的基于新型的UIDService的方式,那么问题会不会好些呢?
支付会话系统,记账系统,网关系统再也不需要自己生成UID了,也不需要维护其他系统的ID和IDType了,代替的是UIDService生成的global的UID,这样系统与系统之间的交互是不是有了一个统一的指挥棒了?!
当然,有人会说,在一个系统内可能UID会有多个,而且有层级关系,比如会话ID的下面需要指定requestUID,也就是说一个会话可能被重试了多次,每次都有个唯一的requestUID。因此这里就我想到了一个借鉴加密机里面主秘钥派生子秘钥的思想。UID本身是否可以生成子UID,而子UID又能代表或者简单推导出父UID。如下图3-3所示:
图3-3所示,每个系统只要维护自己的UID和UID_ABC的对应关系,比如平台A维护了<UID_A, UID_ABC>,而不需要知道UID_ABC到底对应着多少个其他系统,在内部交互的过程中用到UID_A,而一旦需要跟外部交互就转换成外部通用的UID_ABC。而这种转换过程可以通过者数据库对应来实现,而如果UID_ABC的层级设计的足够好,完全可以通过通用算法得到,就像主秘钥生成子秘钥可以通过通用RSA算法加随机算法来实现。这里就不展开了,我会在后续的文章中详细描述UID的层级设计。
所谓世事洞察皆学问,在总结支付结果通知机制的过程中,我们总是在前人的基础上进行改进,然后得出一个更优的解决方案。
在特定的历史时刻, RCS这种传统的开放平台API方式也许能够满足当时的需要。但是随着互联网的发展,越来越需要一种更加灵活的方式进行系统交互过程。
IPN作为一种可靠的交互过程被广泛运用到系统与系统之间,平台与平台之间,尤其是公司与公司的平台之间,本人涉及到的除了PayPal的IPN通知机制外,其他的诸如北美的ProPay,澳洲的Paymate和欧洲的Skrill(MoneyBookes)也是通过类似的IPN通知来完成对eBay这样的电子商务平台的通知。
后来由于对于整个互联网电子商务平台中用户体验的重视,PDT这种即时通知机制的非可靠的方式,也是对于IPN的一种有益补充。
而最后对于WebHooks思想的出现,也是对现有技术的一种再组合利用。第一次听到WebHooks是在Github中,GitHub把代码资源的状态变化定义成了一种事件,比如代码从commit到push的状态变化是一种事件,因此可以让事件关心者灵活的订阅和感知这种变化,而不需要定时去检测。这在持续集成(CI)中特别有用,比如任何一次代码checkin都可以push到Hudson集成环境中,对于团队合作来说是见非常有用的方式。
不过不管从RCS再到IPN再到WebHooks,往往只能适应当下的需求,我想未来可能会有越来越多的更加安全简洁高效的交互方式。
比如对于系统交互UID的问题,我也提出了一些我的一些思想。希望能给自己在今后的架构设计中能更好的考虑平台交互机制的问题。