由于业务场景中使用了base64编码进行数据的处理,最近被它折腾的不轻,今天就来看看,都是哪里出了问题。
一、参与签名时,对base64编码结果处理不当
我们知道,base64编码是由大小写字母、数字和+/=三个特殊字符,合计65个字符构成,而=是作为末尾补位符使用,参与编码的字符是64个。
因此,base64编码结果存在等号(=),经过urlencode编码过后,等号会被处理成%3D
假定被base64编码的参数为note,其原始内容为:note=中国牛逼~,base64_encode的结果为:5Lit5Zu954mb6YC8fg==,经过urlencode处理的结果为:5Lit5Zu954mb6YC8fg%3D%3D,因此,我们的请求参数变成了:note=5Lit5Zu954mb6YC8fg%3D%3D,当我们约定note参与签名时,对方就可能得到3种不同的参与签名内容:
md5('note=中国牛逼~')
md5('note=5Lit5Zu954mb6YC8fg==')
md5('note=5Lit5Zu954mb6YC8fg%3D%3D')
一般来说,不刻意进行base64_decode处理,第一种不会出现。
但是第二种和第三种就比较容易混淆了,他们受到web服务器解析参数影响,有些web服务器对参数默认进行urldecode处理,有些却没有。
因此,假定我们参与签名的是urlencode的内容,即md5('note=5Lit5Zu954mb6YC8fg%3D%3D')时,默认进行urldecode的业务方,得到的内容却是note=5Lit5Zu954mb6YC8fg==,如果对方直接对内容进行生成签名,就会导致md5签名无法匹配,校验失败。
md5('note=5Lit5Zu954mb6YC8fg==')=142b4b3921701512f39e88a6bf9a97a3
md5('note=5Lit5Zu954mb6YC8fg%3D%3D')=fbb7df69af21293c8a280a4d1b4663af
如果一开始对接的时候测试内容note的base64_encode结果就带有等号,那对接时就会发现签名异常。
但还有一种情况,base64_encode在某种情况下,不需要补位处理,比如:
base64_encode('中国牛逼')=5Lit5Zu954mb6YC8
这时候,对接的时候无论web服务器是否默认进行urldecode处理,参与签名的内容都是note=5Lit5Zu954mb6YC8,签名是可以通过的。
嘿嘿~~ 埋下一颗隐雷,等着生产环境踩上去。
二、前端对移除补位数据解析异常
由于base64_encode的补位规则是可以预知的,因此,按理来说,直接移除末尾的补位等号,正常也是可以解码成功的。
但是,本人照着这种常规推理来实现功能的时候,又踩坑了!
这个坑要从网络上传播的一段js版本的base64_decode代码说起,下面这个是其中一份copy:
异常测试效果如下,移除末尾等号和不移除返回数据长度不一致,MD5有差异:
Base64.decode('5Lit5Zu954mb6YC8fg').length
7
Base64.decode('5Lit5Zu954mb6YC8fg==').length
5
md5(Base64.decode('5Lit5Zu954mb6YC8fg'))
"558bcf327d6e5c908927176e61ea3707"
md5(Base64.decode('5Lit5Zu954mb6YC8fg=='))
"7e903f42b3062d75aedd01c7d1a95375"
我们来看看代码:
decode:function(input)
{
varoutput="";
varchr1,chr2,chr3;
varenc1,enc2,enc3,enc4;
vari=0;
input=input.replace(/[^A-Za-z0-9\+\/\=]/g,"");
while(i
{
enc1=this._keyStr.indexOf(input.charAt(i++));
enc2=this._keyStr.indexOf(input.charAt(i++));
enc3=this._keyStr.indexOf(input.charAt(i++));
enc4=this._keyStr.indexOf(input.charAt(i++));
chr1=(enc1<<2)|(enc2>>4);
chr2=((enc2&15)<<4)|(enc3>>2);
chr3=((enc3&3)<<6)|enc4;
output=output+String.fromCharCode(chr1);
if(enc3!=64)//移除末尾补位等号导致进入此条件,多返回String.fromCharCode(0)
{
output=output+String.fromCharCode(chr2);
}
if(enc4!=64)//移除末尾补位等号导致进入此条件,多返回String.fromCharCode(0)
{
output=output+String.fromCharCode(chr3);
}
}// Whend
output=Base64._utf8_decode(output);
returnoutput;
}
异常原因是,移除末尾的补位等号之后,此类库的decode方法获取最后两位补位的时候,无法正常获取等号,导致最后补位返回了字符 String.fromCharCode(0)的结果,这个是一个肉眼空白字符,但是是有长度的
因此,此类库的decode方法需要修复问题的话,有两种解决方案:提前补充末尾等号
input='5Lit5Zu954mb6YC8fg'
"5Lit5Zu954mb6YC8fg"
input+='='.repeat(4-(input.length%4))
"5Lit5Zu954mb6YC8fg=="
处理结果移除末尾空白符
returnoutput.replace(/\0+$/,'')
至此,前端类库修复完成。
三、前端类库无修复条件(无限复制导致维护成本过高)
实际业务过程中,还可能遇到这样的情况,前端类库无限复制,导致修改成本过高,我们只能曲线救国,从后端去解决补位等号导致的签名隐患。
根据base64编码的原理分析,我们可以发现,只要数据长度为3的倍数,则不需要补位等号。
>>>base64_encode('a')
=>"YQ=="
>>>base64_encode('aa')
=>"YWE="
>>>base64_encode('aaa')#长度为3
=>"YWFh"
>>>base64_encode('aaaa')
=>"YWFhYQ=="
>>>base64_encode('aaaaa')
=>"YWFhYWE="
>>>base64_encode('aaaaaa')#长度为6
=>"YWFhYWFh"
因此,我们的目标就是:让数据的长度扩充成3的倍数。
注意:
对于字符串来说,修改数据的长度可能会破坏数据本身,因此大家需要根据自身的业务场景来决定,切勿盲目修改。
对于数组来说,如果为其增加一个补位数据,不会破坏数据的使用,则可以通过此方式处理优化。
下面举例说明下针对数组的处理,从输出可以看出,当数组json后的数据长度不是3的倍数时,数组会追加一个补位数据_pad_,然后通过给他赋值补位,实现数组json后的数据长度满足3的倍数,从而得到最终无需补位末尾等号的base64_encode结果。
functionautoFixArrayBase64Pad($arr)
{
$jsonOrig=$jsonStr=json_encode($arr);
$jsonLen=strlen($jsonStr);
if($jsonLen%3!=0){
//加入填充数组
$arr['_pad_']='';
$jsonLen=strlen(json_encode($arr));
//补齐缺失长度
$autoPadLen=(3-($jsonLen%3))%3;
$arr['_pad_']=str_repeat('_',$autoPadLen);
$jsonStr=json_encode($arr);
}
echo"jsonOrig: ".$jsonOrig.", len: ".strlen($jsonOrig)."\njsonFix : ".$jsonStr
.", len: ".strlen($jsonStr)."\nbase64Orig: ".base64_encode($jsonOrig)
."\nbase64Fix : ".base64_encode($jsonStr)."\n\n";
returnbase64_encode($jsonStr);
}
for($i=1;$i<=3;$i++){
autoFixArrayBase64Pad(['name'=>str_repeat('a',$i)]);
}
for($i=1;$i<=3;$i++){
autoFixArrayBase64Pad([str_repeat('a',$i)]);
}
# 输出
jsonOrig:{"name":"a"},len:12
jsonFix:{"name":"a"},len:12
base64Orig:eyJuYW1lIjoiYSJ9
base64Fix:eyJuYW1lIjoiYSJ9
jsonOrig:{"name":"aa"},len:13
jsonFix:{"name":"aa","_pad_":""},len:24
base64Orig:eyJuYW1lIjoiYWEifQ==
base64Fix:eyJuYW1lIjoiYWEiLCJfcGFkXyI6IiJ9
jsonOrig:{"name":"aaa"},len:14
jsonFix:{"name":"aaa","_pad_":"__"},len:27
base64Orig:eyJuYW1lIjoiYWFhIn0=
base64Fix:eyJuYW1lIjoiYWFhIiwiX3BhZF8iOiJfXyJ9
jsonOrig:["a"],len:5
jsonFix:{"0":"a","_pad_":"_"},len:21
base64Orig:WyJhIl0=
base64Fix:eyIwIjoiYSIsIl9wYWRfIjoiXyJ9
jsonOrig:["aa"],len:6
jsonFix:["aa"],len:6
base64Orig:WyJhYSJd
base64Fix:WyJhYSJd
jsonOrig:["aaa"],len:7
jsonFix:{"0":"aaa","_pad_":"__"},len:24
base64Orig:WyJhYWEiXQ==
base64Fix:eyIwIjoiYWFhIiwiX3BhZF8iOiJfXyJ9
参考资料:未经同意禁止转载!