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 个字符。
对每一块:
- 取 3 个字节,把它们写成连续的 24 个比特。
- 把这 24 个比特切成四组,每组 6 比特。
- 把每组读作 0 到 63 之间的一个数。
- 在字母表中查出这个数对应的字符并输出。
字母表是一张固定的表。索引 0 是 A,25 是 Z,26 是 a,51 是 z,52 是 0,61 是 9,62 是 +,63 是 /。完整的表格见 Base64 字母表页面。
| 范围 | 字符 | 示例 |
|---|---|---|
| 0–25 | A–Z | 19 → T |
| 26–51 | a–z | 46 → u |
| 52–61 | 0–9 | 52 → 0 |
| 62 | + | |
| 63 | / |
解码就是把同样的步骤倒过来:查出每个字符对应的 6 比特,把四个拼成 24 比特,再分成 3 个字节。
完整示例:“Man”变成 TWFu
经典示例使用 ASCII 文本 Man。在 ASCII 中,M 是 77,a 是 97,n 是 110。
| M | a | n | |
|---|---|---|---|
| 十进制 | 77 | 97 | 110 |
| 二进制 | 01001101 | 01100001 | 01101110 |
把这些比特拼起来:010011010110000101101110。现在改为每 6 比特切一刀,而不是每 8 比特:
| 6 比特分组 | 010011 | 010110 | 000101 | 101110 |
|---|---|---|---|---|
| 值 | 19 | 22 | 5 | 46 |
| 字符 | T | W | F | u |
所以 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 比特分组 | 010011 | 010110 | 000100 | (无) |
|---|---|---|---|---|
| 值 | 19 | 22 | 4 | |
| 字符 | T | W | E | = |
第三组是 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 是一种压缩吗?
不是。它总会让数据变大:每 3 个输入字节变成 4 个输出字符,大约多出 33%。精确公式见 Base64 体积开销。
为什么叫 Base64?
每个输出字符承载 64 种可能取值之一,也就是 6 比特信息。名字来自字母表的大小,就像 hex 是十六进制(base 16)一样。
任何数据都能用 Base64 编码吗?
能。算法处理的是字节,并不关心它们的含义,所以文本、图片、PDF、密钥和压缩包的编码方式完全一样。文本需要先用某种字符编码(通常是 UTF-8)转换成字节。
Base64 输出中会出现空格或换行吗?
纯粹的 RFC 4648 Base64 不会。有些格式会把它折成多行,例如 MIME 邮件每行 76 个字符、PEM 文件每行 64 个,大多数解码器会跳过这些空白。