Base64

一种将二进制数据转换为 ASCII 字符串的编码方式,使用 A-Z、a-z、0-9、+ 和 / 共 64 个字符。

Base64 是一种将二进制数据转换为 ASCII 字符串的编码方式。它使用 A-Z (26 个)、a-z (26 个)、0-9 (10 个)、+ 和 / 共计 64 个字符,将 3 个字节的数据表示为 4 个字符。当输入数据长度不是 3 的倍数时,末尾会添加填充字符 (=)。

Base64 之所以必要,是因为电子邮件和 HTTP 等基于文本的协议无法直接传输原始二进制数据。它广泛应用于电子邮件附件 (MIME)、数据 URI 方案 (data:image/png;base64,...)、JWT (JSON Web Token) 以及 API 的请求/响应体等场景。例如,把小图标图片以数据 URI 的形式经 Base64 编码直接嵌入 HTML,可以减少 HTTP 请求数量。

在 JavaScript 中,btoa() 用于编码,atob() 用于解码。但这两个函数只能处理 Latin-1 字符,包含中文、日文等多字节字符时需要配合 TextEncoder 使用。在 Node.js 中则使用 Buffer.from(data).toString('base64')。Python 标准库提供了 base64 模块,Java 提供了 java.util.Base64 类。

一个常见的误解是把 Base64 与加密混为一谈。Base64 说到底只是编码 (可逆转换),不提供任何安全方面的保护。任何人都能轻松解码,因此不能用来保护密码或机密信息。此外还要注意数据大小会增加约 33%:3 个字节变成 4 个字符,所以结果是原始数据的 4/3 倍。用数据 URI 嵌入图片虽然减少了请求数,但 HTML 会相应膨胀,而且图片无法再作为独立文件被缓存。判断标准是:仅限几 KB 以内的图标或分隔线;照片这类较大的图片,仍以文件形式分发更有利。

在 URL 或文件名中包含 Base64 字符串时,标准的 + 和 / 会被当作特殊字符处理而引发问题。为此人们使用把 + 换成 -、把 / 换成 _ 的 base64url。规范出处是 RFC 4648"The Base16, Base32, and Base64 Data Encodings"(2006 年,废止了 RFC 3548),标准 Base64 定义在第 4 节,base64url 定义在第 5 节。JWT 签名部分使用的正是这种形式,JWS 的规范 (RFC 7515,2015 年) 规定在采用 base64url 的基础上省去末尾所有的 =,并且不得包含任何换行或空白。这就是 JWT 令牌中看不到 = 的原因。

实现时容易被绊倒的地方是换行的处理。RFC 4648 规定,除非引用它的规范明确要求,否则不得在 base64 输出中插入换行。另一方面,电子邮件的 MIME 按 76 个字符换行,PEM 格式的证书和密钥按 64 个字符换行,因此从这些来源取出的字符串里会混有换行。RFC 4648 同时规定,解码器必须拒绝含有字母表之外字符的数据 (base64 的字母表是 62 个字母数字字符加上 + 和 / 共 64 个字符,除此之外只有填充用的 = 被允许;像 MIME 那样另有允许忽略的规范时除外),所以把带换行的字符串交给严格的解码器就会报错。当复制粘贴来的密钥或令牌无法解码时,首先怀疑换行和首尾空白是个好习惯。

从字符计数的角度看,Base64 编码后的字符串长度可以根据原始数据的字节数精确算出。带填充的标准形式为 ceil(n / 3) * 4 个字符,省略填充的形式为 ceil(4n / 3) 个字符。一张 100 KB (102,400 字节) 的图片会变成 136,536 个字符,把它直接嵌入 HTML,页面的字符数就会相应增加。在换行的格式下,换行符也会被逐个计入,因此按 76 个字符折行的 136,536 字符输出会额外加入约 1,800 个换行。在需要把字符数与字节数对照的场合,先确认统计时是否把换行算进去了,就更容易查出差值的来源。

分享这篇文章