跳到正文
返回博客
·约 2 分钟读完

Base64 是什么?中文乱码、URL 安全、+ 号变空格问题怎么解决

Base64 是编码不是加密。本文讲 4 个字符对应 3 个字节、体积为什么涨三分之一、btoa 遇到中文报 InvalidCharacterError 的原因和 UTF-8 写法、Base64URL 与 JWT、查询参数里 + 变空格,以及 macOS base64 -D 与 -d、openssl 的实测差别。

简短回答: Base64 是把任意字节改写成 64 个可打印字符(A-Z、a-z、0-9、+、/)的编码,不是加密,没有密钥,谁都能还原。每 3 个字节写成 4 个字符,所以长度是 4 × ceil(n/3),约多三分之一。中文乱码的根源几乎都是"先把文字变成字节"这一步用错了编码:要用 UTF-8。放进网址或 JWT 时用 - 和 _ 代替 + 和 /,并常常去掉末尾的 =。

想直接试,可以用 Base64 编码解码工具。它在浏览器里运行,内容不上传。下面的结果都是我在这台 Mac 上用 Node v25.8.0、Python 3.9.6 和系统自带的 base64、openssl 实际跑出来的。

体积为什么多三分之一

我用 Node 分别编码 1、2、3、4、10、100、1000 个字节,长度是 4、4、4、8、16、136、1336,和公式 4 × ceil(n/3) 每一项都一致。字节数不是 3 的倍数时,末尾用 = 补齐到 4 的倍数:hello 编码是 aGVsbG8=。

中文用 btoa 报错:InvalidCharacterError

在 Node 里直接 btoa("你好"),得到 InvalidCharacterError: Invalid character,btoa("😀") 同样报错。原因是 btoa 只接受每个字符不超过 255 的字符串,也就是 Latin-1。btoa("é") 能成功,得到 6Q==,但这是把 é 当成单个字节 0xE9 编码的,而 UTF-8 里 é 是两个字节,所以 Buffer.from("é").toString("base64") 是 w6k=。两者不一样,别人按 UTF-8 解码时就会出错。

正确做法是先转 UTF-8 字节再编码。我验证过的两种写法:

// Node
Buffer.from("你好").toString("base64")            // 5L2g5aW9
// 浏览器和 Node 都可以
const u8 = new TextEncoder().encode("你好");
btoa(String.fromCharCode(...u8))                   // 5L2g5aW9

解码要反过来。如果只用 atob("5L2g5aW9"),得到的是 ä½ å¥½ 这种乱码,因为它给出的是每个字节一个字符;需要再用 TextDecoder 按 UTF-8 解开,我实测得到 你好。页面里的工具内部走的就是这条路径。

URL 安全的 Base64URL 和 JWT

标准字母表里的 +、/、= 在网址里都有特殊含义。Base64URL 把 + 换成 -,/ 换成 _,通常还省略 =。字节 fb ff bf 的标准编码是 +/+/,URL 安全写法是 -_-_。JWT 的三段就是 Base64URL:{"alg":"HS256","typ":"JWT"} 编码为 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9;{"name":"张三"} 标准编码是 eyJuYW1lIjoi5byg5LiJIn0=,Base64URL 去掉了末尾的 =。要看里面写了什么,可以用 JWT 解码器。

注意:对这个无填充的中文例子,Node 的 atob 能解开,但结果同样是 {"name":"å¼ ä¸"},需要再按 UTF-8 解码。含 - 或 _ 的内容,atob("-_8") 直接报 InvalidCharacterError,要先替换回 + 和 /。

+ 变成空格

查询字符串里,+ 会被当成空格。我实测 new URLSearchParams("d=a+bc").get("d") 得到 a bc,Python 的 parse_qs("d=a+b/c==") 也是 a b/c==。所以把 Base64 直接拼进网址,+ 就会变空格,数据悄悄被改。两种解法:用 encodeURIComponent(a+b/c== 变成 a%2Bb%2Fc%3D%3D),或者改用 Base64URL。需要进一步处理网址编码,见 URL 编码解码工具。

更麻烦的是,变成空格之后往往不会报错。网站工具的解码器会忽略空格,所以 a+bc 解出字节 6b e6 dc,a bc 解出 69 b7,没有任何提示(我用同一份工具函数跑的)。macOS 的 base64 -D 对 a bc 则输出为空。所以结果不对时,先检查中间有没有环节把 + 改成了空格。

命令行

macOS:

echo -n 你好 | base64          # 5L2g5aW9
echo 5L2g5aW9 | base64 -D       # 你好
echo 5L2g5aW9 | base64 -d       # 你好

在这台 macOS 上,-D 和 -d 都能解码,base64 --help 写的是 -D, -d, --decode。我只在这一台上验证,没有测过更旧的 macOS 或 Linux;一般认为旧版 macOS 只认 -D、Linux 只认 -d,写脚本时请在目标机器上试一下。

几个我实测到的差别:

  • echo hello | base64 是 aGVsbG8K,echo -n hello | base64 是 aGVsbG8=。echo 默认带换行,编码进去了,这是最常见的"对不上"。
  • macOS base64 默认不换行;base64 -b 76 才按 76 个字符折行。100 个 x 的结果是 76 加 60 字符两行。openssl base64 默认每行 64 个字符(64、64、8),加 -A 变成一行。
  • 缺少 = 时:printf YQ | base64 -D 输出 0 字节、返回码 0,openssl base64 -d -A 也没有输出,而 Python 的 b64decode("YQ") 报 binascii.Error: Incorrect padding。这种静默失败要小心。

data URI 和 76 字符换行

网页里的 data:text/plain;base64,5L2g5aW9 就是在 Base64 前面加了类型说明。邮件(MIME)规定每行最多 76 个字符,所以邮件附件的 Base64 才会折行;其他场景一般不需要折行,混进换行有时反而让解析失败。

什么时候用,什么时候别用

Base64 适合把二进制塞进只能放文本的地方,比如 JSON 字段、邮件、网址参数或网页里的小图片。代价是体积增加,也多了一步编码和解码,所以大文件能直接传二进制就别转。再强调一次:它不保密,看到一串 Base64 不代表数据被保护了,解开只需要一行命令。

我验证了什么

我用 28 个文本(空串、中文、emoji、含零宽连接符的家庭 emoji、BOM、\u0000、100 字符的长串等)和 7 组二进制字节(含 0 到 255 的全部字节),共 35 个用例,用网站工具的函数分别编码为标准和 URL 安全两种形式。结果与 Python base64、macOS base64 和 Node Buffer 逐个比对,没有发现不一致;用网站的解码函数反向还原,70 次也全部还原一致;折行 76 的输出和 base64 -b 76 也相同。没有验证的:其他浏览器、Linux 的 base64、Windows PowerShell 的行为。

常见问题

Base64 是加密吗,能用来保护密码吗?

不是,也不能。Base64 只是改变数据的写法,没有密钥,任何人都能解开。不要用它保护密码、令牌或个人信息,需要保密请使用真正的加密。

为什么 Base64 编码后变长了?

每 3 个字节被拆成 4 个 6 位的组,每组用一个字符表示,所以长度是 4 × ceil(n/3),大约多三分之一。我测的 1000 字节对应 1336 个字符。如果还按 76 字符折行,会再多出换行符。

中文编码后和别人的结果不一样怎么办?

先确认双方都用 UTF-8 把文字转成字节。"你好"按 UTF-8 编码是 5L2g5aW9。如果对方用的是 GBK 等编码,结果会不同,解码端也要用同样的编码。btoa 不能直接用于中文,需要像上面那样先转字节。

Base64 放进网址后 + 变空格,该怎么办?

用 encodeURIComponent 编码整个值,或者改用把 + / 换成 - _ 的 Base64URL。解码前如果发现里面出现了空格,往往是 + 被改了,先换回 + 再解。

作者

独立开发者,在 Obelisk Club 做本地优先的 Mac 与 iOS 效率应用。 关于作者 →