一张表看懂区别
两种变体都定义在 RFC 4648 中:第 4 节是标准 Base64,第 5 节是“URL 和文件名安全”的字母表。它们共用 62 个字符和同一套算法,只有索引 62 和 63 不同。
| 标准 Base64(RFC 4648 §4) | Base64URL(RFC 4648 §5) | |
|---|---|---|
| 索引 0–61 | A–Z a–z 0–9 | 相同 |
| 索引 62 | + | - |
| 索引 63 | / | _ |
| 填充 | 默认必须有 = | 通常省略 |
| 可直接放进 URL 路径或查询参数 | 否 | 是 |
| 可用作文件名 | 否(/ 是路径分隔符) | 是 |
同一个输入分别用两种方式编码:
| 输入 | 标准 | Base64URL | Base64URL,不填充 |
|---|---|---|---|
<<???>> | PDw/Pz8+Pg== | PDw_Pz8-Pg== | PDw_Pz8-Pg |
字节 fb ff | +/8= | -_8= | -_8 |
Man | TWFu | TWFu | TWFu |
最后一行提醒我们:很多字符串在两种变体下完全一样。只有包含 +、/、- 或 _,或者缺少填充时,才能区分出来。分辨不出时,用一个两种字母表都接受的解码器最稳妥。
拿到一个字符串时,可以这样快速判断:
- 含有
-或_:Base64URL。 - 含有
+或/:标准 Base64。 - 长度不是 4 的倍数且没有
=:没有填充,几乎总是 Base64URL。 - 同时含有
+和-,或同时含有/和_:有什么东西把两种字母表混在了一起,或者这根本不是 Base64。Base64 校验器会标出哪些字符不对。 - 由点号分成三段,并以
eyJ开头:这是 JWT,按定义就是 Base64URL。
为什么会有 Base64URL
在 URL 中,/ 分隔路径段,+ 在表单编码的查询字符串里表示空格,= 分隔键和值。标准 Base64 这三个字符全都会用到。把它放进 URL 就得做百分号编码,ab+/8= 会变成 ab%2B%2F8%3D,更长、更难看,还多了一个容易出错的步骤。
RFC 4648 §5 选用了 - 和 _ 来解决这个问题:它们在 URL 中属于非保留字符,在所有常见文件系统中都可以出现在文件名里。RFC 还指出,长度已知时可以省略填充,因为 = 本身也需要转义成 %3D。详见 填充指南。
两种变体分别用在哪里
标准 Base64:
- MIME 电子邮件附件和编码后的邮件头(电子邮件中的 Base64)
- PEM 证书和密钥
- HTML 和 CSS 中的
data:URI - HTTP Basic 认证请求头
- Kubernetes Secret 以及大多数传输文件的 JSON API
Base64URL,通常不带填充:
- JSON Web Token:头部、载荷和签名都是不带填充的 Base64URL(JWT 中的 Base64)
- JWK 密钥,以及整个 JWS 和 JWE(RFC 7515–7518)
- WebAuthn 的凭据 ID 和挑战值
- OAuth PKCE 的
code_challenge值(RFC 7636) - 随机 URL 令牌,例如 Python 的
secrets.token_urlsafe(),32 个随机字节会得到 43 个字符的字符串
两者之间的转换
转换只是字符替换,填充单独处理,字节本身不会变。
标准转 URL 安全:把 + 换成 -,/ 换成 _,再去掉末尾的 =。
URL 安全转标准:把 - 换成 +,_ 换成 /,然后补 = 直到长度是 4 的倍数。
const toUrl = (b64) =>
b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
const fromUrl = (u) => {
const b64 = u.replace(/-/g, "+").replace(/_/g, "/");
return b64 + "=".repeat((4 - (b64.length % 4)) % 4);
};
toUrl("PDw/Pz8+Pg=="); // "PDw_Pz8-Pg"
fromUrl("PDw_Pz8-Pg"); // "PDw/Pz8+Pg=="
大多数语言也自带直接的编码器,很少需要自己倒腾字符串:
| 语言 | Base64URL 编码器 | 填充 |
|---|---|---|
| Python | base64.urlsafe_b64encode(b) | 保留;用 .rstrip(b"=") 去掉 |
| Node.js | buf.toString("base64url") | 省略 |
| Go | base64.RawURLEncoding / base64.URLEncoding | 省略 / 保留 |
| Java | Base64.getUrlEncoder().withoutPadding() | 省略 |
| PHP | rtrim(strtr(base64_encode($s), '+/', '-_'), '=') | 省略 |
解码时,Node 的 Buffer.from(s, "base64") 两种字母表都接受,也不在乎缺少填充。Python 的 base64.urlsafe_b64decode 需要先补回填充。浏览器的 atob() 遇到 - 和 _ 会抛出 InvalidCharacterError,所以要先转换,或者在支持的环境中使用带 alphabet: "base64url" 的 Uint8Array.fromBase64。其他语言的代码见代码示例页,例如 Python 和 Java。
如果只是要转换一个字符串,Base64 转 Base64URL 和 Base64URL 转 Base64 可以直接在浏览器里完成。
常见坑
+ 变成了空格
这是最常见的 bug。标准 Base64 不经转义直接放进查询字符串,会被表单解码搞乱,因为它把 + 当作空格:
from urllib.parse import parse_qs
parse_qs("t=ab+/8=")
# {'t': ['ab /8=']}
new URLSearchParams("t=ab+/8=").get("t");
// "ab /8="
下游的解码器要么在空格处报错,要么跳过空格返回错误的字节。解决办法是改用 Base64URL,或者在传入时调用 encodeURIComponent()。作为接收方的临时补救,把空格替换回 + 通常可行,因为 Base64 中从来不会出现空格。
悄悄混用字母表
除非传入 validate=True,Python 的 base64.b64decode 会丢弃标准字母表以外的字符。给它一个 Base64URL 字符串,它会扔掉 - 和 _,然后解码剩下的部分:
import base64
base64.b64decode("PDw_Pz8-Pg==")
# b'<<\x0f\xcf\xc3\xe0' (wrong; the input was "<<???>>")
base64.b64decode("PDw_Pz8-Pg==", validate=True)
# binascii.Error: Only base64 data is allowed
base64.urlsafe_b64decode("PDw_Pz8-Pg==")
# b'<<???>>'
第一次调用不会报错,却返回了错误的字节。URL 安全的输入请用 urlsafe_b64decode,或者加上 validate=True,让不匹配的问题暴露出来。
双重编码
这个名字指的是两种不同的情况,实际中都会遇到。
百分号编码了两次。标准 Base64 字符串被你的代码转义一次,又被 HTTP 客户端转义一次,于是 + 先变成 %2B,再变成 %252B。服务器只解码一次,收到的是字面上的 %2B。Base64URL 没有这个问题,因为根本没有需要转义的字符。
Base64 套 Base64。一个值在两层被编码,例如应用编码一次,框架或 Kubernetes 清单工具又编码一次。Man 先变成 TWFu,再变成 VFdGdQ==。识别方法是:解码后得到的仍然全是 Base64 字符。再解码一次,看结果是否说得通。
对填充的预期不一致
有的接收方要求必须有填充,有的则拒绝填充。Go 的 RawURLEncoding 遇到含 = 的输入会失败,而 Python 的解码器遇到缺少填充的输入会失败。设计 API 时,请在文档中写明变体和填充规则,并让你自己的解码器两种都接受。
粘贴 URL 安全的字符串,有没有填充都可以,看看里面是什么。
常见问题
Base64URL 等于对 Base64 字符串做 URL 编码吗?
不是。百分号编码会把 + 变成 %2B、/ 变成 %2F,字符串会变长。Base64URL 换用了另一套字母表,根本不需要转义。
标准 Base64 解码器能解 Base64URL 吗?
不一定。有些解码器(比如 Node 的 Buffer)两种字母表都接受;有些会拒绝 - 和 _;而 Python 的 b64decode 默认会悄悄丢弃它们,得到错误的字节。
Base64URL 有填充吗?
RFC 4648 允许有填充,Python 的 urlsafe_b64encode 也会保留。但包括 JWT 在内,大多数使用 Base64URL 的格式都会去掉 =,因为长度本身已经能确定填充。
URL 里的令牌该用哪一种?
用不带填充的 Base64URL。它放在路径、查询字符串、Cookie 或文件名中都不需要转义。