Base64 编码器 / 解码器

为什么 Base64 总是让数据恰好膨胀 33%(数学原理)

Base64 接收二进制数据——即 8 位字节流——并只用 64 个不同的字符将其重新编码为文本:A–Z、a–z、0–9,外加两个额外字符(通常是 + 和 /)。选择 64 这个数在数学上是刻意的:64 恰好是 2 的 6 次方,因此每个输出字符代表 6 位输入。编码器把源比特流重新分组为 6 位一组,再将每组映射到 64 个字母表字符之一。 体积开销来自简单的算术。3 个输入字节包含 24 位,正好可以整除为 4 个 6 位分组。因此每 3 个输入字节变成 4 个输出字符——4/3 的比例,即体积增加 33.33%。如果输入长度不是 3 的倍数,编码器会用零位填充最后一组,并追加一个或两个 = 字符,让接收方知道最后一个四元组中实际有多少真实字节。一个 = 表示最后的四元组编码了 2 个源字节;两个 == 表示编码了 1 个源字节;没有 = 则表示是完整的 3 字节分组。 这解释了几个常见现象:base64 字符串的长度总是 4 的倍数。在许多实现中,填充在功能上是可选的(解码器可以根据长度取模推断出来),这就是为什么你会在 JWT 和现代 URL 安全场景中看到“去掉填充”的 Base64。严格遵循 RFC 4648 要求填充必须存在;很多现实软件对输入宽容,对输出严格。 33% 这个数字是渐近值。对于非常小的输入,由于填充取整,开销会更高——编码 1 个字节会产生 4 个字符,对那个字节而言是 300% 的膨胀。当输入达到几百字节以上时,开销渐近趋向 33.33%。在可打印 ASCII 范围内没有比它做得更好的方案;Base85(Adobe PDF 中使用)能把开销降到 25%,但使用了反斜杠和引号等在 JSON 或 URL 中会出问题的字符。Base64 处于紧凑性与可移植性之间的实用最佳平衡点。
Input bytes:    M       a       n
                |_______|_______|_______|
Binary:        01001101 01100001 01101110
Re-grouped:    010011 010110 000101 101110
Lookup A-Z..:  T      W       F      u
Output:        "TWFu"

// 1 byte: pads with ==
btoa("M");   // "TQ=="

// 2 bytes: pads with =
btoa("Ma");  // "TWE="

// 3 bytes: clean
btoa("Man"); // "TWFu"

// length always % 4 === 0
btoa("hello world").length;  // 16
btoa("a").length;            // 4 (was 1 byte → 4 chars)

URL 安全 Base64 与标准 Base64,以及 btoa 陷阱

RFC 4648 定义了两套字母表。标准 Base64 使用 + 和 / 作为最后两个字符,用 = 作填充。URL 安全 Base64(有时称“Base64URL”)将 + 替换为 -,将 / 替换为 _,并且通常去掉末尾的 = 填充。原因很简单:+、/ 和 = 在 URL、HTTP 表单体以及某些文件系统中都有保留含义。把标准 Base64 字符串不加转义地放进查询参数,会在第一个 + 处出错,服务器随后会把它解码成一个字面空格。 这就是 JWT 的头部 / 载荷 / 签名段全部只使用 Base64URL 的原因。令牌必须通过 HTTP Authorization 头、Cookie 和 URL 传递,而这些都与标准字母表相处不佳。用错方言,要么在接收端产生解析错误,更糟的是得到一个“几乎能用”的令牌——因为服务器恰好只在某些代码路径中把 + 规范化为空格,而在其他路径中不这样做。 浏览器的 btoa / atob 出现在 URL 安全 Base64 之前,只会生成标准字母表。更糟的是,它们只接受 Latin-1 输入——传入包含任何高于 U+00FF 的字符(任何非 ASCII / 非 Latin-1 字符,包括大多数 CJK 字符和 emoji)的字符串都会抛出 InvalidCharacterError。2026 年的正确做法是:先用 TextEncoder 把字符串编码成 UTF-8 字节,再对字节做 Base64 编码,然后视需要转换为 URL 安全形式并去掉填充。解码时按相反顺序进行。 Node.js 处理得更干净:Buffer.from(str, 'utf-8').toString('base64url') 直接生成 URL 安全的 Base64,没有填充,也没有字符集意外。在浏览器中,提案中的 Uint8Array.prototype.toBase64() / fromBase64()(截至 2026 年处于 TC39 stage 3)终于提供了干净的内置方案。在它覆盖到你的最低支持浏览器之前,下面这套 TextEncoder + btoa + replace 组合就是公认的标准变通方法。 一个简短的安全提示:永远不要认为 Base64 是安全意义上的“已编码”。它既不是加密,甚至算不上有意义的混淆。任何人用任何工具花五秒钟就能还原出字节。只把它用于传输,绝不要用它替代真正的密码学手段。
// Browser: UTF-8 safe Base64URL (no padding)
function toBase64Url(str) {
  const bytes = new TextEncoder().encode(str);
  let bin = '';
  for (const b of bytes) bin += String.fromCharCode(b);
  return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

function fromBase64Url(s) {
  s = s.replace(/-/g, '+').replace(/_/g, '/');
  while (s.length % 4) s += '=';
  const bin = atob(s);
  const bytes = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
  return new TextDecoder().decode(bytes);
}

// btoa breaks on non-Latin-1
btoa('한글');                  // InvalidCharacterError
toBase64Url('한글');           // "7ZWc6riA"  (works)

// Node 16+
Buffer.from('한글', 'utf-8').toString('base64url'); // "7ZWc6riA"

什么时候不该用 Base64——三个真实案例

Base64 有它的适用领域,而开发者经常把它拖出这个领域,结果得不偿失。以下是它常常弊大于利的三个场景: (1) 用 data URL 内联图片。把一个 4 KB 的图标嵌入为 <img src="data:image/png;base64,..."> 很有诱惑力。字节成本是 +33%,但真正的损害在于缓存失效。每个单独内联的资源都属于承载它的页面包,因此只要更新一个图标,就会让它所在的整个 HTML 文档或 CSS 文件的缓存失效。更糟的是,浏览器无法在页面之间共享这个图标——每个页面都要重新下载内联的字节。在 HTTP/2 和 HTTP/3 上,临界点大约在 1–2 KB,此时外部文件的每请求开销已经足够小,超过这个大小内联在所有指标上都会落败。只有在能保证不会变化、且保证出现在同一关键路径页面上的极小 SVG / CSS 中的 data URI 场景下,才考虑内联。 (2) 为了“节省空间”把结构化数据“编码”成一个 Base64 数据块。Base64 让数据变大 33%,而不是变小。如果你发现自己在做 JSON.stringify 然后 Base64 再 Gzip,请去掉中间那一步——gzip 直接处理原始文本没有任何问题,而 Base64 这一层只是给 gzip 制造更多冗余模式去压缩,既浪费编码时间,压缩率也比直接 gzip 原始 JSON 更差。Base64 用于把二进制数据搬过纯文本通道;如果你的通道本来就是文本,就不需要它。 (3) 把 JWT 或会话令牌存进会被记录的 URL 中。Base64URL 字母表对 URL 是安全的,但在任何隐私意义上都不是对日志安全的。Web 服务器、代理和 CDN 默认记录完整的请求 URL。一个把会话令牌塞进 ?token= 的“退出登录链接”,会把这个令牌永远泄漏到访问日志中,而 Base64 包装对它没有任何保护作用。要么接受这个值是公开的(一次性、短时效),要么把它移到 HTTP 请求体、HttpOnly Cookie,或者代理已配置为脱敏的 Authorization 头中。 贯穿始终的结论是:Base64 解决的是传输问题,而不是存储、安全或压缩问题。只有当你有一个二进制载荷和一个纯文本通道时才恰当地使用它,其他任何地方都不要用。
// BAD: data URL for a logo on every page
<img src="data:image/png;base64,iVBORw0KGgoAAAA...(20KB)..." />
// → busts HTML cache on every change, no cross-page reuse

// GOOD: external file with long cache TTL
<img src="/static/logo.v3.png" />
// → CDN caches once, reused everywhere

// BAD: redundant Base64 in a compression pipeline
const blob = btoa(JSON.stringify(data));
const compressed = gzip(blob);  // gzip is now LESS effective

// GOOD: gzip the JSON directly
const compressed = gzip(JSON.stringify(data));

// BAD: token in URL → leaked to access logs
<a href="/logout?token=eyJhbGciOi...">

// GOOD: Authorization header
fetch('/logout', { method: 'POST', headers: { Authorization: 'Bearer ' + token } });
最后更新:

关于此工具

Base64 是一种将二进制数据用 64 个 ASCII 字符表示的编码方案。常用于在邮件(MIME)、HTTP 头、JSON 负载以及 HTML/CSS 中的 data URL 等纯文本通道中传输二进制数据。约 33% 的体积开销,但能保证数据在任何文本环境中安全传输。

使用方法

  1. 选择 Encode 将纯文本转换为 Base64,或选择 Decode 从 Base64 恢复原始文本。
  2. 将输入内容——UTF-8 文本、JSON 或任意字符串——粘贴到输入框中。
  3. 输出会随着输入自动更新,无需点击提交按钮。
  4. 点击交换箭头可以对调输入和输出并切换模式,方便进行往返验证。
  5. 按下复制按钮获取结果,然后粘贴到你的代码、终端或 HTTP 客户端中。

常见用例

  • 使用 data: URL 将小图片直接嵌入 CSS 或 HTML,省去一次额外的 HTTP 请求
  • 在 Authorization 头中编码 HTTP Basic Auth 凭据
  • 在 JSON 负载中存储或传输二进制文件内容
  • 对 MIME 邮件附件进行编码,防止 SMTP 中继损坏内容
  • 查看 JWT 的各个段,因为头部和载荷是 Base64URL 编码的 JSON
  • 通过会剥离或破坏不可打印字符的系统发送二进制数据

常见问题

Q. Base64 是加密吗?

A. 不是。Base64 纯粹是一种编码——任何人都能立即解码。绝不要用它来隐藏机密;那种需求请使用 AES 或其他加密算法。

Q. 为什么我的 Base64 字符串末尾有 = ?

A. = 字符是填充,用来让输出长度成为 4 的倍数。有些实现会省略填充(URL 安全 Base64),但标准 Base64 会保留它。

Q. 什么是 Base64URL,它有什么不同?

A. Base64URL 把 + 替换为 -,把 / 替换为 _,使输出可以安全地放在 URL 和文件名中。JWT 使用这个变体。

Q. Base64 会让我的数据膨胀多少?

A. 大约比原始二进制大 33%,因为每 3 个字节会变成 4 个 ASCII 字符。