一篇文章彻底弄懂 base64 及原理

按:base64如今仍然是常见的编码方式,尤其是在“原始数据是二进制,而传输协议只支持文本”的场合。可惜的是,许多开发人员并不清楚其中的原理,只知道“看起来毫无意义,但又有一大堆等号的”就是base64。恰好,
我的朋友胡永浩写了这篇文章,深入浅出讲解了base64,值得认真阅读。

一篇文章彻底弄懂 base64 及其原理

胡永浩

Base32, Base64

Base32
是一个 
binary-to-text encoding [1]

schemes,顾名思义,就是将二进制数据转换为编码只有基础 32 个字符的数据编码方式, Base64
则是 64 个。注意编码不等同于加密,网上有误解 Base 编码方式为加密方式,实际上标准 Base64 编码解码无需额外信息即完全可逆。

Base 编码常见用途如下:

如定义所言,binary to text

一些协议如  HTTP
,  FTP (File Transfer Protocol)
[当指定发送文本时],  SMTP (Simple Mail Transfer Protocol)
是  text-based protocol
,也就是只支持文本传输,不支持二进制传输。是的,http 上传文件,图片时使用的  multipart/form-data
也是需要转成文本的。

所以附件如图片,文件等(binary)就可以用  Base64
编码为 text再传输。

将资源编码为字符串

如 
data URI scheme [2]

定义了如下语法来识别网页中的资源:
data:[<media type>][;base64],<data>

HTML
中可以在标签中指定识别  Base64编码
来展示资源,

<div>
<p>Taken from wikpedia</p>
<img src="data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAAUA
AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO
9TXL0Y4OHwAAAABJRU5ErkJggg==" alt="Red dot" />
</div>

但因为  Base64
是每 3 个原始字符编码成 4 个字符,不够时补  =
(下文会详述),因此编码后的大小是有可能会比原文件大的,所以  html
用  Base64
来展示图片而不是用具体的图片好处大概就只有少建立一条 http 连接以及少一个 http 请求(在 HTTP 1.1 以下),这种办法只有大量的小图片才有优越性了。

统一转成『合法』字符

为了避免出现不符合规则的字符,方便把含有不可见字符串的信息用可见字符串表示出来。比如  http
协议当中的 headers 头部,必须进行  URLEncode
不然出现的等号可能使解析失败,空格也会使 http 请求解析出现问题,比如请求行也就是 request 就是以空格来划分的  POST /hi/you HTTP/1
,值得注意的是  Base32
的字符列表里有不合法字符  /

还有避免原始信息经过百花齐开的路由,网关多次转发,因有部分系统不支持此不可识别字符或将此作为控制符,将其转义、丢弃等,造成信息丢失,所以如电子邮件里的附件也是用 base64 编码的。

base64url

有 base64 编码的变种  base64url
,将base64 编码中的  +
换成  -
以及将  /
换成 _
,甚至不需要往后面补  =
了。这样子在 url 中传递东西时,不再需要  URL encode
,好处就是长度短了,以及好看了一点,毕竟  %
有点视觉污染(实际上,还可以直接将编码后的东西存数据库了,因为  base64
比  URLEncode
更通用了 )

Base64 的由来——参考 RFC

RFC 向来都不会说明设计的历史由来,自然  base64
编码也是一样,我参考的 
rfc4648 [3]

也只是说明了因为当时开发者们自己发明使用base 64并不规范,没有统一的标准,因此定义了一份通用标准。

然后呢, Base64
就是自己选了  ASCII
子集(64 个字符)为标准字符集,当然这也是因为 64 是  2的 x 次方
(如 64 就是 2 的6次方),而1个 bit 分别有 0和 1 两种状态,6 个 bit也就是 2 的 6 次方=64 个状态,刚好可以表示 64 个字符,因此  6 个 bit
就可以表达出 64 个字符了。就是下面定义的 64 个:

Table 1: The Base 64 Alphabet


Value Encoding Value Encoding Value Encoding Value Encoding
0 A 17 R 34 i 51 z
1 B 18 S 35 j 52 0
2 C 19 T 36 k 53 1
3 D 20 U 37 l 54 2
4 E 21 V 38 m 55 3
5 F 22 W 39 n 56 4
6 G 23 X 40 o 57 5
7 H 24 Y 41 p 58 6
8 I 25 Z 42 q 59 7
9 J 26 a 43 r 60 8
10 K 27 b 44 s 61 9
11 L 28 c 45 t 62 +
12 M 29 d 46 u 63 /
13 N 30 e 47 v
14 O 31 f 48 w (pad) =
15 P 32 g 49 x
16 Q 33 h 50 y

编码定义

The encoding process represents 24-bit groups of input bits as output
strings of 4 encoded characters.



输入:二进制(图片,文件,字符串本质就是二进制)



输出:编码后的字符串



处理过程:处理输入的二进制时,每 24 个 bit (3 个字节)作为一组,编码输出为  base64 处理
后的  4
 个标准字符集中的字符。

值得注意的是,网上的示例或说明中,都或多或少有以下偏颇之处:



输入的例子可以是16 进制数字、二进制、一串数字等,很多文章举的例子都是字符串;让人忽略  binary to text
的  binary



是每 24 位(同样需要注意不一定是 3 个 8 位的字符,3 个字节bytes才准确)为一组来处理,输出 4 个编码后的字符。强调这点是因为,24 位为一组,不够的都需要补  =
,如按其他人的文章说的 8 位 8 位的转,根本不清楚要补多少  =



24 位转成 4 个编码后的字符(也就是 4*8=32位),所以编码后的长度肯定会变大



综上所述,RFC 原文才是最对的定义,有时细微的区别意味着理解有问题。下面会一一说明。

特殊处理

When fewer than 24 input
bits are available in an input group, bits with value zero are added
(on the right) to form an integral number of 6-bit groups.
Padding at the end of the data is performed using the ‘=’ character.



每 24 位为一组来编码输入的 binary 时,如果最后的一组不足24 位,往后补 0直到 补足到 24



对于最后对于全为 0 的一组,补充  =

举一些例子来说明一下:

Input data: 0x14fb9c03d97e
16进制: 1 4 f b 9 c | 0 3 d 9 7 e
2进制: 00010100 11111011 10011100 | 00000011 11011001 01111110
6位一组: 000101 001111 101110 011100 | 000000 111101 100101 111110
Decimal: 5 15 46 28 0 61 37 62
Output: F P u c A 9 l +

16 进制的  0x14fb9c03d97e
作为输入,先转成二进制,然后 2 进制的每 24 位 选出来编码,上面例子就是: 00010100 11111011 10011100
,然后 6 位一组的分开,得到  000101 001111 101110 011100

然后分别转 10 进制,也就是  000101
变成 5, 001111
变成 15等,再去 base64 定义的字符列表中找出此 10 进制对应的字符,以此类推,就是 base64 后的结果了。

上面例子是输入刚好是有48 位, 2个 24 位,刚刚够,不需要补  =

下面看看需要补  =
的例子:

Input data: 0x14fb9c03
Hex: 1 4 f b 9 c | 0 3
8-bit: 00010100 11111011 10011100 | 00000011 开始补 0 =》00000000 00000000
pad
6-bit: 000101 001111 101110 011100 | 000000 110000 000000 000000
Decimal: 5 15 46 28 0 48
pad with = =
Output: F P u c A w = =
注意上述输入只有 32 位,第一个 24 位处理完后,还剩下 8 位,因此需要补16 个 0.
补完后,就是 48 位的输入了,照样每 24 位输出 4 个编码后的字符。

观察后半部分,  000000 110000 000000 000000
,第一个  000000
因为后面还有内容,所以10 进制为 0,因此编码字符为 A,这个很正常;而  1100000
之后的两个 6 位  0
,都是纯粹的填充(pading)了,因此并不用  A
而都用  =
代替掉
,注意不用  A

Base64 decode

说完 encode,decode 就容易啦,无非就是逆过程,RFC 都不屑于讲了。。。
一串 base64 后的字符串,根据每个字符在 base64 字符表里找到对应的 10 进制,然后转成 2 进制,最后多余补足的 000000 去掉,完了:cold_sweat:

URL_Encode
以及  Unicode
相关留待下次说。

References

[1]
binary-to-text encoding:  https://www.wikiwand.com/en/Binary-to-text_encoding

[2]
data URI scheme:  https://www.wikiwand.com/en/Data_URI_scheme

[3]
rfc4648:  https://tools.ietf.org/html/rfc4648#page-3