[指南] Base64 是什么?它是如何工作的?
一篇从第一性原理出发的实战讲解。弄懂 Base64 背后的 6-bit 分组技巧、为什么输出会大 33%、= 填充从哪儿来、以及什么时候该用它。
// 一句话定义
Base64 是一种用 64 个可打印 ASCII 字符来表示二进制数据的文本编码方式:A–Z、a–z、0–9 以及两个符号(通常是 + 和 /,另外用 = 作为填充标记)。整个思路就是这么简单。每 3 个字节的二进制数据被重新切分为 4 个 Base64 字符,仅此而已。
它是一种编码(encoding),不是加密(encryption)——任何人都能在毫秒级内把它还原。它的目的不是保密,而是安全地传输。Base64 让你能够把二进制载荷(图片、证书、PDF 文件、加密密钥)塞进那些只接受文本的通道:HTML 属性、JSON 字符串、URL、邮件正文、YAML 配置、数据库 TEXT 列、环境变量等等。
// 为什么我们需要它
纯文本通道会把它们无法识别的字节丢弃或篡改掉。只理解 7-bit ASCII 的邮件网关会破坏所有非英文字符的高位。JSON 解析器会拒绝嵌入的 null 字节。URL 里如果出现字面量空格、引号或 & 号,在它们被百分号编码之前是无效的。1990 年代的 SMTP 标准默认邮件正文永远是纯英文——但人们同时又想要发送照片和表格。
Base64 通过把输出限制在 64 个能够穿越一切文本通道的字符来解决这个问题:26 个大写字母、26 个小写字母、10 个数字,再加上两个符号,这两个符号(a)可打印;(b)不被常见转义机制占用;(c)在 ASCII 里彼此区分度足够好,能完整往返。再加上一个同样安全的填充字符 =。
// 6-BIT 分组技巧
Base64 的核心机制一句话就能讲完:把输入当成比特流,切成 6-bit 为一组,然后每一组到 64 字符的字母表里查一下对应的字符。
为什么是 6 位?因为 2^6 = 64,每个 6-bit 的组正好对应字母表里的一个字符。为什么不是 7 位或 8 位?7 位会产生 128 个值(太多了——并非每个值都可打印),而 8 位则又回到了原始二进制。6 位是一个甜蜜点——每一个可能的值都对应一个人类可读的字符。
三个输入字节 = 24 位 = 恰好 4 组 6-bit = 4 个 Base64 字符。这个 3 进 4 出的比例是固定的;这也是为什么 Base64 输出大约是输入的 4/3,即膨胀约 33%。
// Worked example: encoding the 3 bytes 'Man' (77 97 110)
// ASCII: M a n
// Binary: 01001101 01100001 01101110
// Re-group into 6-bit chunks:
// 010011 010110 000101 101110
// Decimal: 19 22 5 46
// Base64: T W F u
// Result: 'TWFu' (stored as 4 ASCII bytes: 84 87 70 117)
// BASE64 字母表
RFC 4648 §4 定义了标准字母表。位置 0–25 是 A–Z,26–51 是 a–z,52–61 是 0–9,位置 62 是 +,位置 63 是 /。
URL-safe 变体(RFC 4648 §5,也叫 base64url)把 + 换成 -,把 / 换成 _,这样输出就能直接用在 URL 和文件名里而不需要再转义。只要应用对应的字母表,两种变体解码后的结果完全一致。
// Standard Base64 alphabet (index → character)
// 0–25: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
// 26–51: a b c d e f g h i j k l m n o p q r s t u v w x y z
// 52–61: 0 1 2 3 4 5 6 7 8 9
// 62: + (or - in URL-safe)
// 63: / (or _ in URL-safe)
// pad: =
// = 填充是怎么来的
算法期望输入长度是 3 的倍数字节(这样可以整齐地分成 4 个 Base64 字符为一组)。当输入长度不是 3 的倍数时,会剩下 1 或 2 个字节,不足以填满最后一组 4 字符的输出。用 = 做填充能指示缺了多少个字节。
• 输入长度 mod 3 == 0 → 无需填充(例如 'Man' → 'TWFu')。
• 输入长度 mod 3 == 1 → 填两个 = 字符(例如 'M' → 'TQ==')。
• 输入长度 mod 3 == 2 → 填一个 = 字符(例如 'Ma' → 'TWE=')。
// Why 'M' becomes 'TQ==':
// Byte: M = 01001101 (8 bits)
// Padded to 12 bits with zeros: 010011 010000
// Decimal: 19, 16 → 'T', 'Q'
// Output length must be multiple of 4 → add '==' padding
// Result: 'TQ=='
//
// When decoding, the decoder strips '==' and recovers the first 8 bits,
// discarding the trailing zero bits.
// 33% 的开销是精确值吗?
接近但不完全等于。精确公式是:输入长度为 n 字节时,输出是 ceil(n / 3) × 4 个字符。对于 100 KB 的输入,就是 ceil(102400 / 3) × 4 = 136,534 个字符——比 100 KB 多 33.3%。实际场景中,一旦加上 HTTP 层的 gzip 或 Brotli 压缩,线上的实际开销通常只有 10–15% 左右,因为 Base64 文本本身也还算容易压缩(虽然不如原始二进制那么好压缩)。
对于小载荷,填充的影响就明显了:对 1 个字节编码需要 4 个字符(体积变成 4 倍),对 2 个字节编码也需要 4 个字符。所以很短的 Base64 字符串相对输入看起来显得格外长。
// BASE64 在真实世界里的身影
-
>
HTML/CSS 中的 Data URI——把图标、logo 内联到页面:
<img src="data:image/png;base64,…"> - > JSON 与 GraphQL 载荷——需要通过文本协议传输二进制数据(文件上传、缩略图)的 API
- > JWT 令牌——一个 JWT 用点分隔的三段都是 base64url 字符串
- > HTTP Basic Auth——Authorization 头里的 username:password 经过 Base64 编码(如果不叠加 TLS,仍然不安全!)
- > 电子邮件(MIME)——附件用 Base64 编码,以便穿过老旧的 7-bit SMTP 服务器
- > PEM 格式的证书和密钥——-----BEGIN CERTIFICATE----- 块包裹的正是一段经过 Base64 编码的 DER 数据
- > SSH 密钥——id_rsa.pub 和 authorized_keys 里每一行都是 Base64 编码的公钥
- > 数据库 TEXT 列——当你无法使用 BLOB 类型时,Base64 让你可以把二进制按文本存
- > 环境变量——Kubernetes Secret 的值是 Base64 编码的(但并未加密)
- > 二维码和魔法链接——可安全嵌入 URL 的短小 Base64 令牌
// BASE64 不是加密
这是最常见、也最容易上当的一个误解。Base64 不会隐藏你数据的内容——它只是用一张公开、被充分文档化的映射表把二进制重写为文本。把 password 编码为 cGFzc3dvcmQ= 与直接传 password 一样脆弱;任何人一个函数调用就能解码。请把 Base64 当成跟十六进制一样的东西对待:它是一种表示形式,而不是一种防护手段。
如果你需要保密,请先对明文应用真正的密码学原语(AES-GCM、ChaCha20-Poly1305、age、PGP),然后才在需要通过文本通道传输时对密文进行 Base64 编码。Base64 是最后一公里,不是安全层。
Kubernetes Secret 是教科书式的陷阱:Kubernetes 用 Base64 存储 secret 值,这让它们看起来像是被保护过。其实并没有——任何拥有该 namespace 读权限的人都能把它还原。真正的 secret 保护需要 SealedSecrets、Vault、SOPS 或云厂商 KMS 集成这类工具。
// 各种语言里 30 秒速写的编/解码
// JavaScript (browser):
// btoa('Hello') → 'SGVsbG8='
// atob('SGVsbG8=') → 'Hello'
// (btoa/atob only handle Latin-1; use TextEncoder for Unicode)
//
// Node.js:
// Buffer.from('Hello').toString('base64') → 'SGVsbG8='
// Buffer.from('SGVsbG8=', 'base64').toString() → 'Hello'
//
// Python:
// import base64
// base64.b64encode(b'Hello') → b'SGVsbG8='
// base64.b64decode('SGVsbG8=') → b'Hello'
//
// Go:
// base64.StdEncoding.EncodeToString([]byte("Hello")) → "SGVsbG8="
//
// Ruby:
// Base64.strict_encode64('Hello') → 'SGVsbG8='
//
// Shell:
// echo -n 'Hello' | base64 → 'SGVsbG8='
// echo 'SGVsbG8=' | base64 -d → 'Hello'
// 要避开的常见错误
-
>
用标准字母表编码 URL-safe 字符串——接收方的解码器会拒绝
+和/。当输出要进入 URL、Cookie 或文件名时,请使用 base64url。 - > 重复编码——把已经是 Base64 的文本再丢给编码器。编码前一定要先判断数据是否已经是 Base64。
-
>
忘了 UTF-8——
btoa('héllo')在浏览器里会抛错,因为é已经超出 Latin-1。先用TextEncoder把字符串转成字节。 -
>
填充剥离不一致——base64url 通常省略
=填充。如果你的解码器要求严格匹配,就要把填充补回来:input + '==='.slice((input.length + 3) % 4)。 - > 把 Base64 当成安全手段——它并不安全。加密是另一回事。永远是。
- > 对超大二进制用 Base64——把一个 500 MB 的文件编码到一个字符串里会把内存耗尽。请改用流式处理。
-
>
换行 vs 不换行——MIME/PEM 会把 Base64 在第 64 或 76 列用
\n折行。大多数其他上下文期望单行。按需剥离或插入换行符。
// 30 秒版 BASE64 小抄
- > 64 个可打印 ASCII 字符(A–Z、a–z、0–9、+、/),外加 = 填充
- > 3 字节进 → 4 字符出(比例固定)
- > 输出比输入大约大 33%
- > 可逆:是编码,不是加密
- > URL 和文件名请用 base64url(+ → -,/ → _)
- > 短输入用 1 或 2 个 = 填充,使输出长度是 4 的倍数
- > 对 Unicode 文本:先把字符串编码为 UTF-8 字节,再对这些字节做 Base64
- > 主流语言的标准库都能解码
- > 在 HTML、JSON、URL、邮件、PEM 文件里都安全
- > 但作为安全层不安全——敏感数据请务必配合真正的加密
// 下一步
现在你已经懂了原理,去试试我们的 Base64 编码器或 Base64 解码器,逐字符地检查输出。需要 URL-safe 载荷的话,切到 base64url。要处理图片,看 图片 → Base64。
相关深入阅读:
• Base64 URL-safe 与标准版对比——何时用哪种字母表
• 用 Base64 处理 UTF-8 与 Unicode——避开 btoa() 陷阱
• JavaScript 与 Node.js 中的 Base64——atob、btoa、Buffer
• Base64、Base64URL 与 Base32 对比——对照表