[指南] 9 min read

[指南] Base64 是什么?它是如何工作的?

一篇从第一性原理出发的实战讲解。弄懂 Base64 背后的 6-bit 分组技巧、为什么输出会大 33%、= 填充从哪儿来、以及什么时候该用它。

April 2026 | fundamentals

// 一句话定义

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 对比——对照表