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 陷阱
浏览器给了你 btoa 和 atob,而它们是个陷阱。两者处理的都是以字符串表示的字节,所以 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 校验进行解码,所以无效的字节序列会被报告为二进制,而不是被悄悄替换成问号。
最后审校于 。发现有内容过时了? 告诉我们.
