把明文丢进 Base64 在线编码解码工具,得到一串看不懂的字符,很多人就以为加密了。Base64 不加密 —— 它只是把二进制数据换成 64 个可打印字符,任何人贴回去解码都能还原原文。
搞懂它的三个特性(3 字节变 4 字符、= 补位、字符集决定乱码),基本能解决 90% 的线上问题。
一、Base64 不是加密,只是编码
| 对比项 | 编码(Base64) | 加密(AES 等) |
|---|---|---|
| 目的 | 让二进制能走只支持文本的通道 | 让没有密钥的人读不出来 |
| 需要密钥吗 | 不需要 | 需要 |
| 逆向难度 | 任何人都可以直接解 | 没密钥解不出来 |
| 典型用途 | 邮件附件、Data URI、HTTP Basic 认证、JWT | 存储密码、传输敏感数据 |
HTTP Basic 认证里的 Authorization: Basic dXNlcjpwYXNz 就是 user:pass 的 Base64,等同于明文,必须配合 HTTPS。同理 JWT 的 payload 也只是 Base64,别往里放密码。
二、3 字节变 4 字符:体积为什么涨三分之一
原理只有一句:取 3 个字节共 24 bit,切成 4 组每组 6 bit,6 bit 取值 0~63,再查表映射成字符。字符表是 A-Z(26)+ a-z(26)+ 0-9(10)+ + /(2)= 64 个。
所以输入输出的比例固定是 3 : 4,编码后长度 = 4 × ceil(字节数 / 3),必然是 4 的倍数,体积约 +33%。
三、宽字符换算表:为什么中文编出来特别长
Base64 按字节算,不按字符算。UTF-8 下一个汉字占 3 字节,一个 emoji 占 4 字节,这才是长度暴涨的原因:
| 输入 | UTF-8 字节数 | Base64 字符数 | 末尾 = |
|---|---|---|---|
a |
1 | 4 | 2 |
ab |
2 | 4 | 1 |
abc |
3 | 4 | 0 |
abcd |
4 | 8 | 2 |
中 |
3 | 4 | 0 |
中文 |
6 | 8 | 0 |
997 工具箱 |
13 | 20 | 2 |
𠮷 |
4 | 8 | 2 |
四、末尾的 = 是什么
最后一组凑不满 3 字节时,用 = 占位:缺 1 个字节补 1 个 =,缺 2 个字节补 2 个 =,正好 3 字节时没有 =。
所以反过来说:一段 Base64 去掉 = 之后,接收方只要看剩余长度 mod 4 是 2 还是 3,就知道该补几个等号(余 1 是不可能出现的情况,出现就说明传丢了字符)。
= 只是长度标记,不携带信息,所以很多场景(URL-safe、JWT)会把它去掉,解码时再按长度补回来。判断一段 Base64 是否完整,第一件事就是看长度是不是 4 的倍数。
五、标准表 +/ 与 URL-safe -_
标准字符表的第 62、63 位是 + 和 /,这两个字符在 URL 和文件名里有特殊含义 —— 尤其是 +,在 query string 里会被解成空格。RFC 4648 因此定义了 URL-safe 变体:
| 位置 | 标准 Base64 | URL-safe Base64 |
|---|---|---|
| 62 | + |
- |
| 63 | / |
_ |
| 补位 | 保留 = |
通常去掉 |
| 典型场景 | 邮件、Data URI、Basic 认证 | URL / 文件名 / JWT / Cookie |
// 两者互转,就两行
const toUrlSafe = (s) => s.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
const toStandard = (s) => s.replace(/-/g, '+').replace(/_/g, '/')
在线工具一般把这个做成开关:勾上之后编码输出会自动换成 -_ 并去掉尾部 =,解码时再自动补回等号。
还有两个常被忽略的细节:一是别把两种变体混用,一段 URL-safe 的串丢给只认标准表的解码器,- 和 _ 会直接判为非法字符;二是 URL-safe 只是换了字符表,没有改变体积比例,别指望去掉 = 能省多少流量。
六、中文乱码的真正根因
坑一:btoa 遇到中文直接抛错。
btoa('中文')
// ❌ Uncaught InvalidCharacterError: String contains an invalid character
原因:btoa 把每个字符按 Latin-1(单字节,0~255) 处理,中文的码位远超 255,直接报错。正确做法是先转成 UTF-8 字节再编码:
const enc = (str) => btoa(String.fromCharCode(...new TextEncoder().encode(str)))
const dec = (b64) => new TextDecoder().decode(Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)))
enc('997 工具箱') // 'OTk3IOWbvuW6lOi0pQ=='
dec('OTk3IOWbvuW6lOi0pQ==') // '997 工具箱'
注意 String.fromCharCode(...bytes) 在几百 KB 以上会爆调用栈,大文件要按块拼接(常见做法是每 0x8000 字节一段)。
坑二:编码端和解码端字符集不一致。 老系统按 GBK 生成的字节流被当成 UTF-8 解,出来就是经典的 锟斤拷。统一 UTF-8 是唯一解;必须收 GBK 时用 iconv -f gbk -t utf-8 转。
在线工具如果只写 btoa/atob,中文一定是报错或乱码,选工具时认准写「UTF-8 安全」的:
七、Data URI 图片:方便但有代价
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...
好处是零请求、不怕图床挂。代价有三条:
- 体积 +33%,同样一张图要多传三分之一流量
- 无法单独缓存,每次都必须随 HTML / CSS 一起下载,改一个字整段失效
- 阻塞首屏,内联在 CSS 里的 base64 会拖慢样式解析
经验值:小于 2~4KB 的小图标内联划算;超过 10KB 就老实用 URL。要验证一段 base64 是不是你想要的图,解码后直接预览最快。
生成和验证在命令行里也就两行:
# 图片转 base64(Linux 记得加 -w 0,否则每 76 字符插一个换行)
printf 'data:image/png;base64,%s' "$(base64 -w 0 logo.png)" > logo.txt
# 反过来:剥掉 data URI 前缀再解码成文件
sed 's/^data:image\/png;base64,//' logo.txt | base64 -d > logo.png
顺带一个实用技巧:不用解码也能看出文件类型,看开头几个字符即可,因为它们由文件头字节决定:
| Base64 开头 | 文件类型 |
|---|---|
iVBORw0KGgo |
PNG |
/9j/ |
JPEG |
R0lGOD |
GIF |
JVBERi0 |
|
UEsDB |
ZIP / Office 文档 |
八、JWT 三段里为什么是 Base64URL
JWT 形如 header.payload.signature,前两段是 Base64URL 编码的 JSON,第三段是对前两段做 HMAC 后得到的二进制再 Base64URL。用 URL-safe 而不是标准 Base64,是因为 JWT 要放进 URL、Cookie 和 Authorization 头,+ / = 都会引发转义问题。
反过来也说明:JWT 的 payload 一贴进解码工具就能看到明文,它是签名不是加密 —— 签名保证没被篡改,不保证不被看见。
九、命令行怎么用
# 编码(务必用 -n 或 printf,否则换行会被编进去)
echo -n '997 工具箱' | base64
printf '%s' '中文' | base64
# Linux(GNU coreutils):-w 0 表示不插换行
base64 -w 0 file.png > file.b64
# macOS(BSD):用 -i / -o
base64 -i file.png -o file.b64
# 解码
echo 'OTk3IOWbvuW6lOi0pQ==' | base64 -d
base64 -d file.b64 > file.png
base64 -D file.b64 > out.png # 老版本 macOS 用 -D 不是 -d
# openssl(两边通用)
openssl base64 -in file.png -out file.b64
openssl base64 -d -in file.b64 -out file.png
最常见的低级错误是 echo 'x' | base64 没加 -n:换行符被一起编码,解码出来末尾多一个空行,和预期结果怎么比对都对不上。
第二个坑是 GNU 和 BSD 参数不同:Linux 上是 base64 -d -w 0,macOS 自带的 BSD 版没有 -w,解码要用 -D(新版本也支持 -d)。脚本里要跨机器跑,直接用 openssl base64 最稳。
十、常见报错对照表
| 报错 / 现象 | 原因 | 解决 |
|---|---|---|
InvalidCharacterError(编码时) |
btoa 遇到中文 / emoji 等非 Latin-1 字符 |
先 TextEncoder 转 UTF-8 字节 |
InvalidCharacterError(解码时) |
含空格、换行,或长度不是 4 的倍数 | 先 trim、去掉换行,URL-safe 补回 = |
解码出来是 锟斤拷 |
GBK 字节流被当 UTF-8 解 | iconv -f gbk -t utf-8 |
| 解码结果末尾多一个空行 | 编码时用了不带 -n 的 echo |
改用 echo -n / printf '%s' |
| 两个系统编出来的串对不上 | 一边用了 URL-safe,一边没有 | 统一 -_ 与 +/ |
| URL 传参后解码失败 | + 在 query 里被当成空格 |
换 URL-safe,或编码后再 encodeURIComponent |
| 解出来的 JSON 报 Unexpected token | 原文不是 JSON,或带 BOM | 参考 JSON 在线格式化的排错清单 |
十一、速查表
编码后长度 = 4 × ceil(字节数 / 3) # 一定是 4 的倍数
体积开销 = 33%
补位规则 = 缺 1 字节补 1 个 =,缺 2 字节补 2 个 =
标准表 = A-Z a-z 0-9 + / # 第 62、63 位是 + /
URL-safe = A-Z a-z 0-9 - _ # 通常去掉 =
UTF-8 = 汉字 3 字节,emoji 4 字节
记不住细节没关系,记住「它是编码不是加密」和「中文要先转 UTF-8」这两条,就不会出安全事故、也不会出乱码。
相关工具:Base64 编码解码|JSON 格式化|JSON 转 CSV/Excel
全部纯前端实现,输入的内容不上传服务器。