2020年04月05日 01:51:39

base64编码处理数据踩过的坑

作者: 

本文列出base64编码处理数据过程踩过的坑以及解决方案,“控诉”网络一直传播的一段js base64编码代码隐藏的bug,希望能对同样踩坑的朋友有所帮助。

由于业务场景中使用了base64编码进行数据的处理,最近被它折腾的不轻,今天就来看看,都是哪里出了问题。


一、参与签名时,对base64编码结果处理不当

我们知道,base64编码是由大小写字母、数字和+/=三个特殊字符,合计65个字符构成,而=是作为末尾补位符使用,参与编码的字符是64个。

因此,base64编码结果存在等号(=),经过urlencode编码过后,等号会被处理成%3D

假定被base64编码的参数为note,其原始内容为:note=中国牛逼~,base64_encode的结果为:5Lit5Zu954mb6YC8fg==,经过urlencode处理的结果为:5Lit5Zu954mb6YC8fg%3D%3D,因此,我们的请求参数变成了:note=5Lit5Zu954mb6YC8fg%3D%3D,当我们约定note参与签名时,对方就可能得到3种不同的参与签名内容:

  1. md5('note=中国牛逼~')
  2. md5('note=5Lit5Zu954mb6YC8fg==')
  3. md5('note=5Lit5Zu954mb6YC8fg%3D%3D')

一般来说,不刻意进行base64_decode处理,第一种不会出现。

但是第二种和第三种就比较容易混淆了,他们受到web服务器解析参数影响,有些web服务器对参数默认进行urldecode处理,有些却没有。

因此,假定我们参与签名的是urlencode的内容,即md5('note=5Lit5Zu954mb6YC8fg%3D%3D')时,默认进行urldecode的业务方,得到的内容却是note=5Lit5Zu954mb6YC8fg==,如果对方直接对内容进行生成签名,就会导致md5签名无法匹配,校验失败。

  1. md5('note=5Lit5Zu954mb6YC8fg==') = 142b4b3921701512f39e88a6bf9a97a3
  2. md5('note=5Lit5Zu954mb6YC8fg%3D%3D') = fbb7df69af21293c8a280a4d1b4663af

如果一开始对接的时候测试内容note的base64_encode结果就带有等号,那对接时就会发现签名异常。

但还有一种情况,base64_encode在某种情况下,不需要补位处理,比如:

  1. base64_encode ('中国牛逼') = 5Lit5Zu954mb6YC8

这时候,对接的时候无论web服务器是否默认进行urldecode处理,参与签名的内容都是note=5Lit5Zu954mb6YC8,签名是可以通过的。

嘿嘿~~ 埋下一颗隐雷,等着生产环境踩上去。

二、前端对移除补位数据解析异常

由于base64_encode的补位规则是可以预知的,因此,按理来说,直接移除末尾的补位等号,正常也是可以解码成功的。

移除base64_decode末尾补位等号

但是,本人照着这种常规推理来实现功能的时候,又踩坑了!

这个坑要从网络上传播的一段js版本的base64_decode代码说起,下面这个是其中一份copy:

JS的base64编码解码

异常测试效果如下,移除末尾等号和不移除返回数据长度不一致,MD5有差异:

  1. Base64.decode('5Lit5Zu954mb6YC8fg').length
  2. 7
  3. Base64.decode('5Lit5Zu954mb6YC8fg==').length
  4. 5
  5. md5(Base64.decode('5Lit5Zu954mb6YC8fg'))
  6. "558bcf327d6e5c908927176e61ea3707"
  7. md5(Base64.decode('5Lit5Zu954mb6YC8fg=='))
  8. "7e903f42b3062d75aedd01c7d1a95375"

我们来看看代码:

  1. decode: function (input)
  2. {
  3. var output = "";
  4. var chr1, chr2, chr3;
  5. var enc1, enc2, enc3, enc4;
  6. var i = 0;
  7. input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
  8. while (i < input.length)
  9. {
  10. enc1 = this._keyStr.indexOf(input.charAt(i++));
  11. enc2 = this._keyStr.indexOf(input.charAt(i++));
  12. enc3 = this._keyStr.indexOf(input.charAt(i++));
  13. enc4 = this._keyStr.indexOf(input.charAt(i++));
  14. chr1 = (enc1 << 2) | (enc2 >> 4);
  15. chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
  16. chr3 = ((enc3 & 3) << 6) | enc4;
  17. output = output + String.fromCharCode(chr1);
  18. if (enc3 != 64) //移除末尾补位等号导致进入此条件,多返回String.fromCharCode(0)
  19. {
  20. output = output + String.fromCharCode(chr2);
  21. }
  22. if (enc4 != 64) //移除末尾补位等号导致进入此条件,多返回String.fromCharCode(0)
  23. {
  24. output = output + String.fromCharCode(chr3);
  25. }
  26. } // Whend
  27. output = Base64._utf8_decode(output);
  28. return output;
  29. }

异常原因是,移除末尾的补位等号之后,此类库的decode方法获取最后两位补位的时候,无法正常获取等号,导致最后补位返回了字符 String.fromCharCode(0)的结果,这个是一个肉眼空白字符,但是是有长度的

base64_decode异常

因此,此类库的decode方法需要修复问题的话,有两种解决方案:

  1. 提前补充末尾等号

    1. input = '5Lit5Zu954mb6YC8fg'
    2. "5Lit5Zu954mb6YC8fg"
    3. input += '='.repeat(4 - (input.length % 4))
    4. "5Lit5Zu954mb6YC8fg=="
  2. 处理结果移除末尾空白符

    1. return output.replace(/\0+$/, '')

至此,前端类库修复完成。

三、前端类库无修复条件(无限复制导致维护成本过高)

实际业务过程中,还可能遇到这样的情况,前端类库无限复制,导致修改成本过高,我们只能曲线救国,从后端去解决补位等号导致的签名隐患。

根据base64编码的原理分析,我们可以发现,只要数据长度为3的倍数,则不需要补位等号。

  1. >>> base64_encode ('a')
  2. => "YQ=="
  3. >>> base64_encode ('aa')
  4. => "YWE="
  5. >>> base64_encode ('aaa') #长度为3
  6. => "YWFh"
  7. >>> base64_encode ('aaaa')
  8. => "YWFhYQ=="
  9. >>> base64_encode ('aaaaa')
  10. => "YWFhYWE="
  11. >>> base64_encode ('aaaaaa') #长度为6
  12. => "YWFhYWFh"

因此,我们的目标就是:让数据的长度扩充成3的倍数。

注意:

对于字符串来说,修改数据的长度可能会破坏数据本身,因此大家需要根据自身的业务场景来决定,切勿盲目修改。
对于数组来说,如果为其增加一个补位数据,不会破坏数据的使用,则可以通过此方式处理优化。

下面举例说明下针对数组的处理,从输出可以看出,当数组json后的数据长度不是3的倍数时,数组会追加一个补位数据_pad_,然后通过给他赋值补位,实现数组json后的数据长度满足3的倍数,从而得到最终无需补位末尾等号的base64_encode结果。

  1. <?php
  2. function autoFixArrayBase64Pad($arr)
  3. {
  4. $jsonOrig = $jsonStr = json_encode($arr);
  5. $jsonLen = strlen($jsonStr);
  6. if( $jsonLen % 3 != 0) {
  7. //加入填充数组
  8. $arr['_pad_'] = '';
  9. $jsonLen = strlen(json_encode($arr));
  10. //补齐缺失长度
  11. $autoPadLen = (3 - ($jsonLen % 3)) % 3;
  12. $arr['_pad_'] = str_repeat('_', $autoPadLen);
  13. $jsonStr = json_encode($arr);
  14. }
  15. echo "jsonOrig: " . $jsonOrig . ", len: " . strlen($jsonOrig) ."\njsonFix : " . $jsonStr
  16. . ", len: " . strlen($jsonStr) ."\nbase64Orig: " . base64_encode($jsonOrig)
  17. . "\nbase64Fix : " . base64_encode($jsonStr). "\n\n";
  18. return base64_encode($jsonStr);
  19. }
  20. for($i = 1; $i <= 3; $i++) {
  21. autoFixArrayBase64Pad(['name' => str_repeat('a', $i)]);
  22. }
  23. for($i = 1; $i <= 3; $i++) {
  24. autoFixArrayBase64Pad([str_repeat('a', $i)]);
  25. }
  26. # 输出
  27. jsonOrig: {"name":"a"}, len: 12
  28. jsonFix : {"name":"a"}, len: 12
  29. base64Orig: eyJuYW1lIjoiYSJ9
  30. base64Fix : eyJuYW1lIjoiYSJ9
  31. jsonOrig: {"name":"aa"}, len: 13
  32. jsonFix : {"name":"aa","_pad_":""}, len: 24
  33. base64Orig: eyJuYW1lIjoiYWEifQ==
  34. base64Fix : eyJuYW1lIjoiYWEiLCJfcGFkXyI6IiJ9
  35. jsonOrig: {"name":"aaa"}, len: 14
  36. jsonFix : {"name":"aaa","_pad_":"__"}, len: 27
  37. base64Orig: eyJuYW1lIjoiYWFhIn0=
  38. base64Fix : eyJuYW1lIjoiYWFhIiwiX3BhZF8iOiJfXyJ9
  39. jsonOrig: ["a"], len: 5
  40. jsonFix : {"0":"a","_pad_":"_"}, len: 21
  41. base64Orig: WyJhIl0=
  42. base64Fix : eyIwIjoiYSIsIl9wYWRfIjoiXyJ9
  43. jsonOrig: ["aa"], len: 6
  44. jsonFix : ["aa"], len: 6
  45. base64Orig: WyJhYSJd
  46. base64Fix : WyJhYSJd
  47. jsonOrig: ["aaa"], len: 7
  48. jsonFix : {"0":"aaa","_pad_":"__"}, len: 24
  49. base64Orig: WyJhYWEiXQ==
  50. base64Fix : eyIwIjoiYWFhIiwiX3BhZF8iOiJfXyJ9

参考资料:

base64编码原理介绍



未经同意禁止转载!
转载请附带本文原文地址:base64编码处理数据踩过的坑,首发自 Zjmainstay学习笔记
阅读( 9294 )
看完顺手点个赞呗:
(4 votes)

1.PHP cURL群:PHP cURL高级技术
2.正则表达式群:专精正则表达式
3. QQ联系(加请说明):QQ联系博主(951086941)
4. 邮箱:zjmainstay@163.com
5. 打赏博主:

网站总访问量: