上传文件
  1. base64hub
  2. 学习 Base64
  3. Base64 常见错误
故障排查

Base64 解码报错与乱码的解决方法

大多数 Base64 解码失败都源于五个原因:字母表用错、缺少填充、混入杂字符、字符集不对,或者数据被编码了两次。在下面找到你遇到的报错信息,直接跳到解决方法。

报错信息速查

报错信息出现在哪里最可能的原因
binascii.Error: Incorrect paddingPython base64.b64decode末尾缺少 =,通常因为是 Base64URL 或字符串被截断
binascii.Error: Invalid base64-encoded string: number of data characters (5) cannot be 1 more than a multiple of 4Python输入被截断,或 -/_ 被悄悄丢弃
binascii.Error: Only base64 data is allowedPython,使用 validate=True 时输入中有换行符、空格、- 或 _
InvalidCharacterError: Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded.Chrome、EdgeBase64URL 字符、杂字符、长度错误
InvalidCharacterError: String contains an invalid characterFirefox同上
InvalidCharacterError: Invalid characterNode.js atob同上
IllegalArgumentException(“Illegal base64 character …”)Java java.util.Base64解码器与字母表不匹配,或者有换行符
FormatException.NET Convert.FromBase64StringBase64URL 字符或缺少填充
base64: invalid inputGNU coreutils base64 -d字母表之外的字符

解决解码器报错

Python 报 Incorrect padding

Base64 输出的长度总是 4 的倍数,末尾用 = 填充。Python 的解码器要求必须有填充:

>>> import base64
>>> base64.b64decode("SGVsbG8")
Traceback (most recent call last):
  ...
binascii.Error: Incorrect padding
>>> base64.b64decode("SGVsbG8=")
b'Hello'

缺少填充通常意味着字符串是 Base64URL(JWT、URL 令牌和许多 API 会去掉 =),或者被什么东西截掉了。把填充补回去:

s = "SGVsbG8"
s += "=" * (-len(s) % 4)

如果长度比 4 的倍数多 1,补填充也无济于事。这时 Python 会报 number of data characters (5) cannot be 1 more than a multiple of 4。多出的单个字符只承载 6 位,所以有一部分数据丢失了。即使字符串没有被截断,也可能遇到这条信息:不加 validate=True 时,Python 会丢弃字母表之外的字符,包括 - 和 _。base64.b64decode("SGk_Pg==") 丢掉 _ 之后,只剩 5 个数据字符。对于 Base64URL 输入,请使用 base64.urlsafe_b64decode。

Invalid character 与 atob 失败

各浏览器实现 atob 的算法相同,但报错措辞不同。Chrome 说 “The string to be decoded is not correctly encoded”,Firefox 说 “String contains an invalid character”,Node.js 说 “Invalid character”。遇到以下输入时,三者都会抛出 InvalidCharacterError:

  • Base64URL 字符:atob("SGk_Pg") 因为 _ 而失败。请先把 - 换成 +、_ 换成 /。
  • 去掉空白后长度为 4n+1,例如 atob("A")。
  • 不该出现的字符:从 JSON 中一起复制过来的引号、data: 前缀、URL 编码留下的 %2F。

atob 也会宽容一些情况。它会跳过空格和换行符,并且在长度合适时接受缺失的填充,所以 atob("SGVsbG8") 返回 "Hello"。与之对应的另一个错误是 btoa 报 “characters outside of the Latin1 range”,那是 Unicode 文本的问题,见 Uint8Array.toBase64。

一个兼容两种字母表、空白和缺失填充的宽松解码器:

function decodeLoose(s) {
  s = s.replace(/\s+/g, "").replace(/-/g, "+").replace(/_/g, "/");
  s += "=".repeat((4 - (s.length % 4)) % 4);
  return Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
}

new TextDecoder().decode(decodeLoose("SGk_Pg")); // "Hi?>"

等价的 Python 版本:

import base64

def b64decode_any(s: str) -> bytes:
    s = "".join(s.split())
    s = s.replace("-", "+").replace("_", "/")
    s += "=" * (-len(s) % 4)
    return base64.b64decode(s, validate=True)

修复 Base64 工具会进行同样的清理,并告诉你它改了什么。

Java 的 Illegal base64 character 与 .NET 的 FormatException

Java 的 java.util.Base64 提供三个解码器,每个都对自己的字母表很严格。错误信息会以十六进制给出出问题的字符:5f 是 _,2d 是 -,2f 是 /,20 是空格,a 是换行符。

输入包含应使用
+ 和 /Base64.getDecoder()
- 和 _Base64.getUrlDecoder()
换行符(MIME、PEM 正文)Base64.getMimeDecoder()

基本解码器拒绝换行符和空格,但接受缺失的填充,所以 SGVsbG8 可以顺利解码。

.NET 的 Convert.FromBase64String 遇到 Base64URL 字符和缺失的填充时,会抛出 FormatException。它会跳过空格和换行符。调用之前,请按上面的方法修正字母表和填充。完整示例见 Java 中的 Base64 和 C# 中的 Base64。

解码后出现乱码

解码成功了,但文本看起来不对。Base64 存储的是字节,而这些字节是用某种字符编码从文本生成的。用另一种编码读回来,就会得到乱码。

你看到的原文发生了什么
CaféCaféUTF-8 字节被当作 Latin-1 或 Windows-1252 读取
Caf�CaféWindows-1252 字节被当作 UTF-8 读取
H e l l o 或 H\0e\0l\0l\0o\0HelloUTF-16LE 字节被当作 UTF-8 读取

第一行是经典的 atob 陷阱。atob("Q2Fmw6k=") 返回 "Café",因为 atob 把每个字节映射成一个字符。正确做法是按 UTF-8 解码字节:

const bytes = Uint8Array.from(atob("Q2Fmw6k="), (c) => c.charCodeAt(0));
new TextDecoder().decode(bytes); // "Café"

UTF-16LE 在 Windows 工具中很常见:PowerShell 的 -EncodedCommand 和许多 .NET API 都会生成它。Base64 转文本 可以切换字符集,直到输出读起来正确为止;Base64 转 UTF-16 专门处理宽字符的情况。

输出看起来是二进制数据

如果解码结果满是 �、控制字符,或者夹杂着 PNG、PK、%PDF 之类的片段,说明载荷是一个文件。看看 Base64 开头的几个字符:iVBORw0KGgo 是 PNG,JVBERi0 是 PDF,H4sI 是 gzip。完整列表见 常见的 Base64 前缀。用 Base64 转文件 打开数据,它会识别格式并提供下载。对于 H4sI,请使用 解码 gzip Base64。

解码出的字节与预期不符

没有报错,但字节不对。常见原因有这些:

  • 编码时多了一个结尾换行符:echo "hello" | base64 得到 aGVsbG8K(含 \n),而 echo -n "hello" | base64 得到 aGVsbG8=。请使用 printf '%s' 或 echo -n。
  • + 变成了空格:在 URL 查询字符串和表单正文中,+ 表示空格。URLSearchParams("t=PGI+Ym9sZDwvYj4=").get("t") 返回 PGI Ym9sZDwvYj4=。有些解码器会因此报错,有些则会丢掉空格,得到乱码。把 Base64 放进 URL 之前,请用 encodeURIComponent 进行百分号编码,或者改用 Base64URL。
  • 残留的百分号编码:SGk%2FPg%3D%3D 必须先经过 URL 解码变成 SGk/Pg==,再进行 Base64 解码。
  • 从 JSON 中复制来的转义序列:从原始 JSON 中复制字符串,可能会带进一个字面的 \n(两个字符)。Python 的宽松解码器会丢掉反斜杠、保留 n,在不报错的情况下返回错误的字节:b64decode("SGVs\\nbG8=") 得到 b'Hel\x9d\xb1\xbc'。请解析 JSON,而不是从中复制。JSON Base64 解码器 会对每个字段都这样处理。
  • 对空白的严格与宽松处理:不加 validate=True 的 Python、atob、.NET 和 Java 的 MIME 解码器会跳过换行符。加了 validate=True 的 Python 和 Java 的基本解码器则会拒绝它们。MIME 和 PEM 数据会在 76 或 64 个字符处换行,所以请选一个能接受换行的解码器。

被编码了两次的数据

解码 U0dWc2JHOGdkMjl5YkdRaA== 得到 SGVsbG8gd29ybGQh,它又是有效的 Base64,解码后是 Hello world!。如果一次解码的结果只由 Base64 字符组成,且长度是 4 的倍数,就要怀疑是重复编码。

这种情况在 Kubernetes Secret 中很常见。data: 下的值是 Base64,而 kubectl create secret --from-literal 会替你编码。如果你自己先编码了一次,Pod 收到的就是 Base64 文本,而不是密码。纯文本值请用 stringData:,或者用 Kubernetes Secret 解码器 查看实际存储的内容。拿不准时,Base64 校验工具 会显示字符串是否有效以及包含多少字节。

常见问题

如何解决 binascii.Error: Incorrect padding?

在末尾追加 =,直到长度是 4 的倍数:s += "=" * (-len(s) % 4)。如果字符串是 Base64URL,还要把 - 换成 +、_ 换成 /,或者直接用 base64.urlsafe_b64decode。

为什么 atob 解码 JWT 会失败?

JWT 使用 Base64URL,其中有 - 和 _。atob 只认识 + 和 /。请先转换这些字符,或者把令牌粘贴到 JWT 解码器 中。

为什么解码出的文本把 é 显示成 é 这样的乱码?

这些字节是 UTF-8,却被当作 Latin-1 或 Windows-1252 读取了。请按 UTF-8 解码字节,例如在 JavaScript 中用 TextDecoder,在 Python 中用 .decode("utf-8")。

长度为 4n+1 的 Base64 字符串还能修复吗?

不能。多出的一个字符只承载 6 位,还不到一个字节,所以至少缺了一个字符。请找到原始来源,或者检查字符串是在哪里被截断的。

怎么判断数据是不是被 Base64 编码了两次?

先解码一次。如果结果仍然是只由 A-Z a-z 0-9 + / = 组成、长度为 4 的倍数的字符串,就再解码一次。

继续阅读