彻底弄懂 base64 及其原理

>![](https://upload-images.jianshu.io/upload_images/47789-62d133dd5bc0c72d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
转自 https://mp.weixin.qq.com/s/TcSNPY1a6z8kP76usH6dCA

### Base32 与 Base64

`Base32` 是一个 `binary-to-text encoding` 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` 定义了如下语法来识别网页中的资源：

```
 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`也只是说明了因为当时开发者们自己发明使用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 就容易啦，无非就是逆过程。

一串 base64 后的字符串，根据每个字符在 base64 字符表里找到对应的 10 进制，然后转成 2 进制，最后多余补足的 000000 去掉。

---

参考：

https://www.lucidchart.com/techblog/2017/10/23/base64-encoding-a-visual-explanation/

https://www.liaoxuefeng.com/wiki/1016959663602400/1017684507717184
https://www.zhangxinxu.com/wordpress/2018/08/js-base64-atob-btoa-encode-decode/
https://www.wikiwand.com/en/Binary-to-text_encoding
https://www.wikiwand.com/en/Data_URI_scheme
https://tools.ietf.org/html/rfc4648#page-3