URL 安全 Base64 与标准 Base64 的实用指南

2026-09-30 · 1142 词

标准 Base64 用 64 个字符:A–Z、a–z、0–9,再加上 + 和 /。问题就出在最后两个。在 URL 里,+ 在 application/x-www-form-urlencoded 中表示空格,/ 是路径分隔符;在文件名里,/ 是目录分隔符。所以一个完全合法的 Base64 字符串,一旦放进查询参数、Cookie 值或 JWT,就立刻变成了结构上有危险的东西。

修法是替换字母表 —— 而大量的解码失败正是从这里开始的。

替代规则,以及它的名字

URL 安全 Base64(也叫 base64url,或 RFC 4648 §5)把那两个麻烦字符换掉:

其余完全一样。62 个字符是共用的,所以大部分字符串在两套字母表下都合法、解码结果也相同。只有含 +、/、-、_ 的字符串才能告诉你,生产者用的是哪一种。

那四个字符里有两个是模糊的,这正是问题核心。如果字符串里有 -,几乎可以确定是 base64url;如果有 +,那是标准 Base64。但只含 [A-Za-z0-9] 和 = 的字符串在两套里都合法,光看文本无法判断 —— 好在两种解读给出的字节相同,所以这种模糊无害。

真正危险的是"标准 Base64 里出现了 - 或 _",而这根本不可能,因为那两个字符不在标准字母表里。于是规则比看上去更简单:见到 - 或 _ 就当 base64url 处理并转换;见到 + 或 / 就当标准处理;两者混在一起,说明这个字符串在两套字母表下都不合法,是上游把两个不同的字符串拼在了一起。

又是填充

RFC 4648 §5 允许 base64url 省略 = 填充,而绝大多数 JWT 与 URL token 的生产者正是这么做的。填充不携带任何信息 —— 它完全由长度决定 —— 所以在一个要塞进请求头的 token 里,它就是纯开销。

这就是为什么裸 JWT 段常常长度不是 4 的倍数:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

这一段恰好 36 个字符,凑巧能被 4 整除。payload 段通常就不是。严格的解码器会拒绝它,所以解码前先补回填充 —— 这个过程是确定且无损的:

const padded = s + '='.repeat((4 - (s.length % 4)) % 4);

两种形态之间互转

在 JavaScript、浏览器和 Node 里,转换就是文本层面各方向的两个字符替换:

const toUrlSafe = (b64) => b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const fromUrlSafe = (s) => {
  const b64 = s.replace(/-/g, '+').replace(/_/g, '/');
  return b64 + '='.repeat((4 - (b64.length % 4)) % 4);
};

Node 内置了两个方向的转换,Buffer 和字符串都支持:

Buffer.from(bytes).toString('base64url');        // 编码
Buffer.from(token, 'base64url').toString('utf8'); // 解码

Python 里叫 urlsafe_b64decode,对应的是 urlsafe_b64encode:

import base64
raw = base64.urlsafe_b64decode(token + '=' * (-len(token) % 4))

注意那个容易让人踩坑的不对称:某些版本的 urlsafe_b64decode 同时接受两套字母表的字符,所以面对含 + 的标准字符串它不会报错。它在"宽容无害"的方向上宽容,在"错了要紧"的方向上对填充严格。

解码器为什么要报告它用了哪套字母表

如果字符串里有 - 或 _,而你按标准 Base64 解码,会在越界的那一个字符处报错 —— 至少它告诉你出了问题。但有些解码器会悄悄替你转换字符,而最坏的失败模式正是发生在这里:一个本来就是标准 Base64、含合法 + 或 / 的字符串,因为解码器假定是 base64url,被变成错误的字节。

对图片来说,结果是一张解码不报错但渲染不出来的图;对 JWT 段来说,是一个验证失败却没有任何解释的签名。

这就是解码器要明确报出自己选了哪套字母表、并且提供勾选框让用户覆盖的原因。我们那个 URL 安全 Base64 转图片 页面,专为"你知道来源用的是 URL 安全字母表、希望从一开始就按它处理、不想猜"的场景而存在。当输入含 - 或 _ 而开关关闭时,工具会明确说明,而不是悄悄选一种解读。

实用的判断规则

三个问题,按顺序问:

  1. 这个字符串会经过 URL、文件名或 Cookie 吗? 会,就产出 base64url。它零成本,并且一次性消除一整类转义 bug。
  2. 字符串里有 + 或 / 吗? 当标准 Base64 处理 —— 这两个字符不可能出现在 base64url 里。
  3. 有 - 或 _ 吗? 当 base64url 处理。

还有一个值得养成的习惯:先把收到的字符串解码,再重新编码,不要提前重新编码。把标准字符串转成 URL 安全形态再转回来是无损的,但在你还不知道字母表的时候投机性地做归一,就是那个让 + 在查询串里变成空格、解码后又变成另一个字节的路径。先按收到的形态解码,再按下一个消费方需要的形态重新编码。

实践中会在哪儿遇到

JWT 是最熟悉的场景:三段 base64url 用点分隔,无填充、无换行。紧凑就是它的全部意义,而标准 Base64 会在 + 和 / 上毁掉每一个 JWT。

不那么显眼的场景包括不透明的会话 Cookie、签名 URL(AWS 预签名 URL 有些参数用标准 Base64、有些用 URL 安全)、请求头里的关联 ID,以及在从未设计用来承载二进制数据的 API 里用查询参数传图片。它们失败的形态都一样:一个"应该能解码"的字符串,要么在某个字符位置报错,要么干脆解出一堆毫无意义的字节。

要警惕的是后一种结果。如果你解码得到的不是预期的文件或载荷,先查字母表,再查别的 —— 图片 Base64 不显示怎么办 给出的排查顺序,第一步问的正是这个问题。如果你希望字母表由工具处理,URL 安全 Base64 转图片 从一开始就把 - 和 _ 当作预期字母表,而 Base64 转图片 会自行识别,并告诉你它用了哪一种解读。

相关阅读

试用 Base64 转图片工具 →