「存之前先 Base64 一下不就行了。」审过足够多的配置文件、内部后台、浏览器 bundle、Webhook 处理器和认证代码,你迟早会看到这句话。因为输出看起来乱糟糟的,听着就像有道理:
c2VjcmV0IHBhc3N3b3Jk
但 Base64 不是加密。它不是密码保护。它也不是在 URL 里安全地藏 API 密钥、JWT claims、用户 ID、Basic 认证凭据或私密数据的办法。同一个值一次调用就能解出来:
atob("c2VjcmV0IHBhc3N3b3Jk");
// "secret password"
现场规则简单粗暴:如果原值不用密钥就能回来,那 Base64 就没保护它,只是改了个表达方式而已。
快速安全自检
在代码里看到 Base64,先问一句它在干什么活儿:
| Base64 被用来做什么 | 合理用途? | 需要检查什么 |
|---|---|---|
| 把字节塞进 JSON | 是 | 大小上限保持合理 |
嵌入一个小的 data: URI |
是 | 别无脑内联大文件 |
| 把密文、nonce、盐或签名字节以文本形式存储 | 是 | 加密步骤必须在 Base64 之前 |
| 携带一个不透明的分页游标 | 通常可以 | 当作接口细节,不是访问控制 |
| 构造 Basic 认证头 | 只能在 HTTPS 下 | 保护请求的是 TLS,不是 Base64 |
| 在前端代码里藏 API 密钥 | 否 | 每个浏览器都收到值和解码器 |
| 存密码 | 否 | 用慢速密码哈希 |
| 藏 JWT claims | 否 | 签名后的 JWT 载荷若不做 JWE 加密就是可读的 |
| 在 URL 里藏用户 ID 或角色 | 否 | 服务端授权仍然必须执行 |
如果你脑子里那句是「这样就没人能读了」,你说的不是 Base64,说的是加密、哈希、签名、访问控制或密钥管理。
Base64 实际做的事
Base64 是一种二进制到文本的编码方案。RFC 4648 定义了常见的 Base64 字母表:
A-Z a-z 0-9 + / =
字符 = 是填充。Base64 把输入字节按 24 位一组分组:3 个输入字节变成 4 个可打印字符。所以 Base64 输出比压缩前的原始字节大约多 33%。
btoa("hello");
// "aGVsbG8="
atob("aGVsbG8=");
// "hello"
同样的往返在终端里也一样:
printf '%s' 'secret password' | base64
# c2VjcmV0IHBhc3N3b3Jk
printf '%s' 'c2VjcmV0IHBhc3N3b3Jk' | base64 -d
# secret password
没有密码提示、私钥、解密步骤、盐、nonce、IV、tag,也没有工作因子。谁拿到编码字符串谁就能还原。
Base64 与 Base64url
标准 Base64 不总是 URL 安全,因为 +、/ 和 = 放在 URL、文件名、Cookie、Token 段里很别扭。Base64url 换了字母表:
| 特性 | 标准 Base64 | Base64url |
|---|---|---|
| 第 62 和 63 个字符 | + 和 / |
- 和 _ |
| 填充 | 通常是 = |
常常省略 |
| 常见场景 | MIME、PEM、Basic 认证、二进制字段 | JWT、URL Token、文件名 |
| 安全差异 | 无 | 无 |
调试 JWT 时这一点很关键。JWT 用的是 Base64url,不是带填充的标准 Base64:
header.payload.signature
如果你把一段 JWT 粘进标准 Base64 解码器失败了,那段字符串未必是秘密的、也未必被加密了,可能只是需要按 Base64url 处理:
function base64urlToBase64(part) {
let out = part.replace(/-/g, "+").replace(/_/g, "/");
while (out.length % 4 !== 0) out += "=";
return out;
}
这个转换只换字母表,并不增加机密性。
UTF-8:浏览器的坑
浏览器的 btoa() 和 atob() 处理的是二进制字符串。纯 ASCII 的例子没问题:
btoa("hello");
// "aGVsbG8="
但 Unicode 文本需要先经过一步转字节。用 hello 测过之后,再去编码客户姓名、中文、Emoji 或阿拉伯文,团队常常会被打个措手不及:
btoa("你好");
// InvalidCharacterError in browsers
用 UTF-8 字节:
function utf8ToBase64(text) {
const bytes = new TextEncoder().encode(text);
const binary = Array.from(bytes, (b) => String.fromCharCode(b)).join("");
return btoa(binary);
}
function base64ToUtf8(base64) {
const binary = atob(base64);
const bytes = Uint8Array.from(binary, (ch) => ch.charCodeAt(0));
return new TextDecoder().decode(bytes);
}
const encoded = utf8ToBase64("你好");
base64ToUtf8(encoded);
// "你好"
本站的 Base64 Encode & Decode 工具在浏览器里完成 UTF-8 往返,非 ASCII 文本按文本原样回来,不会变成乱码。
开发者为什么会把 Base64 当加密
Base64 常常出现在真安全机制附近:
- TLS 证书以 PEM 文本形式保存。
- JWT 使用 Base64url 段。
- HTTP Basic 认证用 Base64 编码
username:password。 - SSH 公钥里包含 Base64 编码的密钥材料。
- 加密后的密文经常在存储或传输前做 Base64 编码。
最后这一点造成了最多混淆。被加密的数据可能是 Base64 编码的,但 Base64 不是那份加密。安全来自 AES-GCM、XChaCha20-Poly1305、RSA-OAEP、JWE 或 TLS 这类密码学操作,Base64 只是把结果字节变得便于放进 JSON、邮件、请求头或数据库列里。
在代码和产品文案里用精确的措辞:
encodedBackup // 仅仅是 Base64,OK
encryptedBackup // 只有真的做了加密才 OK
signedState // 有 HMAC 或签名保护完整性时 OK
passwordHash // 由密码哈希算法产生时 OK
这里的命名不是装饰。误导性的名字会让后来的评审者以为存在一道安全边界——而实际上没有。
编码 vs 加密 vs 哈希 vs 签名
这些操作解决的是不同问题:
| 操作 | 主要作用 | 是否需要密钥 | 是否可逆 | 例子 |
|---|---|---|---|---|
| 编码 | 改变表示 | 否 | 是 | Base64、Base64url、URL 编码 |
| 加密 | 隐藏明文 | 是 | 有密钥则可逆 | AES-GCM、XChaCha20-Poly1305、JWE |
| 哈希 | 单向指纹 | 否 | 否 | 校验和用的 SHA-256 |
| 密码哈希 | 慢速密码验证 | 盐加成本参数 | 否 | Argon2id、bcrypt、scrypt |
| 签名 / MAC | 证明完整性和出处 | 是,或者密钥对 | 否 | HMAC、JWS、Ed25519 签名 |
Base64 回答的是「这串字节能走文本通道吗?」。它不回答「谁能读?」「谁改过?」「这个用户能访问那条记录吗?」。
真实安全错误 1:前端代码里藏 API 密钥
这种模式出现在 React 应用、浏览器扩展、WordPress 主题以及内部管理工具里:
// 密钥依然会被发到每个浏览器。
const apiKey = atob("c2stbGl2ZS1hYmMxMjM0NTY3ODk=");
fetch("https://api.example.com/report", {
headers: { Authorization: `Bearer ${apiKey}` },
});
Base64 帮不上忙,因为浏览器同时拿到了编码后的值和解码器。谁都能打开 DevTools,看 JavaScript bundle,跑一次同样的 atob(),把密钥抄走。
修复要从架构层面做:
- 真正的密钥放在服务端。
- 浏览器上的密钥用严格的 scope 和域名限制。
- 敏感的上游调用通过你的后端代理。
- 已经发到用户那边的密钥统统轮换。
- 假设密钥扫描器能解码类 Base64 字符串。
编码一个密钥可能让代码审查更累,但密钥不会更安全。
真实安全错误 2:把密码存成 Base64
这个特别糟,因为它可以在数据库里静静地待上好几年:
// 可逆存储,别这么做。
const storedPassword = btoa(password);
// 之后:
const password = atob(storedPassword);
数据库一泄,每一个密码立刻还原。没有破解成本,因为根本没什么好破解的。
密码存储必须是单向的:
// 用密码哈希库的流程形状。
const hash = await argon2.hash(password);
const ok = await argon2.verify(hash, candidatePassword);
正常的登录系统根本不需要还原用户的密码,只需要用候选密码去比对保存的哈希。
真实安全错误 3:把 Basic 认证误认为加密
那段头看上去足够「编码过」,能骗到人:
Authorization: Basic dXNlcjpwYXNzd29yZA==
它解出来是:
atob("dXNlcjpwYXNzd29yZA==");
// "user:password"
HTTP Basic 认证只有在请求被 HTTPS 保护时才可以接受。TLS 负责传输中的加密。Base64 只是把 username:password 打包成适合放在头里的 ASCII 字符串。
在纯 HTTP 上,网络观察者能直接看到凭据。在日志、反向代理、APM 追踪和支持导出中,Basic 认证头应当按明文密码对待。
真实安全错误 4:把 JWT 载荷当作私密
一个普通签名 JWT 有三段 Base64url:
header.payload.signature
前两段解码成 JSON,第三段是签名。
eyJzdWIiOiIxMjM0Iiwicm9sZSI6ImFkbWluIn0
解出来:
{
"sub": "1234",
"role": "admin"
}
这不代表 token 是假的。签名 JWT 可以做到防篡改的同时仍然可读。签名保护的是完整性:告诉服务端这个 token 是不是可信方签发的、header 或 payload 有没有被改。它不隐藏载荷。
不要在普通签名 JWT 的载荷里放密码、API 密钥、会话密钥、完整信用卡号、医疗信息或私密的个人资料。如果 claim 需要机密性,用 JWE,或者干脆不要把这些数据放进 token。
还有一点:解码不是验证。开发者工具能显示 claims;服务端在信任它们之前,仍然必须验证签名、issuer、audience、过期时间、key ID、算法策略以及应用自身的授权规则。
真实安全错误 5:看起来不透明的 URL 参数
老应用常这么传状态:
/profile?data=eyJ1c2VySWQiOjQyfQ==
解出来:
{
"userId": 42
}
于是有两个问题。谁都能读,谁都能改后重新编码。如果 /profile?data=... 是决定加载哪条用户记录的唯一根据,那 bug 是缺少授权。
更安全的做法:
- 把解码出来的 URL 数据当作不可信输入。
- 在服务端再校验一次权限。
- 状态敏感时优先用一个短的服务端随机引用。
- 客户端如果需要携带防篡改状态,用 HMAC 或签名。
- 私密状态别放在 URL 里,URL 会被复制到日志、浏览器历史、分析和技术支持截图中。
真实安全错误 6:只做了编码却叫「加密导出」
代码审查的另一个气味是这样的函数:
function exportEncryptedBackup(data) {
return btoa(JSON.stringify(data));
}
函数名说加密,代码说编码。
如果这个导出只是想做一段便于携带的文本 blob,改名字:
function exportBase64Backup(data) {
return btoa(JSON.stringify(data));
}
如果确实告诉用户这个导出是加密的,就用真正的认证加密,并把密钥管理明明白白写清楚:经过评审的密码库、随机 nonce/IV、认证 tag、安全的密钥生成,以及密钥存放方案。之后再用 Base64 包一层密文当然可以。
Base64 真正擅长的事
Base64 是有用的,错的是给它派安全任务。
好的用途:
- JSON、XML、HTML 或邮件里的二进制数据。
- MIME 邮件附件。
- 图片或字体的小
data:URI。 - 密文、签名、nonce、盐和公钥的密码学输出格式化。
- JWT 和 JOSE 紧凑序列化的各段。
- 可读性属于产品关切、而不是安全边界的不透明分页游标。
- 调试 API 字段、JWT 载荷和配置 blob。
安全的措辞是「Base64 编码」,不是「加密」。就是这一个词,能避免生产环境里数量惊人的误解。
该用什么代替
如果你伸手去拿 Base64 是想要安全,那就选对应场景的原语:
| 目标 | 用什么代替 |
|---|---|
| 对用户或攻击者隐藏数据 | 认证加密,比如 AES-GCM 或 XChaCha20-Poly1305 |
| 保护传输中的数据 | HTTPS/TLS |
| 存密码 | Argon2id、bcrypt 或 scrypt,配每条密码独立盐和调过的成本参数 |
| 存 API 密钥 | 密钥管理服务,服务端环境、scope、审计日志和轮换 |
| 证明某个值没被改过 | HMAC 或数字签名 |
| 防止用户改 URL 状态 | 服务端授权,加上签名的状态或服务端会话引用 |
| 让 JWT claim 保密 | JWE,或者干脆别把私密 claim 放进 token |
| 把二进制塞进 JSON、邮件或请求头 | Base64 就是对的选择 |
难的部分往往不是算法名字,而是威胁建模、密钥管理、轮换、访问控制,以及一开始就得决定谁可以看明文。
一份实用的代码审查清单
先找出 Base64 的使用点:
rg "btoa\\(|atob\\(|base64|Buffer\\.from\\(.*base64|toString\\('base64'\\)"
对每个命中的位置分类:
- 传输:为 JSON、MIME、data URI、PEM 或存储做字节到文本的转换。通常没问题。
- 加密后的格式化:密文、nonce、签名、盐或公钥。密码学正确的话通常没问题。
- 不透明接口:游标、邀请码或客户端携带的状态。检查授权和防篡改。
- 藏秘密:API 密钥、密码、token、凭据、私密 claim 或用户数据。这不 OK。
- 命名误导:变量或 UI 文案说加密、安全、受保护、秘密、掩码、隐藏。要么改名字,要么改实现。
然后问自己:
- 编码后的值会不会到达浏览器、移动端、用户、日志采集器、代理或支持工具?
- 旁边是不是就摆着解码器?
- 解码之后有没有服务端在做权限校验?
- 如果客户端能修改,这个值是否被签过名?
- 如果是 JWT,做出信任决策前是否校验过签名?
- 如果是密码,为什么这个东西是可逆的?
这样能让评审踩在地上。data URI 里的 PNG 是无聊话题;JavaScript bundle 里的生产 API 密钥不是。
事件响应:找到 Base64「秘密」怎么办
如果你在源代码、日志、工单、分析、URL 或数据库导出里发现了 Base64 编码的秘密:
- 本地解码确认到底是什么。
- 把解码后的值当作已经泄露。
- 轮换任何可能已经被看到的凭据、token 和密钥。
- 尽可能从源代码、日志、截图、工单中移除编码副本。
- 用服务端密钥、有 scope 的公钥、签名的状态、真正的加密或密码哈希替换原设计。
- 加一条审查规则,让同样的模式别再回来。
别浪费时间讨论那串 Base64「难不难被注意到」。自动化扫描器、浏览器用户和攻击者都能解开。
你真正需要一个加密 token 的时候
标准的签名 JWT 使用 JWS:载荷可读、签名防篡改。如果你确实需要 claim 的机密性,用 JWE(JSON Web Encryption)。
一个紧凑 JWE 有五段 Base64url:
protected-header.encrypted-key.iv.ciphertext.authentication-tag
这些段仍然是 Base64url 文本。机密性来自 JWE 的加密算法和接收方的密钥,不是来自 Base64url 本身。
在很多 Web 应用里,更简单的设计更好:把敏感状态留在服务端,只给浏览器发一个短的随机会话标识。不是每个可读的 JWT 都要变成 JWE。
常见问题
Base64 是加密吗?
不是。Base64 是没有密钥的可逆编码。任何拿到这串字符串的人都能用浏览器函数、终端命令、在线工具或几行代码解开。
Base64 至少算混淆吧?
只是最弱的意义上。它也许能挡住随意一眼扫过的人,但挡不住开发者、攻击者、扫描器、浏览器扩展、日志读取器和支持工具。
Base64 对密码或 API 密钥安全吗?
不安全。密码需要 Argon2id、bcrypt、scrypt 这类慢速密码哈希。API 密钥应该留在服务端,或者放在带 scope、审计能力和轮换机制的密钥管理服务里。
谁都能读 JWT 载荷吗?
普通的签名 JWT 是的。header 和 payload 就是 Base64url 编码的 JSON。签名在验证之后能证明完整性,但并不隐藏 claim。
既然不安全,Basic 认证为什么要用 Base64?
Basic 认证用 Base64 是为了把 username:password 塞进 HTTP 头。传输层的加密由 HTTPS/TLS 提供。没有 HTTPS,Basic 认证的凭据在网络上基本就是明文。
Base64url 比 Base64 更安全吗?
不。Base64url 把 + 和 / 换成 - 和 _,且常常省去填充,好让值在 URL 和 JWT 里工作。它仍然是可逆编码。
要加密的话该用什么代替 Base64?
用经过评审的认证加密设计,比如 AES-GCM、XChaCha20-Poly1305 或 JWE,配上妥当的密钥生成、密钥存储、轮换和访问控制。不要自造密码学格式。
那 Base64 到底是干嘛的?
Base64 把字节表示成文本:JSON 的二进制字段、邮件附件、data URI、PEM 文件、签名、密文格式化、URL 安全的 token 段。它是包装层,不是安全边界。
在浏览器里编码和解码
现在就想看看某个 Base64 字符串?fixjson.org 上的 Base64 Encode & Decode 可以在浏览器里本地编码或解码文本,包括 UTF-8 内容。用它来看看 JWT 载荷、API 响应字段或 data: URI,但别把解码当成验证或解密。
相关工具与指南
- Base64 Encode & Decode — 在浏览器里本地编码和解码 Base64。
- 解码 Base64 字符串和 JWT 载荷 — Base64 与 Base64url 的实用解码。
- 如何解码 JWT — 读取 claim,理解为什么解码不是验证。
- 敏感 JSON 与本地工具 — 判断什么内容可以放心贴进浏览器工具。
- JSON Stringify — 查看并转义嵌套的 JSON 字符串。
- RFC 4648:Base64 标准 — Base64 与 Base64url 的正式背景。
- RFC 7519:JSON Web Token — JWT 结构与安全注意事项。
参考
- RFC 4648 — Base64、Base64url、填充、字母表与安全注意事项。
- RFC 7617 — HTTP Basic 认证及其 Base64 凭据格式。
- RFC 7519 — JWT 结构、信任决策与隐私考量。
- RFC 7516 — JSON Web Encryption 与紧凑 JWE 序列化。
- MDN btoa 与 MDN atob — 浏览器 Base64 原语与 UTF-8 注意点。
- OWASP Cryptographic Storage Cheat Sheet — 加密、认证模式和密钥管理。
- OWASP Password Storage Cheat Sheet — 密码哈希指南。
最后审阅:2026 年 7 月。