Base64 在开发日常里出现的频率,可能比绝大多数其他编码都高:邮件附件、JSON 接口、JWT、data URI、Kubernetes 的 Secret……很多开发者用了好几年,却从没坐下来搞清楚它到底在做什么。这篇文章就来补上这一课。读完你应当明白那 64 个字符是什么、为什么输出永远比原数据大三分之一、三种常见变体差在哪,以及 btoa('中文') 为什么会在浏览器里直接抛错。
Base64 到底是什么
Base64 是一种把任意二进制数据表示成文本的方式。这句话就是它的全部。它接收一串字节,输出一串只由 64 个特定字符(外加一个填充符)组成的字符串。这里没有密钥、没有秘密、也没有压缩——任何人拿到这串字符,都能在瞬间把它还原回去。
它之所以重要,是因为很多系统从设计上只能传文本、不能传字节。早期的电子邮件(SMTP)按 7 位 ASCII 设计;JSON 和 XML 是文本格式;URL 也是文本。如果你把一张图片的原始字节或者一个 ZIP 包硬塞进这些东西里,就会撞上一些有特殊含义的字符,被丢弃或者把通道搞乱。Base64 给这些字节套上一层"纯文本伪装",让它们能安全通过。
那 64 个字符的字母表
标准 Base64 的字母表是:
A–Z (26 个大写字母)
a–z (26 个小写字母)
0–9 (10 个数字)
+ / (2 个符号)
= (填充符,不算在 64 个之内)
26 + 26 + 10 + 2 = 64,恰好是 2 的 6 次方。所以每个符号能表示 6 位信息。填充符 = 只在输入长度不是 3 的倍数时才加在末尾,它本身不携带任何数据。
为什么体积会膨胀到 4/3
根本原因很机械:每个输出字符只装 6 位,而一个字节有 8 位,这个开销是算数逼出来的:
- 3 个字节 = 24 位。
- 24 位 ÷ 每个字符 6 位 = 4 个字符。
- 所以 3 字节进、4 字符出,体积固定膨胀到 4/3,也就是大约 33%。
实际里一个 3 MB 的文件编码后大约是 4 MB,一个小 JSON 字段也会被同比例撑大。这是信息论的结果,不是某个库的怪癖。如果你想让数据变小,应该去用压缩(gzip、zstd)——Base64 只会让它更大。
三种常见变体
| 变体 | 改动 | 典型场景 |
|---|---|---|
| 标准 | 用 + 和 /,保留 = 填充 | 通用、PEM 文件、密码学 |
| URL 安全 | +→-,/→_,常省略 = | JWT、查询串、文件名 |
| MIME | 每 76 个字符插入一个换行 | 邮件正文(RFC 2045) |
URL 安全变体存在的原因很具体:+ 和 / 在 URL 里都是保留字符。查询串里的 + 常被当成空格,/ 又是路径分隔符。把它们换成 - 和 _——这两个符号不在任何 URL 特殊含义列表里——文本就能在 URL 里完整往返而不被破坏。注意填充也随之变化:很多 URL 安全实现会直接丢掉 =,在解码时再根据长度推断出来。
最容易踩的坑:多字节字符
这是浏览器端 JavaScript 里最常见的 Base64 错误。新手会写出这样的代码:
btoa('中文') // 抛错:InvalidCharacterError
btoa('😀') // 同样抛错
原因是 btoa 把输入当成 Latin-1 字符串处理——每个字符都必须塞进单个字节(0–255)。而 JavaScript 的字符串是 UTF-16 的,任何超出 Latin-1 的内容(比如中文、emoji)根本没地方放,于是直接报错。
正确的路子是先用 TextEncoder 把文本转成原始字节,再逐字节送进 btoa:
function toBase64(str) {
const bytes = new TextEncoder().encode(str);
let binary = '';
for (const b of bytes) binary += String.fromCharCode(b);
return btoa(binary);
}
function fromBase64(b64) {
const binary = atob(b64);
const bytes = Uint8Array.from(binary, c => c.charCodeAt(0));
return new TextDecoder().decode(bytes);
}
反向解码时同理:先 atob,把结果拼成 Uint8Array,再交给 TextDecoder。少了 TextDecoder 这一步,任何非 ASCII 内容都会变成乱码。在 Node.js 里这个问题已经被解决了:Buffer.from(str, 'utf8').toString('base64') 会正确处理编码,所以服务端代码很少撞到这堵墙。
Base64 的边界在哪
Base64 很简单,但有些边缘情况忘了就会咬人:
- 截断。把一串 Base64 从中间砍断,解码要么失败、要么悄悄丢掉尾巴。你没法"只看前半段"。
- 换行。MIME 变体会插入换行;如果你用一个拒绝空白字符的严格解码器去解 MIME 字符串,它会报错。当数据来源是邮件或 PEM 时,解码前先清掉空白。
- 丢了填充。有些编码器会省略
=。大多数解码器能容忍,但严格的不能。和未知系统对接时,先规整填充再说。 - 大小写敏感。Base64 区分大小写,
AB和ab解码出来的字节完全不同。
它是编码,不是加密
跟我念一遍:Base64 什么都不隐藏。它是一种没有秘密成分的可逆变换,谁拿到字符串都能解。用 Base64 来"保护"密码、密钥或者会话令牌,等于把内容用更大、更好认的字体重写了一遍。
真正需要保密时,传输层用 TLS,对称加密用 AES-GCM,密码存储用 bcrypt 或 argon2(严格说它们是哈希而非加密)。Base64 干的活纯粹是机械性的:把字节搬过只能传文本的世界。
想自己试一试?到 Base64 编解码 里粘一段带重音的字符或 emoji,再勾上 URL 安全选项,看看字母表怎么变。