Base64 与 URL 编码:作用、陷阱、正确用法
摘要 (TL;DR)
执行 echo -n "Hi" | base64,输出恰好 4 个字符 SGk=。两个输入字节(16 比特)按 6 比特切片,补成 4 字符输出,整个算术就到此为止。把这条简单规则当成”凡事都包一层比较保险”的默认值,代价立刻堆起来——3 输入字节恒等于 4 输出字符就是约 33% 膨胀,我曾看到一个服务把 1.2MB 的 JPEG 包进 JSON 字段,传到线上变成大约 1.64MB。编码不是加密,也不是压缩,它只是用另一套字母表表达原本字母表的数据的规则,让数据通过一个本来会糟蹋它的通道。Base64(RFC 4648 §4)是为了把任意字节送过只懂文本的系统——邮件正文、JSON 字符串字段、内联图像。Base64URL(同 RFC §5)替换两个字符(+→-,/→_),让结果能安全地塞进 URL 路径、文件名、JWT。百分号编码(RFC 3986)是另一回事:它把对 URL 有结构意义的字符(?、&、#、空格、Unicode)转成不会冲突的 %XX 序列。按通道选编码:通道支持二进制就直接传,文本通道用 Base64,URL 嵌入数据用 Base64URL,URL 上下文里的字符用百分号编码。暴露出来的 Base64 字符串任何带解码器的人都能读,所以永远别把编码当成保密手段。
背景与概念
文本专用通道远在被要求承载图像之前就存在了。邮件历史上假定 7 比特 ASCII,会破坏高位置 1 的字节;HTTP 头至今对部分字符有限制;JSON 字符串字段是 UTF-8,但无法安全地容纳原始控制字节(0x00–0x1F)。RFC 4648 标准化的 Base64 通过把每 3 输入字节(24 比特)映射到 4 个 6 比特的输出字符来解决这个问题。字母表 64 个字符:A-Z(索引 0–25)、a-z(26–51)、0-9(52–61)、+(62)、/(63),加上 = 作填充让输出长度是 4 的倍数。1 字节输入产出 XX==(8 比特 → 6+2 加填充),2 字节产出 XXX=(16 比特 → 6+6+4 加填充),3 字节产出 XXXX(无填充)。
URL 安全变体只换掉字母表索引 62 和 63——+ 变 -,/ 变 _——因为传统表单提交里 + 解码为空格,/ 是路径分隔符。在很多 URL 安全上下文里 = 填充也会去掉;这是 JSON Web Token(RFC 7519)使用的形式,紧凑的 header.payload.signature 三段必须不重新转义就塞进 Authorization: Bearer ... 头。
百分号编码(也常称 URL 编码)是另一回事。RFC 3986 把 ASCII 分成两类:可原样出现的非保留字符(A-Z、a-z、0-9、-、.、_、~),以及具有结构意义、作为数据出现时必须编码的保留字符(:、/、?、#、[、]、@、!、$、&、'、(、)、*、+、,、;、=)。这个集合之外的任何字节——包括 UTF-8 编码的非 ASCII 字符的字节——都用十六进制写成 %XX。JavaScript 的 encodeURIComponent 比较激进,把保留字符也一起编码,以便安全嵌入到一个组件里;encodeURI 假设你传的是整个 URL,所以更宽容。
三种编码即使输出字节看起来重叠,也不可互换。一个百分号编码的查询参数仍是 ASCII 字符表达另一些 ASCII 字符;把它喂进 Base64 解码器只会得到无意义的字节。把标准 Base64 字符串直接放进 URL 路径会因里面的 / 被当作路径分隔符而悄悄出错——我见过同事为了这一个字符耗掉半天。生产里一半的编码 bug 都源于这种混淆。
对比与数据
| 项目 | Base64(标准) | Base64URL | 百分号编码 |
|---|---|---|---|
| 何时用 | 拒绝原始二进制的文本通道(MIME 正文、JSON 里的字节) | 同上,但要嵌进 URL、文件名或 JWT | URL 单一组件里的保留或非 ASCII 字符 |
| 字母表 | A–Z a–z 0–9 + / = | A–Z a–z 0–9 - _(填充可选) | 非保留集;其余变 %XX |
| 开销 | 约 33%(每 3 字节 4 字符),MIME 还加 76 字符换行 | 约 33%,常规无填充 | 可变——1 字节 UTF-8 变 %XX 三字符 |
| 何时坏 | URL 里的原始 +//、严格解码器对未填充长度报错 | 喂给只接受 +/= 的标准 Base64 解码器 | 重复编码(已编码值再编码一次) |
| 常见用途 | MIME 附件、data URI、JSON 内联二进制 | JWT、URL 安全 ID、短链 | 查询参数、路径段、表单体 |
数字本身就值得算一笔账。1MB 图像 Base64 编码后约 1.37MB;加上 MIME 76 字符换行还要再多 2% 左右。在 HTTP 响应里这种膨胀对服务端带宽和客户端解析都有影响。百分号编码在字符串层通常没那么夸张,但 CJK 文本会成倍膨胀。一个汉字在 UTF-8 里 3 字节,编码后是 9 个 ASCII 字节(%XX × 3)。两字”你好”在 URL 里就成了 %E4%BD%A0%E5%A5%BD 这 18 字节。
实战场景
**场景 1 — 邮件附件。**PDF 通过 Content-Transfer-Encoding: base64 装在 MIME 部里。邮件客户端 Base64 编码文件、按经典 MIME 限制每 76 字符换一次行、然后发出。接收端反过来还原。SMTP 的”文本前提”让 Base64 在这领域很久以前就成了事实默认,即便有 8BITMIME、BINARYMIME 等扩展,多数邮件服务器仍为安全起见输出 Base64。33% 的开销被接受为可靠投递的代价。
场景 2 — JSON Web Token。JWT(RFC 7519)是 header.payload.signature,每段是无填充的 Base64URL 编码 JSON 或字节。URL 安全字母表让令牌能出现在 Authorization 头、access_token 查询参数、日志行里,无须重新转义。无填充让令牌更短。一句警告:任何能解码 JWT 的人都能读到 claims,编码不是安全,HMAC 或 RSA 签名才是安全。别把秘密放进 payload。
场景 3 — 小图的 data URI。background-image: url("data:image/png;base64,iVBORw0K...") 直接把 PNG 内联到 CSS。这能给极小的资产省一次往返。但 Base64 的 33% 开销加上失去浏览器缓存和并行请求,意味着只有当资产小到额外 HTTP 往返成本更高时才划算。我的经验里,平衡点大约在 1–2KB;超过这个值,单独缓存的文件或 SVG 通常更快。
**场景 4 — 上传用户头像。**一个常见反模式是用 FileReader.readAsDataURL 读文件得到 data:image/png;base64,...,然后把这个字符串作为 JSON 字段 POST 出去。能跑,但几乎不是对的形状:载荷大了 33%,服务器要在写盘前再解一次 Base64,两端都被字符串副本占内存。我见过一个案例里 5MB 的图被吹到约 6.7MB 的 JSON,在移动网络上超时;改用 multipart/form-data 后同一个文件就是 5MB。只有当传输层真的要求 JSON 字符串时(比如某第三方 API 不接受 multipart)再考虑 Base64。
常见误解
“Base64 是加密。“不是。映射在 RFC 4648 公开发布、字母表固定,任何解码器都还原原始字节。编码保护的是传输而非内容。敏感数据要先加密,再把密文 Base64 用于传输。
“URL 编码只对非 ASCII 字符需要。“很多保留 ASCII 字符作为数据出现时也要编码。值里含 & 必须变 %26,否则服务端会把它当作新参数;# 必须变 %23,否则其后被当作 fragment。规则基于结构而不是字符集。
**“过 HTTP 时一切都用 Base64 才安全。“**HTTP 完全能承载二进制 body——Content-Type: application/octet-stream、分块传输、任意字节值。Base64 是为不能这么做的通道而设的变通方案,在已经能处理字节的通道上额外付 33% 纯属交税。文件上传用 multipart/form-data 或原始 body,把 Base64 留给通道真正需要文本的场合(JSON 字段、URL 参数、MIME 正文)。
**“Base64 和 Base64URL 可以互换。“**不行。把 URL 安全字符串喂进只认 +/ 的严格 Base64 解码器,会失败或产出垃圾。库通常两种都提供,从编码到解码端到端保持一致——Node 的 Buffer.from(s, 'base64') 两种字母表都接受,但不是所有语言的标准库都这么宽容。
决策清单
- **通道是 7 比特专用还是结构化文本?**邮件正文、JSON 里的字节、CSS
data:URI → Base64。 - **值要进 URL、文件名或 JWT 吗?**用 Base64URL,是否带填充取决于消费者。
- **值本身就是 URL 组件吗?**只对需要的部分做百分号编码。
- **数据大吗?**先确认通道是否真的要文本——原始二进制 body 能省下 33%。
- **解码器严格还是宽松?**端到端匹配标准/URL 安全字母表,别混用。
- **目的是保密吗?**先加密。仅靠编码永远不会让数据保密。
相关工具
Patrache Studio Base64 编解码器 在本地处理标准与 URL 安全两种变体,所以输入字节不会离开浏览器——查看令牌或小型密钥时很方便。如果 Base64 载荷碰巧是 JWT,搭配 JSON 格式化、校验、JSON Schema 在实战中的差异 干净地查看解码后的 claims。如果编码后的内容是从 UUID 派生的紧凑 ID,UUID v1、v4、v7 对比与数据库主键设计 解释为什么你 Base64URL 编码前的原始字节对排序与索引仍然重要。
参考资料
- IETF RFC 4648, “The Base16, Base32, and Base64 Data Encodings” — https://datatracker.ietf.org/doc/html/rfc4648
- IETF RFC 3986, “Uniform Resource Identifier (URI): Generic Syntax” — https://datatracker.ietf.org/doc/html/rfc3986
- MDN, “Base64” 术语解释 — https://developer.mozilla.org/en-US/docs/Glossary/Base64
- MDN, “encodeURIComponent()” — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent