跳到主内容
d.devtul.fun
EN
编解码 · 2026-09-12

什么是 Base64 编码?写给开发者的入门说明

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 安全选项,看看字母表怎么变。

继续阅读