回顾昨天拿到的response,乍看之下以为API将我们传给他的内容原封不动传回来了。
但实际上比对一下内容就会发现,API也给了我们一串我们需自行解开的密文。
其中有三个关键:
请睁大眼睛看一下,这个Nonce和我们传给他的Nonce不一样,所以千万不要拿自己刚那个Nonce自作聪明的往下作。
在我们经过这麽多天的特训後,应该闭着眼睛都可知道如何解开这个密文,以及验证这个密文是由永丰API正确无误无遭人窜改的内容吧!
#-- Response: {'Version': '1.0.0', 'ShopNo': 'NA0249_001', 'APIService': 'OrderCreate', 'Sign': '8A001F83ABF5EAF292119ADBFDBCFE7F34A535781E8F77A7B0D09A9FD56E90BF', 'Nonce': 'NjM3NjgxMDcxMzg2OTUuMTo4MTQwODE2NzBiMGUyYTdiNTAzZDExN2Q5NDhmOGMzMTVlZWRhOGI5ODY2OGUyOGNkMGFiM2MzZDhiNGEzZGRi', 'Message': '4AAC75A87C46EC473D94FFFB270DCAF3263CC5DB5F3E49ABCEE8E28A073F16D750469AEE4E77A1F0237DEA7043CD79273E0300D94286C81DF70B4A2C2BEA54DDA7AE4F137D109E9E6FBF4494FDCA9749C61F1DD9A30CFC7A831735D5811B26FAABC23B7C1E6CD7329974AE866EC2A72F09574E2A0C334A8F227FFF1462489E8187CCE9986940272C7B7BB1A676171F898D03909CD96EA6981B6EA7CB02003ED4DC1D95190F76DCB071E4BEDDDB55BB4D1EC7B06681D0FA583051112DDC36B1A3459A14C28789E5EBF02451EC77AC0F0DDBE00D2B07FF0D7BE195E866AF3D341CC21E8C346D2A72C4541898595F81AB60894049A32A5C551C91E4F492EF3F33F32268A8EDAB1AFBAA49F6ED1833BFD756F1955FA6BB1A3FC38773FE42E53DA5B82911073356C3A2211DE51810C5CDB54E73FFCC67BA0441BB7F53BCB4D640BD73F06336BE1FEA4A0ACFA316F07F0A5FE232380CBF245AF01777BBDF770EDD08F77F853BDC2715FDA066F271C58F31424C47B1593829E7D67A5105224AEBF10D99DB2CFC9F6483440601DAACFB20251D724DFD8447C0A28408921966A3084E97C564017973A9B8CED71F00371F391663196D8021CEF2B74C86AF0EACD275A5BFE8F2D1B787648F64EF2CCEB4CB8834B8E1'}
def aes_dec(data_string, resp_nonce):
hash_id_ba = hash_id.encode("utf-8")
iv_ba = get_aes_iv(resp_nonce).encode("utf-8")
cipher = AES.new(key=hash_id_ba, mode=AES.MODE_CBC, iv=iv_ba)
message = bytes.decode(unpad(cipher.decrypt(bytes.fromhex(data_string)), AES.block_size), "utf-8")
return message
resp_nonce = resp["Nonce"]
resp_msg = resp["Message"]
resp_ori_sign = resp["Sign"]
dec = aes_dec(resp_msg, resp_nonce)
print("- Decryption of Response: {}".format(dec))
#{"OrderNo":"A202109838256","ShopNo":"NA0249_001","TSNo":"NA024900000227","Amount":79900,"Status":"S","Description":"S0000 – 处理成功","PayType":"A","ATMParam":{"AtmPayNo":"99922530174963","WebAtmURL":"https://sandbox.sinopac.com/QPay.WebPaySite/Bridge/PayWebATM?TD=NA024900000227&TK=82cd04db-cd70-4bf8-8215-73675e920fd9","OtpURL":"https://sandbox.sinopac.com/QPay.WebPaySite/Bridge/PayOTP?TD=NA024900000227&TK=82cd04db-cd70-4bf8-8215-73675e920fd9"}}
我们取回来的值,会先拆开第一层的json,Version、ShopNo、APIService原则上会和我们呼叫时的内容是一样的。而我们要先将Message
值取出来要做AES解密,而Sign
值取出来要做比对验证确认其内容不可否认性(non-repudiation)。而Nonce
值是要重新产生这次解密的IV
值的基础。
我们要撰写一个AES-CBC的解密aes_dec()
,其实内容和加密差不多。将取回来的Message密文和Nonce传入後,使用AES的decrypt()
将结果解出来,由於双方采用的AES是对称式加密,因此我们手上的AES Key,就是我们先前的Hash ID
。成功解密後,我们就会拿回人类看的懂的第二阶的JSON内容。
记得当我们在加密时有做过pad()的padding手法,一样的我们在解密时也需要做反向的unpad(),把原本有padding的值再拿掉,否则有时候解回原文时最後在尾巴会产生乱码。
接下来,我们就要把JSON内容再拿出讯息内文
来重新计算Sign
的内容。
resp_json = json.loads(dec)
resp_gen_sign = get_sign(resp_json, hash_id, resp_nonce)
print("- 重新产生Sign值: {}".format(resp_gen_sign))
# Output: - 重新产生Sign值: 52BA786E4E6BBE5DB5A41FF8B656565EB529D135B276BFC3D17D0BB9467F4B4C
print("- Sign验证结果,是否样同? {}".format(resp_ori_sign == resp_gen_sign ))
# Output: - Sign验证结果,是否样同? True
原本我们解密回来的是一个JSON字串,所以要把字串经由json.loads()
转成Python Dictionary。这一整串就是讯息内文
,加上Hash ID
以及新取回的Nonce
,规则和先前是一样的,因此就重覆使用我们撰写好的get_sign()
进行计算,会得到新的Sign值内容:52BA786E4E6BBE5DB5A41FF8B656565EB529D135B276BFC3D17D0BB9467F4B4C
我们立刻把这一串安全签章拿去和API回传给我们的比对一下,完全相同!
需要把这一个验证步骤也做完後,才算是完整的流程,但开心之余,我们是不是忘了什麽?
还有一件最重要的事,当然就是要解析API回传给我们的JSON内容:
Status
:要收到S
才代表成功Description
:若是成功的话,会是S0000 – 处理成功
TSNo
:我们的例子比拿到了NA024900000227
ATMParam
:
AtmPayNo
,如果顾客不是选择使用WebATM网页上转帐的话,可在电商的页面上显示这个虚拟帐号让他们使用惯用的方法转帐。我们的例子拿到了99922530174963
。WebAtmURL
。OtpURL
。把值从Dictionary中取出即可,这部份很简单:
tsno = resp_json["TSNo"]
print(tsno)
# Output: NA024900000227
status = resp_json["Status"]
print(status)
# Output: S
desc = resp_json["Description"]
print(desc)
# Output: S0000 – 处理成功
atm_param = resp_json["ATMParam"]
atm_pay_no = atm_param["AtmPayNo"]
print(atm_pay_no)
# Output: 99922530174963
web_atm_url = atm_param["WebAtmURL"]
print(web_atm_url)
# Output: https://sandbox.sinopac.com/QPay.WebPaySite/Bridge/PayWebATM?TD=NA024900000227&TK=82cd04db-cd70-4bf8-8215-73675e920fd9
otp_url = atm_param["OtpURL"]
print(otp_url)
# Output: https://sandbox.sinopac.com/QPay.WebPaySite/Bridge/PayOTP?TD=NA024900000227&TK=82cd04db-cd70-4bf8-8215-73675e920fd9
在规格书有提到:
丰收款会依 BackendURL 或 ReturnURL 将讯息 内 Token 传送给商户,商 户会收到一组 Token 值後使用「 6.5讯息查询服务」来确认内容...
但由於使用虚拟帐户的要求时,ReturnURL
为必填,但BackendURL
并不是。其实我不是很确定ReturnURL
会在什麽情况下被用到。我先假设是使用永丰的WebATM或OTP的服务时,毕竟是在永丰的网站作业,而连过去的网址带了一些资讯应该可让永丰後台mapping到我们这笔交易资料,也理当在执行完付款动作後,就可以将使用者转址回我们当初提供的ReturnURL
的网址中。
但若顾客并没有想要使用永丰的WebATM或OTP时,表示顾客想记下虚拟帐户,使用其他的转帐方法来完成支付动作。这样一来,接下来的付款流程就和永丰可控的网站是脱钩的状态,因此ReturnURL
似乎就没有机会被叫用了。
那这样一来,当初非必填的BackendURL
就似乎变的至关重要了,因为这变成是在这个情境下唯一能取得PayToken
的机会。
需要有PayToken,我们才能使用OrderPayQuery
来查询订单的付款状态,需要能查询我们才能在电商的订单後台中,更新付款状态让客户确认。想像一下如果你是顾客,付完款後,一定会想要确认网站的状态是否更新成「已付款」,才会安心。
我试图想使用WebAtmURL
来看看完成後,是否会进行转址。但我从取回的网址连线後,发现这个测试网页是无法使用的,画面如下:
目前还没有办法实现被转入ReturnURL
,而且这个转址虽然是Client Side转址,但转过去後取得网址列的参数(主要是要拿PayToken)後,也是需要透过Server Side程序去处理与储存,而不是靠顾客的Browser的前端程序。因此也是想找时间实作一个我方的BackendURL
让永指API可回报PayToken
。
<<: [Day18] swift & kotlin 实作篇!(9) Animation -kotlin
关於上篇提到的元件,对我而言,属於在讨论阶段,会比较经常拿出来讨论的元件。真正在实作以及管理画面时,...
今天主要来提提Array、ArrayList、List其中一些不同的地方及概念,那麽首先先提提有关於...
Solving Linear Systems of Equations using HHL HHL ...
如果你听过 PWA,那麽对今天的主题ㄧ定不陌生,因为今天要讲的 Service Worker 就是...
回圈有两种语法可以使用,分别是while与for回圈,今天这篇会先来讲到while回圈的部分。 基本...