简短回答: 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。解码前如果发现里面出现了空格,往往是 + 被改了,先换回 + 再解。