上传文件
  1. base64hub
  2. 学习 Base64
  3. Base64 的工作原理
详解

Base64 的工作原理:逐字节拆解

Base64 用 64 个安全字符改写任意字节。输入 3 个字节,输出 4 个字符,其余不过是一些收尾的细节。

Base64 是什么,为什么会有它

Base64 是一种只用 64 个可打印 ASCII 字符来书写任意二进制数据的方法:A–Z、a–z、0–9、+ 和 /,另加用于填充的 =。它是一种编码,意味着任何人不需要密钥就能还原,它什么也藏不住(详见 编码与加密)。

它的目的是让字节完好无损地通过只能传输文本的通道。PNG 文件中含有 0x00、0x1A、0x89 这样的字节,文本通道可能会删掉、转换它们,或者把它们当成控制码。写成 Base64 后,同一个文件变成 iVBORw0KGgo...,可以原样通过电子邮件、JSON、XML、URL(使用 URL 安全变体)、终端和复制粘贴。

当前的定义是 RFC 4648。更早的 MIME 邮件规范(RFC 2045)和 PEM 证书规范描述的是同一套字母表,另外加了一些关于行长的规则。各变体的概览见 Base64 标准页面。

电子邮件为什么需要它

早期的电子邮件是为 7 位 ASCII 文本设计的。当时的邮件中继可能会去掉每个字节的最高位、折断长行、改写行尾,这对一封信无伤大雅,对 ZIP 文件却是致命的。MIME 需要一种能让任何附件通过这条管道的方法,也需要一套沿途每个系统都不会改动的字母表。

Base64 正是为此挑选了这 64 个字符。字母和数字在 ASCII 以及几乎所有传统字符集中都完全相同,+ 和 / 也差不多一样安全,而且它们都不是控制字符或空白。64 也是用这个安全字符集能凑出的最大的 2 的幂,而 2 的幂能让位运算保持简单。代价是体积:每个 8 位字符只承载 6 个有效比特,输出是输入的 4/3,再加上填充。

现代邮件服务器基本不再有 8 位的问题,但只要二进制数据需要放进文本格式,同样的限制就会出现。JSON 没有二进制类型,HTML 属性、CSS 和 HTTP 头也都是文本,所以 Base64 仍然是把字节放进它们的默认方式。

算法:输入 3 个字节,输出 4 个字符

Base64 以 24 比特为一块进行处理。24 是能同时被 8(一个字节)和 6(一个 Base64 字符)整除的最小的数,所以 3 个字节恰好对应 4 个字符。

对每一块:

  1. 取 3 个字节,把它们写成连续的 24 个比特。
  2. 把这 24 个比特切成四组,每组 6 比特。
  3. 把每组读作 0 到 63 之间的一个数。
  4. 在字母表中查出这个数对应的字符并输出。

字母表是一张固定的表。索引 0 是 A,25 是 Z,26 是 a,51 是 z,52 是 0,61 是 9,62 是 +,63 是 /。完整的表格见 Base64 字母表页面。

范围字符示例
0–25A–Z19 → T
26–51a–z46 → u
52–610–952 → 0
62+
63/

解码就是把同样的步骤倒过来:查出每个字符对应的 6 比特,把四个拼成 24 比特,再分成 3 个字节。

完整示例:“Man”变成 TWFu

经典示例使用 ASCII 文本 Man。在 ASCII 中,M 是 77,a 是 97,n 是 110。

Man
十进制7797110
二进制010011010110000101101110

把这些比特拼起来:010011010110000101101110。现在改为每 6 比特切一刀,而不是每 8 比特:

6 比特分组010011010110000101101110
值1922546
字符TWFu

所以 Man 编码为 TWFu。注意,没有任何一个输出字符与输入字母一一对应。W 由 M 的后 4 个比特和 a 的前 2 个比特组成。这就是为什么改动输入的一个字节会改变输出的两个字符,也是为什么即使输入是普通英文,Base64 也无法用肉眼读出来。

在任何 shell 中都可以验证:

echo -n Man | base64
# TWFu

不加 -n 时,echo 会多加一个换行字节,而 Man\n 编码为 TWFuCg==。

输入长度不是 3 的倍数时:填充

实际数据的长度很少能被 3 整除。这时最后一块只有 1 或 2 个字节,Base64 会用零比特和 = 号把它补满。

剩下两个字节,“Ma” → TWE=。两个字节是 16 比特。补两个零比特凑够 18 比特,即三组 6 比特:

6 比特分组010011010110000100(无)
值19224
字符TWE=

第三组是 a 的 0001 加上补的两个零,得到 4,也就是 E。一个 = 代替了缺失的第四个字符。

剩下一个字节,“M” → TQ==。一个字节是 8 比特。补四个零比特凑够 12 比特,即两组:010011(19,T)和 010000(16,Q)。两个 = 号把这一块补满。

所以 = 号的个数告诉你最后一块有几个字节:没有 = 表示 3 个,一个表示 2 个,两个表示 1 个。永远不会出现第三个 =,因为不存在剩余零个字节的块。填充指南介绍了什么时候可以省略 =,以及如何把它补回来。

import base64
for s in [b"Man", b"Ma", b"M"]:
    print(s, base64.b64encode(s))
# b'Man' b'TWFu'
# b'Ma' b'TWE='
# b'M' b'TQ=='

文本必须先变成字节

Base64 编码的是字节。文本只有在选定字符编码之后才会变成字节,而选择不同,结果也不同。单词 café 在 UTF-8 中是 5 个字节(é 占两个),编码为 Y2Fmw6k=;在 Latin-1 中是 4 个字节,编码为 Y2Fm6Q==。

这就是 JavaScript 中一个著名陷阱的根源。浏览器函数 btoa() 把字符串的每个字符当作一个字节,这是 Latin-1 的行为:

btoa("café");        // "Y2Fm6Q==" (Latin-1, not UTF-8)
btoa("café ✓");      // throws InvalidCharacterError

要在 JavaScript 中得到 UTF-8 的 Base64,请先用 TextEncoder 编码成字节,或者在 Node.js 中使用 Buffer。JavaScript 代码页给出了可行的写法,Uint8Array.toBase64() 介绍了较新的内置方法。本站的 Base64 编码器默认使用 UTF-8,也可以选择其他字符集。

你会在哪里遇到 Base64

  • 邮件附件。原始 .eml 文件中的每个附件都是一个每行 76 个字符的 Base64 块。见 电子邮件中的 Base64。
  • data URI。data:image/png;base64,iVBORw0KGgo... 把图片直接嵌入 HTML 或 CSS。这样做是否明智,见 data URI 性能。
  • HTTP Basic 认证。请求头 Authorization: Basic dXNlcjpwYXNz 只不过是 user:pass 的 Base64。
  • JWT。JSON Web Token 中由点号分隔的三个部分都是 Base64URL。见 JWT 中的 Base64。
  • PEM 文件。位于 -----BEGIN ...----- 行之间的证书和密钥,是二进制 DER 数据的 Base64。
  • API 和配置。Kubernetes Secret、云平台元数据、Webhook 载荷以及许多 JSON API,都以 Base64 字符串的形式传递文件或二进制数据。

时间长了,你会开始凭开头几个字符认出数据:iVBORw0KGgo 是 PNG,JVBERi0 是 PDF,/9j/ 是 JPEG,eyJ 是以 {" 开头的 JSON。常见 Base64 开头页面列出了更多。想看看某段数据里装的是什么,Base64 转文件工具会识别格式,并在你的浏览器中预览。

立即解码字符串

粘贴 Base64,查看其中的文本或字节。全程在你的浏览器本地运行。

Base64 解码器

常见问题

Base64 是一种压缩吗?

不是。它总会让数据变大:每 3 个输入字节变成 4 个输出字符,大约多出 33%。精确公式见 Base64 体积开销。

为什么叫 Base64?

每个输出字符承载 64 种可能取值之一,也就是 6 比特信息。名字来自字母表的大小,就像 hex 是十六进制(base 16)一样。

任何数据都能用 Base64 编码吗?

能。算法处理的是字节,并不关心它们的含义,所以文本、图片、PDF、密钥和压缩包的编码方式完全一样。文本需要先用某种字符编码(通常是 UTF-8)转换成字节。

Base64 输出中会出现空格或换行吗?

纯粹的 RFC 4648 Base64 不会。有些格式会把它折成多行,例如 MIME 邮件每行 76 个字符、PEM 文件每行 64 个,大多数解码器会跳过这些空白。

继续阅读