Base64 编码与解码

双向转换,完整支持 Unicode,需要时可切换 URL 安全字母表。

它是干什么用的

Base64 解决一个问题:你手上有任意字节,而通道只肯承载可打印文本。邮件头、JSON 的字符串字段、URL、HTML 属性,以及相当多的老旧基础设施,遇到裸二进制都会翻车。Base64 把每三个字节映射成四个字符,取自一个能穿过上述所有场合的 64 符号字母表。

这就是它的全部用途。它不是压缩——输出还大了三分之一。它不是加密——根本没有密钥。它是一种传输编码,而人们错误地加在它头上的每一种特性,都来自把「我看不懂这个」误认成「这个受到了保护」。

你会在哪里遇到它

  • JWT。 三段全都是,用的是去掉填充的 URL 安全字母表。
  • HTTP 基本认证。 Authorization: Basic 无非就是把 user:password 做了 Base64 编码——这正是为什么在纯 HTTP 上用 Basic 认证,等同于把密码明文发出去。
  • data URI。 data:image/png;base64,... 把一个文件直接嵌进 HTML 或 CSS。
  • PEM 证书和密钥。 -----BEGIN----------END----- 之间的那些行,就是 Base64 编码的 DER。
  • 邮件附件。 SMTP 是文本协议;MIME 用 Base64 来承载一切不是文本的东西。

Unicode 陷阱

浏览器给了你 btoaatob,而它们是个陷阱。两者处理的都是以字符串表示的字节,所以 btoa 遇到任何高于 U+00FF 的字符就会抛错。拿一个 emoji 试试,你会得到 InvalidCharacterError

解决办法是先转成 UTF-8 字节,本页做的正是这件事:

// Encode any string safely
const bytes = new TextEncoder().encode(text);
const b64 = btoa(String.fromCharCode(...bytes));

// Decode back
const binary = atob(b64);
const out = new TextDecoder().decode(
  Uint8Array.from(binary, c => c.charCodeAt(0))
);

编码那一行有个要注意的地方:把一个很大的数组展开传给 String.fromCharCode,在十万个元素上下就会把调用栈撑爆。数据一大就要分块循环——本页就是这么做的,所以它能处理几兆字节的粘贴而不倒下。

两套字母表

标准(RFC 4648 §4)URL 安全(§5)
索引 62+-
索引 63/_
填充=,必需通常省略
使用者MIME、PEM、基本认证JWT、URL、文件名

填充才是惹麻烦的那部分。它唯一的职责是把长度补成四的倍数,而解码器总能算出缺了多少——所以 URL 安全变体干脆把它丢掉。可严格的解码器随后就会拒绝这个结果,这就是为什么一段 JWT 粘进别的工具里那么容易失败。这里的解码器会自己把填充补回来。

它在哪些地方被误用

正因为 Base64 一眼看不懂,它被当成秘密来用。这几种具体模式值得点名:

  • 拿 Base64 存密码。 这是穿了戏服的明文。密码需要的是慢速单向哈希——bcrypt、scrypt、Argon2。
  • 把 JWT 载荷里的 Base64 当成私密的。 载荷对任何持有该令牌的人都是可读的,包括那位用户本人。绝不要往里面放任何你不愿意给他看的东西。
  • 用 Base64 把数据偷偷绕过过滤器。 这管用,但只管用一小会儿,而任何值得一用的扫描器都照样会解码它。

涵盖这三者的一条规则是:如果某样东西的安全性取决于读者不去解码它,那它就没有安全性。

相关

JWT 解码器会一步把令牌拆开并解析两个段。如果你要处理的是「把文本放进 URL 时的转义」而不是「编码字节」,那么百分号编码才是你多半想要的那件不同的东西。而如果你解出来的结果原来是 JSON,格式化器会把它排好。

Base64 相关问题

Base64 是加密吗?

不是。这一点值得说得直白些,因为这种混淆已经酿成过真正的事故。Base64 是一种可逆编码,没有密钥也没有秘密——谁拿到字符串谁就拿到了数据,解码只需要一行代码。它的存在是为了让字节能通过只允许可打印文本的通道,而不是为了藏住什么。把密码以 Base64 存起来,等于以明文存储,只是多了一道手续。

为什么很多 Base64 工具一遇到表情符号或带重音的字符就出错?

因为浏览器的 btoa 函数处理的是字节而不是字符,遇到任何高于 U+00FF 的内容就会抛错。直接调用它的工具在 é、在「日本語」、在每一个 emoji 上都会失败。本页会先编码成 UTF-8 字节,再以同样的方式解码回来,所以任何你能敲出来的文本都能干净地往返一圈。

什么是 URL 安全的 Base64?

标准字母表里包含 + 和 /,这两个字符在 URL 中各有含义,作为填充的 = 也一样。RFC 4648 定义的 URL 安全变体用 - 替换 +、用 _ 替换 /,并且通常去掉填充。JWT 的三段用的都是它,这也是为什么把一段 JWT 粘进标准解码器往往会失败,直到把填充补回来。这里的解码器两种字母表都接受,有没有填充都行。

为什么编码后的字符串比我输入的更大?

总是大约大三分之一。Base64 把输入的每三个字节表示为四个可打印字符,所以在填充之前,输出就是原来的 4/3。这就是只使用那些能穿过「无法承载任意字节」的系统的字符所付出的代价。这也是为什么在 CSS 里用 data: URI 内嵌图片,会让样式表明显比原来的图片文件更大。

我解码出来是一堆乱码,发生了什么?

最可能的情况是这些字节本来就不是文本。Base64 编码的是任意二进制——一张图片、一份证书、一个压缩包——把它们解码成字符串只会得到无意义的内容,因为那里根本没有文本可还原。本页会明确告诉你这一点,而不是给你看一堆乱码:它以严格的 UTF-8 校验进行解码,所以无效的字节序列会被报告为二进制,而不是被悄悄替换成问号。

最后审校于 。发现有内容过时了? 告诉我们.