如果你见过 JWT 里那一长串
eyJhbGciOiJIUzI1NiIs...,或者在 JSON 里塞过图片的字符串,或者在 PEM 证书里复制过BEGIN PUBLIC KEY下面那堆乱码------你已经和 Base64 打过无数次交道了。但很多人只是"会用",并不理解它到底在做什么,以及为什么明明会让数据变大,我们却离不开它。
这篇文章把 Base64 的编码原理和工程逻辑彻底讲清楚。
一、Base64 到底在解决什么问题?
计算机世界里有两套"语言体系":
- 二进制世界 :图片、音频、加密后的密文、protobuf 消息......它们是任意的 0 和 1,可能包含
\0、<、>、&、控制字符等"危险分子"。 - 文本世界 :JSON、XML、HTML、URL、邮件协议、配置文件......它们只认可打印的 ASCII 字符,看到
\0就截断,看到<以为是标签开头,看到&以为是转义符。
Base64 是一座桥:它把二进制数据翻译成文本世界能安全识别的字符,让二进制数据能在邮件里发、在 JSON 里传、在 URL 里带、在配置文件里存。
代价是体积膨胀约 33% (3 字节二进制变成 4 字节 ASCII)。但工程上,兼容性往往比带宽更贵。
二、编码原理:6 位一组的"切片游戏"
为什么是 6 位?
ASCII 有 128 个字符,但安全、无歧义、可打印的只有 64 个:
- 大写
A-Z(26 个) - 小写
a-z(26 个) - 数字
0-9(10 个) - 符号
+和/(2 个)
2⁶ = 64,所以 6 位二进制正好能映射到一个安全字符。
核心过程:3 字节 → 4 字符
以经典的 "Man" 为例:
第一步:转成二进制
| 字符 | ASCII 码 | 二进制 |
|---|---|---|
| M | 77 | 01001101 |
| a | 97 | 01100001 |
| n | 110 | 01101110 |
拼接成 24 位:01001101 01100001 01101110
第二步:按 6 位重新切分
010011 | 010110 | 000101 | 101110
19 | 22 | 5 | 46
第三步:查 Base64 索引表
| 索引 | 字符 |
|---|---|
| 0-25 | A-Z |
| 26-51 | a-z |
| 52-61 | 0-9 |
| 62 | + |
| 63 | / |
r
19 → T, 22 → W, 5 → F, 46 → u
结果 :"Man" → "TWFu"
三、补零与补等号:不足 3 字节怎么办?
Base64 的基本单位是 24 位(3 字节 × 8 位 = 4 组 × 6 位)。如果原始数据不是 3 的倍数,就在末尾补 0,凑成 24 位,输出后再用 = 标记补了多少。
| 原始字节数 | 补齐后 | 输出字符数 | 结尾 |
|---|---|---|---|
| 3 | 无需补齐 | 4 | 无 |
| 2 | 补 8 个 0 → 24 位 | 3 | = |
| 1 | 补 16 个 0 → 24 位 | 2 | == |
示例:"M"(只有 1 字节)
ini
M = 01001101
补齐:01001101 00000000 00000000
切分:010011 | 010000 | 000000 | 000000
索引:19 | 16 | 0 | 0
字符:T | Q | = | =
结果:"M" → "TQ=="
那个 = 不是数据的一部分,只是填充标记,解码时会丢弃。
四、为什么体积大了 33%,我们还离不开它?
Base64 不是"最优解",而是**"工程上最可靠的兼容解"**。以下场景它几乎是标准答案:
1. Data URI:把资源直接嵌进代码
html
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...">
一张 1KB 的小图标,Base64 后变成 1.33KB,但省掉了一次 HTTP 请求。对首屏关键资源来说,零延迟比 300 字节体积重要得多。
2. JWT:在 HTTP Header 里传"自包含令牌"
text
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.SflKxwRJSMeKKF2QT4fwpMe...
JWT 的 Header 和 Payload 本质是 JSON,但必须放在 HTTP Header 这种纯文本环境 里。Base64URL(把 + 换成 -,/ 换成 _)是 JWT 的标准编码方式。
3. 证书与密钥:PEM 格式
text
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----
DER 二进制证书被 Base64 包裹后,可以安全地复制粘贴到聊天框、邮件、GitHub Issue、配置文件里,不会因为字符集转换而损坏。
4. JSON 里的二进制字段
前后端交互时,图片、文件、加密数据往往这样传:
json
{
"filename": "avatar.png",
"content": "iVBORw0KGgoAAAANSUhEUgAAADIA..."
}
虽然直接传二进制更高效,但整个 JSON.stringify + json.Unmarshal 生态都是文本的。Base64 是最省事的兼容方案。
5. 邮件附件:MIME 标准
早期邮件网关只支持 7-bit ASCII,MIME 标准强制用 Base64 编码附件。这个历史包袱至今仍在------你发邮件带 PDF,底层还是 Base64。
五、什么时候不该用 Base64?
| 场景 | 替代方案 | 原因 |
|---|---|---|
| 微服务内部 RPC | Protobuf / gRPC | 二进制直接传,零膨胀,解析快 10 倍 |
| 大文件传输(>10MB) | 分片二进制流 | 33% 膨胀是实打实的带宽和存储成本 |
| 高频低延迟交易 | 固定二进制协议 | 编码解码耗时不可接受 |
| 对象存储(S3/OSS) | 直接上传二进制 | 存储按量计费,Base64 不划算 |
原则:如果通信双方都能处理裸二进制,就不要用 Base64 给自己加戏。
六、Go 实战:标准库零值可用
Go 的 encoding/base64 包设计非常简洁,零值可用:
go
package main
import (
"encoding/base64"
"fmt"
)
func main() {
data := []byte("Man")
// 标准编码
encoded := base64.StdEncoding.EncodeToString(data)
fmt.Println(encoded) // TWFu
// 解码
decoded, _ := base64.StdEncoding.DecodeString(encoded)
fmt.Println(string(decoded)) // Man
// URL 安全编码(JWT 专用)
urlSafe := base64.URLEncoding.EncodeToString([]byte("hello+world/test"))
fmt.Println(urlSafe) // aGVsbG8rd29ybGQvdGVzdA==
}
两个编码器的区别:
| 标准 | URL 安全 | |
|---|---|---|
+ |
+ |
- |
/ |
/ |
_ |
| padding | = |
可选去掉 |
JWT 用的是 base64.URLEncoding,因为标准 Base64 的 + 和 / 在 URL 里会被转义成 %2B 和 %2F,体积反而更大。
七、总结
Base64 的本质不是压缩,也不是加密,而是编码------它用一种文本世界能读懂的字母表,复述了一遍二进制数据的内容。
- 原理 :3 字节拆成 4 个 6 位组,每组查表变成一个 ASCII 字符,不足补
=。 - 代价:体积膨胀 33%。
- 收益 :二进制数据能在 JSON、URL、邮件、HTML、配置文件里无损、无歧义地通行。
- 原则:对外兼容用 Base64,对内性能用二进制。
你在工作中遇到过哪些 Base64 的诡异 bug?比如忘了用 URL 安全版本导致 JWT 解析失败,或者把 Base64 当加密算法用?欢迎在评论区分享。