二进制的数据,每个字符的取值范围都是[0, 255]
,作为 ascii 码解析时,只有部分可打印。
比如,我用文本编辑器 vim 打开一张 jpeg 图片,会发现内容是乱码。以下是头两行数据:
ÿØÿà^@^PJFIF^@^A^A^@^@^A^@^A^@^@ÿÛ^@C^@^F^D^E^F^E^D^F^F^E^F^G^G^F^H
^P
我在 vim 下输入:%!xxd
,可以查看二进制数据对应的十六进值。以下是头两行数据:
00000000: ffd8 ffe0 0010 4a46 4946 0001 0100 0001 ......JFIF......
00000010: 0001 0000 ffdb 0043 0006 0405 0605 0406 .......C........
注意,下面的两行和上面的两行内容长度不一定是相同的。上面的数据是遇到 ascii 码为换行时换行,下面的数据是每 16 个字节换行。
比较上下两份数据,可以看到JFIF
在上面也打印了,在下面的右半部分也打印了。说明他们的数据源确实是同一份,只是展示方式不同。
显然,如果我想肉眼看这份二进制数据,或者说作为文本拷贝这份数据,16 进制的格式要优于二进制 ascii 码格式。
但是,16 进制表示法,需要两个字节才能表示表示原始数据的一个字节。比如4a464946
表示JFIF
。即大小增加了一倍。
有点大?于是,有人发明了 base64 算法。它保持了编码后可打印特性的同时,大小只增加1/3
。
base64 将原始二进制数据每三个字节(也即 24 位)看成一个单元,然后将 24 位按每 6 位进行一次切割,切割成 4 个字节。切割后每个字节的范围是[0, 63]
。
由于[0, 63]
的 ascii 码也并不都是可打印的,于是将[0, 63]
再一一对应换算成一个可打印的字符。
标准文档RFC 4648
对该映射关系定义如下:
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
也即 0 对应 A,1 对应 B,63 对应/
。
换算之后,三个字节就变成可打印的四个字节的内容了。
解码方的逻辑,先将每个可打印的字符按刚才的映射表反算出对应的值。然后每 4 个字节看成一个单位,按位运算还原出原始的 3 个字节。比如:解码前第 1 个字节的低 6 位作为解码后第 1 个字节的高 6 位,解码前第 2 个字节的低 6 位的前 2 位作为解码后第 1 个字节的低 2 位,这样就得到了解码后的第 1 个字节。后续依次按顺序推算。
还有一个问题,原始数据长度如果不是 3 的倍数怎么办?
那么无非是两种情况,一种是最后剩 1 个字节,另一种是剩两个字节:
==
=
即 base64 保证了编码后的字符串长度为 4 的倍数。
有的人会把二进制数据用 base64 编码后,放入 url 的参数中,这么做有一个问题,base64 编码后可能会出现+/=
三个字符,而这三个字符会影响到整个 url 串的解析。
举个例子,url 串https://pengrl.com/all?key=value
,如果将其中的 value 设置成1/2=
,则 url 串变成https://pengrl.com/all?key=1/2=
。其中的/
和=
是不是有点傻傻分不清楚呢?
那么如何解决呢?
编码后的+
和/
两个符号来源于上面给出的那张映射表。于是标准文档RFC 4648
中给出另一张映射表,将其中的+
替换成了中划线-
,/
替换成了下划线_
。
这里额外说一句,base64 并不适合用来做文本加密,因为算法是公开的,并且它只是一种简单的查表映射,有经验的 web 开发者,甚至看到末尾的=
都能猜到是 base64 编码。即使编码和解码都使用自定义的映射表,根据文本规律,也很容易破解出映射表。
剩下=
,上面也说过,是由于原始数据长度不是 3 的倍数填充得到的,解决方法也很简单,编码时不填充,解码时剩余的不够 4 字节的数据按顺序解就好。
上面的两种做法,都需要保证,编码端和解码端是能对上号的。
base64 主要还是对二进制做可打印编码,如果是处理 url 参数,最好还是使用 urlencode。urlencode 是啥,下回再聊。