在昨天讲完了Message
的密文产生细节流程後,回过头来我再来谈Sign
安全签章,我认为这顺序比较容易理解。其实这整段API的呼叫,可分为几个要素:
讯息内文
以及上述作AES加密的AES Key
与IV
的产生依据 (也就是并非直接拿金钥或IV来运用),由API端讲定规则,将上述要素透过安全杂凑的演算法产生数位签章。由传送端(商家)依规则与重要元素产生的数位签章,
当商家呼叫API时,先前已经经由AES对称加密法产生出密文Message
,因此接收端(永丰API)当然有与加密相同的AES Key
来对密文作解密。因此接收端目前可从中取出讯息内文
的明文。
接着,永丰API会另外收到一个安全签章Sign
,还记得当初是如何产生这个Sign
的方法吗?
再复习一次:「需将讯息内文
以及上述作AES加密的AES Key
与IV
的产生依据 (也就是并非直接拿金钥或IV来运用),由API端讲定规则,将上述要素透过安全杂凑的演算法产生数位签章。」
刚刚已经从AES解密拿回讯息内文
了,那剩下的产生依据,既然当初这些都是永丰提供的,他们当然都有这些值。只需要依据讲好的规则,再运作一次,然後「比对这个刚刚做出来的Sign和商家传来的Sign是否相同」即可。(因为杂凑演算法是不可逆推的,因此只需比对结果是否一样就好)
当然,其中还会有机制去多比对Nonce值是否在效期内,以及是否被重覆使用过,甚至判断是否为商家的IP等。这些上面说明的所有作法的目的,都是为了验证一件事:
「即使API收到了一个密文,也顺利解开,看起来也很合理的内容,到底是不是该商家传来的」
但有一个很重要的前提是,商家有好好保护自己的机敏讯息:那4个需要拼了命保护不可泄漏的Hash代码。
虽然商店编号(ShopNo)也很重要,因为有这个就可以透过API问到Nonce值,但我相信正式的运作机制下,在申请服务的初期就会绑定好商家主机固定IP。因此即使其他人乱猜中商店编号,无法从正确的IP位置发送讯息是会被拒绝的。
Sign的制作材料会需要三个:
最後把上述三个黏起来後再做一次SHA256
这个讯息内文不是直接把我们打算要传的JSON格式直接传出去,而是要符合以下规格:
- 先移除所有空值的参数,参数值前後不可有空白。
- 将剩余所有参数值依照「参数名称」由小至大排序 (不分大小写即 A<B and a <B ) ),组成如param1=value1¶m2=value2 的字串。
- 如为多节点参数则不参与 sign 值演算 。
所以这张规格书的内文JSON范例的黄色部份,就是依据上面规则後需要放入的属性与值。
没标黄色的为「空值」或是「多节点参数」,没有入场券,因此他们就不要放进去了。
我们一样先以开发规格书中的范例内容进行处理,先确保写出来的Python程序可以达成和他一样的结果。
这里面的处理有一些地方需要慢慢处理与尝试,光完成这些步骤就花了一点时间,先看一下完整的Python Code:
import urllib
shop_data = {
"ShopNo": "BA0026_001", "OrderNo": "A201804270001", "Amount": 50000, "CurrencyID": "TWD", "PayType": "A",
"ATMParam": { "ExpireDate": "20180502" },
"CardParam": { },
"ConvStoreParam": { }, "PrdtName": "虚拟帐号订单", "ReturnURL": "http://10.11.22.113:8803/QPay.ApiClient/Store/Return", "BackendURL": "http://10.11.22.113:8803/QPay.ApiClient/AutoPush/PushSuccess"
}
print("* The original dictionary : {}".format(shop_data))
# Output: * The original dictionary : {'ShopNo': 'BA0026_001', 'OrderNo': 'A201804270001', 'Amount': 50000, 'CurrencyID': 'TWD', 'PayType': 'A', 'ATMParam': {'ExpireDate': '20180502'}, 'CardParam': {}, 'ConvStoreParam': {}, 'PrdtName': '虚拟帐号订单', 'ReturnURL': 'http://10.11.22.113:8803/QPay.ApiClient/Store/Return', 'BackendURL': 'http://10.11.22.113:8803/QPay.ApiClient/AutoPush/PushSuccess'}
def check_passed_rule_param(value):
if value is None:
return False
elif type(value) is dict or type(value) is list:
return False
elif type(value) is str and not value.strip():
return False
else:
return True
sorted_shop_datat = {key: shop_data.get(key) for key in sorted(shop_data.keys(), key=str.casefold)}
print("* sorted_shop_datat: {}".format(sorted_shop_datat))
# Output: * sorted_shop_datat: {'Amount': 50000, 'ATMParam': {'ExpireDate': '20180502'}, 'BackendURL': 'http://10.11.22.113:8803/QPay.ApiClient/AutoPush/PushSuccess', 'CardParam': {}, 'ConvStoreParam': {}, 'CurrencyID': 'TWD', 'OrderNo': 'A201804270001', 'PayType': 'A', 'PrdtName': '虚拟帐号订单', 'ReturnURL': 'http://10.11.22.113:8803/QPay.ApiClient/Store/Return', 'ShopNo': 'BA0026_001'}
removed_rule_values_shop_data = {key: value for key, value in sorted_shop_datat.items() if check_passed_rule_param(value)}
print("* removed_rule_values_shop_data: {}".format(removed_rule_values_shop_data))
# Output: * removed_rule_values_shop_data: {'Amount': 50000, 'BackendURL': 'http://10.11.22.113:8803/QPay.ApiClient/AutoPush/PushSuccess', 'CurrencyID': 'TWD', 'OrderNo': 'A201804270001', 'PayType': 'A', 'PrdtName': '虚拟帐号订单', 'ReturnURL': 'http://10.11.22.113:8803/QPay.ApiClient/Store/Return', 'ShopNo': 'BA0026_001'}
urlparam = urllib.parse.urlencode(removed_rule_values_shop_data)
print("* urlparam: {}".format(urlparam))
# Output: * urlparam: Amount=50000&BackendURL=http%3A%2F%2F10.11.22.113%3A8803%2FQPay.ApiClient%2FAutoPush%2FPushSuccess&CurrencyID=TWD&OrderNo=A201804270001&PayType=A&PrdtName=%E8%99%9B%E6%93%AC%E5%B8%B3%E8%99%9F%E8%A8%82%E5%96%AE&ReturnURL=http%3A%2F%2F10.11.22.113%3A8803%2FQPay.ApiClient%2FStore%2FReturn&ShopNo=BA0026_001
urlparam_no_percent_encode = urllib.parse.unquote(urlparam).replace("+", " ")
print("* urlparam_no_percent_encode: {}".format(urlparam_no_percent_encode))
# Output: * urlparam_no_percent_encode: Amount=50000&BackendURL=http://10.11.22.113:8803/QPay.ApiClient/AutoPush/PushSuccess&CurrencyID=TWD&OrderNo=A201804270001&PayType=A&PrdtName=虚拟帐号订单&ReturnURL=http://10.11.22.113:8803/QPay.ApiClient/Store/Return&ShopNo=BA0026_001
几个地方需要处理:
属性(Key)
的部份作排序(A->Z)1. 忽略大小写的重新排序
将原面JSON的物件的Key值作sorted(),以字串方式排序,但要加上str.casefold
才会忽略大小写的ASCII值。先取得排序过後的Key List後,再使用Dictionary Comprehension方法把排序过後的物件(以Dictionary结构组成)产生回来,如此一来我们就拿到了属性排序过後的JSON。
2. 去掉不要的属性
我们先准备好一个规则过滤的布林function,主要是用在Dictionary Comprehension後的if条件式。意思就是我们把原本已排序好的新Dictionary重新跑一遍,只会留下规则过滤器结果为True的值。所以我们就会拿到那些没有「多节点」或「空值」的内文版本。
3. 使用URLEncode模组帮忙转出
透过好用的urllib.parse.urlencode()就可以将Dictionary转出成URL的参数表示法。但这里转出後会将非英数字的符号或编码转成以百分比开头的URI percent-encoding rules编码文字。
4. 把文字编码的呈现再最後调整一下
永丰文件中是会希望保留原始带有类似中文的内文去做後续杂凑运算,因此我们需要再加一点工。透过urllib.parse.unquote()方法将百分比编码法则再转回原本的编码。另外若原本参数里面(不是前後)有空白的话,转出来的url参数会将空白变成+
,因此要再换回来一次,否则Sign
的内容会因此而有所差异。
透过上面的步骤处理,我们就能够取得讯息内文
的部份。
但在开发规格中,把上面最终我们取得的URLEncode这串结果称之为「内文杂凑」,我个人认为这个步骤使用杂凑这个词汇似乎是不太正确。这里没有用到任何杂凑的技术。
接下来还要再做两个相当简单的动作,将是将Nonce和Hash ID拼在上面这一串的後面,但文件中把这三个拼接在一起的产出结果称之为「字串杂凑」,我一样是觉得这个动作也没有杂凑在里面。(难道是杂乱的凑在一起!?)
总之,把这三个再拼接起来後,就可以进行最终的SHA256处理,离成功就在不远处!
import hashlib
nonce = "NjM2NjA0MzI4ODIyODguMzo3NzI0ZDg4ZmI5Nzc2YzQ1MTNhYzg2MTk3NDBlYTRhNGU0N2IxM2Q2M2JkMTIwOGU5YzZhMGFmNGY5MjA5YzVm"
hash_id = "17D8E6558DC60E702A6B57E1B9B7060D"
final_shop_data = "{}{}{}".format(urlparam_no_percent_encode, nonce, hash_id)
print(final_shop_data)
# Output: Amount=50000&BackendURL=http://10.11.22.113:8803/QPay.ApiClient/AutoPush/PushSuccess&CurrencyID=TWD&OrderNo=A201804270001&PayType=A&PrdtName=虚拟帐号订单&ReturnURL=http://10.11.22.113:8803/QPay.ApiClient/Store/Return&ShopNo=BA0026_001NjM2NjA0MzI4ODIyODguMzo3NzI0ZDg4ZmI5Nzc2YzQ1MTNhYzg2MTk3NDBlYTRhNGU0N2IxM2Q2M2JkMTIwOGU5YzZhMGFmNGY5MjA5YzVm17D8E6558DC60E702A6B57E1B9B7060D
sign = hashlib.sha256(final_shop_data.encode('UTF-8')).hexdigest().upper()
print(sign)
# Output: A3EAEE3B361B7E7E9B0F6422B954ECA5D54CEC6EAB0880CB484AA6FDA4154331
这边要处理的事情其实蛮简单的,把许久不见的Nonce
和Hash ID
再请出来,然後字串黏一黏之後,用我们先前做过要产生Hash ID时的SHA256一样,依样画葫芦,就可以产生出最後的大魔王Sign
了!
为了要完成之後的每一次API叫用,我们一步一步把所需要的属性都一个一个验证且实作出来了,最困难的当然就是Message
和Sign
,接下来我们就可以使用我们自己产生的资料,来正式对永丰API作呼叫罗!
>>: Day 21 Azure machine learning: Upload data- 自己的资料自己传
上一篇中介绍完 Template-driven forms 後,接着再介绍 Angular 的另一种...
Priority queue Priority queue和queue一样也有两种形式 : max ...
在昨天了解如何建立专案後,今天来说明SPring Boot的启动原理 在不导入第三方library的...
时间过得很快,这边我们已经来到物理模拟篇的最後一节 ~ 二维布料模拟了。 原本其实我是打算把这一篇放...
前言 我们经常会在求职网上看到需要某 SDK、API 的串接经验,我们应该也要做相关功课,才能理解这...