先给结论

Base64 是一种“编码”(Encoding),不是“加密”(Encryption)。它把任意二进制数据变成一串由 64 个可打印字符组成的文本,主要目的是让二进制数据能在只能传输文本的场合安全通过,而不是保密。任何人都能把它还原回去,因为它压根没有“密钥”这回事。

一句话记住:编码是为了“能传”,加密是为了“不能看”。Base64 属于前者。

诞生背景:邮件系统为什么需要它

要理解 Base64,得先回到互联网还很“文本化”的年代。1980 年代前后的电子邮件协议(RFC 822、SMTP)在设计上只保证 7 位 ASCII 文本可靠传输:消息按行组织、每行有长度限制,沿途的网关还可能把第 8 位“吃掉”,或把某些字节当作控制符处理。图片、程序、压缩包这类二进制文件一旦直接塞进邮件正文,几乎必然被改得面目全非。

于是人们想到一个朴素的办法:先把二进制“翻译”成纯文本再发送,收件端再“翻译”回去。Unix 社区在 1980 年就有了 uuencode;1993 年 PEM 标准(RFC 1421)首次定义了今天这套 A–Z、a–z、0–9、+、/ 的字符表;1996 年 MIME(RFC 2045)把它正式纳入邮件标准并命名为 Base64,成为邮件附件的基石;2006 年 RFC 4648 又把它和 URL-safe 变体统一规范。直到今天,你收到的每一封带附件的邮件,附件几乎都经历过这样一趟“文本化”旅程。

所以 Base64 从诞生起就带着一个朴素的目标:保证字节原样抵达,而不是保证别人看不懂。这个出身决定了它后来的一切性质。

顺带一提,MIME 还定义了另一种传输编码 quoted-printable:当邮件以文本为主时,它只对少数非 ASCII 字节做转义,几乎不增加体积;而二进制内容一律交给 Base64。两者分工明确,这也印证了 Base64 的定位始终是“文本化二进制”,而不是“保护内容”。

原理拆解:64 个字符与 6 位分组

字符表为什么是这 64 个

6 个 bit 能表示 2 的 6 次方等于 64 种取值(0–63),所以需要一张恰好 64 项的字符表。标准表按约定顺序排列:

  • 大写字母 A–Z:对应索引 0–25;
  • 小写字母 a–z:对应索引 26–51;
  • 数字 0–9:对应索引 52–61;
  • + 对应索引 62,/ 对应索引 63。

这 64 个字符全部来自 7 位 ASCII 的可打印区,在任何邮件系统、终端、文本编辑器里都不会被当作控制符或换行符,这正是当初选它们的原因。为什么是 6 位一组而不是 8 位一组?因为 8 位需要一张 256 项的字符表,而 ASCII 可打印字符总共只有 95 个,凑不齐;6 位只需要 64 项,正好全部落在可打印区里,任何文本通道都能安全承载。

每 3 个字节拆成 4 组 6 bit

编码过程本质上是一次“重分组”:把输入看成连续的 bit 流,从前往后每 6 个 bit 切一刀,得到一个 0–63 的整数,再查表换成字符。因为 3 个字节恰好是 24 个 bit,正好切成 4 组,所以 Base64 以“3 字节进、4 字符出”为基本单位,输出长度恒为输入的 4/3 倍,也就是大约膨胀 33%。

切分时始终从最高位开始、按固定位序处理,跨语言实现必须遵守同一约定,否则同一段数据会编出完全不同的结果。所幸 RFC 4648 把位序和查表顺序都写死了,主流语言的标准库行为一致,这也是“任何语言编出来的结果都一样”这一好用的性质。

3 字节 × 8 bit = 24 bit = 4 组 × 6 bit。数学上刚好对齐,是整套设计最优雅的地方。

补齐规则:= 号从哪来

当输入字节数不是 3 的倍数时,最后一组凑不满 24 个 bit,规则是末尾补零:

  • 剩 1 个字节(8 bit):末尾补 4 个 0,凑成 12 bit,编码出 2 个字符,再补两个 =
  • 剩 2 个字节(16 bit):末尾补 2 个 0,凑成 18 bit,编码出 3 个字符,再补一个 =
  • 恰好整除:不需要补齐,也没有 =

因此 Base64 结果的长度总是 4 的倍数,= 只出现在结尾且最多两个。解码时数一下 = 的个数,就知道末尾要丢弃多少 bit。注意 = 是填充符而不是数据,URL-safe 变体甚至会把它去掉,解码前再补回来。

剩 1 字节:01001101 0000 → 010011 010000 → T Q     → "TQ=="
剩 2 字节:01001101 01100001 00 → 010011 010110 000100 → T W E → "TWE="
正好 3 字节:01001101 01100001 01101110 → 010011 010110 000101 101110 → T W F u → "TWFu"

手算完整例子:“Man” 变成 “TWFu”

一步一步走完 “Man” 的编码。先查出三个字符的 ASCII 码并转成二进制:

M = 0x4D = 01001101
a = 0x61 = 01100001
n = 0x6E = 01101110

把 24 个 bit 首尾相接,从高位开始每 6 位切一组:

拼接:01001101 01100001 01101110
分组:010011 | 010110 | 000101 | 101110
索引:  19       22       5       46
查表:  T        W       F        u

查表过程:索引 19 落在 A–Z 区间(0–25),数过去是 T;索引 46 落在 a–z 区间(26–51),46 − 26 = 20,即 a 往后数 20 个字母得到 u。四组拼起来就是 TWFu,与任何编程语言里 base64("Man") 的结果一致。整个过程可逆:字符换回索引、索引拼回 bit、bit 按 8 位还原成字节,就得到原文。

与 Hex、Base32、URL-safe Base64 的区别

同为“二进制转文本”,不同方案在每字符承载的位数、字符集和体积开销上各有取舍:

编码每字符承载 bit字符集体积膨胀典型场景
Hex40–9、a–f约 100%哈希摘要、颜色值、调试输出
Base325A–Z、2–7约 60%密钥与 OTP 种子展示、文件名
Base646A–Z、a–z、0–9、+、/约 33%邮件附件、Data URL、JSON
base64url6A–Z、a–z、0–9、-、_约 33%URL 参数、JWT、Cookie

两点提醒:其一,每字符承载的 bit 越多,膨胀越小,但字符的选择余地也越小,这正是 Base64 比 Hex 省空间的原因;其二,base64url 只是把 +/ 换成 -_ 并去掉 =,算法与标准 Base64 完全一致,解码时换回来即可。

再说说 Base32 的定位:它的字符集刻意避开了 0 与 O、1 与 l 这类容易看混的字符,方便人类手抄、电话播报和核对,两步验证的种子密钥常选它;代价是体积膨胀约 60%,只适合对体积不敏感、但对可读性敏感的场合。

五个真实用途

  1. 邮件附件(MIME):发送端把附件按 Base64 编码成纯文本放进邮件体,收件端解码还原。它解决了 SMTP 时代只能可靠传输 7 位文本的难题,今天几乎所有邮件客户端都在背后做着这件事。
  2. Data URL 内嵌资源:data:image/png;base64,... 的形式把图标、小字体直接写进 HTML 或 CSS,省一次 HTTP 请求。适合几十 KB 以内的小资源;大文件会因 33% 的体积膨胀拖慢首屏,得不偿失。
  3. JSON 等文本协议传二进制:JSON 只能表达文本,接口里要传图片、文件或任意二进制时,转成 Base64 字符串放进字段是最省事的通用做法,前后端各编解码一次即可。
  4. JWT 与 URL-safe 场景:JWT 的头部、载荷、签名三段都先用 base64url 编码再拼接,配合签名算法在 URL、Cookie 中安全传递。标准 Base64 的 +/= 在这里会破坏 URL 语义,必须换用变体。
  5. 文本型存储里的二进制:数据库 VARCHAR/TEXT 列、配置文件、XML 报文中要保存小文件、密钥或证书时,Base64 是最省心的中间格式,跨系统、跨语言都不会走样。

三个常见坑深入剖析

坑一:拿它当加密用

Base64 结果长得“看不懂”,很多人就误以为它加密了。实际上它没有密钥、算法公开、完全可逆,任何人拿到字符串在线解码即可还原;而且编码长度与原文长度近似线性,连“隐藏数据量”都做不到。把密码、token 做一次 Base64 就存进数据库或塞进 URL,等于明文裸奔。需要保密必须走真正的密码学:传输用 TLS,静态存储用 AES 等对称加密,口令校验用加盐哈希(bcrypt、argon2)。记住一句:可逆且无密钥的变换,永远不是加密

坑二:中文乱码,忘了“字节”这一层

Base64 编码的对象是字节,不是字符。中文必须先按某一种字符集(通常 UTF-8)转成字节再编码;同一句话用 UTF-8 和 GBK 编码会得到完全不同的 Base64 结果,解码时也必须用编码时相同的字符集还原,否则必然乱码。例如“喵”的 UTF-8 字节是 E5 96 B5,编码结果是 5Za1

// 先把中文按 UTF-8 转成字节,再做 Base64
const bytes = new TextEncoder().encode("喵"); // [229, 150, 181]
btoa(String.fromCharCode(...bytes));          // "5Za1"

解码端若拿错字符集去还原,看到的就不是原来的那句话。

坑三:URL 里的 +、/、= 会捣乱

在 URL 查询串中 + 会被解析成空格,/ 会改变路径层级,= 会干扰参数分隔,直接把标准 Base64 塞进 URL,轻则解码出错,重则路由 404。举个具体例子:一段 Base64 里含有 +,直接拼进 ?token=... 参数后,服务端解析查询串时会把 + 当成空格,解码立刻失败,这也是 JWT 等标准必须使用 base64url 的原因。解决方式是换用 base64url,或对特殊字符做百分号转义。还有一个容易忽略的细节:MIME 规范要求 Base64 每 76 个字符折行,某些实现会插入换行符,拷贝粘贴后混入的换行会导致解码失败,处理时先去掉空白字符。

常见问题(FAQ)

Base64 能多层嵌套吗?

能,对结果再做一次 Base64 完全合法,解码时逐层还原即可。但除了徒增体积没有任何意义,还会让排错变得更痛苦,实践中没人这么做。

Base64 之后数据会变大多少?

理论上最小为 4/3 倍,约膨胀 33%,加上 = 补齐和可能的换行会略多。对比之下 Hex 是 2 倍、Base32 是 1.6 倍,这也是 Base64 在通用场景中最常用的原因。

为什么有时结果没有 = 号?

因为输入字节数恰好是 3 的倍数,不需要补齐。= 只是填充符、不携带信息,解码端会按规则丢弃。

Base64 和 UTF-8 是什么关系?

它们是不同层次的东西:UTF-8 是把字符映射成字节的字符集,Base64 是把字节映射成可打印文本的编码。必须先由字符集定下字节,才谈得上 Base64 编码,这也是中文乱码问题的根源。

用工具边看边验证

纸上得来终觉浅。打开本站的 Base64 编解码工具,按这个顺序试一轮:先输入 Man 看是不是 TWFu;再输入一个或两个字符,观察结尾的 = 号怎么变;然后输入一段中文,对比不同字符集下的结果;最后切到 URL-safe 模式,看看字符表发生了什么变化。折腾一遍,比看十遍文字记得牢。

行动小结

  • 先分清概念:编码可逆、无密钥,加密有密钥、面向保密;Base64 属于前者。
  • 编码文本前先确定字符集(默认 UTF-8),解码时用同一个字符集还原。
  • 数据要进 URL、Cookie 或文件名时,使用 base64url 变体。
  • 需要保密就走密码学:传输用 TLS,静态存储用 AES,口令用加盐哈希。
  • 拿不准就动手验证:用工具编解码几次,建立直觉。