需要使用 Base64 格式吗?那么本网站正好适合您!使用我们的在线工具对数据进行编码或解码,便捷好用。

Bash 中的 Base64 编码:完整指南

你手上有字节,需要变成一个字符串。一个必须住在 JSON 体里的文本文件。一个必须躺在配置行里的图片。一个要穿过 URL、环境变量或 HTTP 头的令牌。一个属于证书库的私钥。这就是 Base64 编码在 shell 里的日常活儿,而 shell 给出的答案是一个小小的、可移植性惊人的单条命令。

一口气说清这笔交易:Base64 把原始数据每三个字节重写成四个字符,字符来自一个 64 个字母的字母表(A-Z、a-z、0-9,再加 +/),当字节数不是三的倍数时,用一两个 = 号给尾巴补上。本站首页完整解释了格式;在这里,我们把时间花在生产文本、为目的地选对方言、以及睁着眼睛付清体积账单上。有一个数字值得揣在口袋里:编码形态通常比原始大约三分之一,每三个字节四个字符,而且它会一再回来找你。

角色阵容很小。coreutils 的 base64 命令(GNU 或较新的 Rust uutils 家族)、同家族的 basenc 负责 URL 安全方言、没有 coreutils 的机器用 openssl base64、嵌入式系统用 BusyBox applet,还有 macOS 上的 BSD 风味。五个工具,一份活儿,几个值得知道的标志。

挑你的编码器

这些工具全都从标准输入或文件读字节、把文本写到标准输出,所以都能塞进同样的流水线。差别在于默认折行和可用的方言:

工具 住在哪里 默认折行 什么时候用它
base64(coreutils) Linux,以及通过 Homebrew 的 macOS 76 字符 默认之选;想要单行就加 -w 0
basenc(GNU coreutils) 装了 coreutils 的 Linux 76 字符 需要 --base64url、base32、base16 或兄弟们
openssl base64 任何装了 OpenSSL 的地方 64 字符 没有 coreutils 时;单行用 -A
busybox base64 Alpine、嵌入式 Linux 76 字符 极简系统;同样的标志,更小的身体
base64(BSD/macOS) macOS、各 BSD 不折行(一整行) macOS 原生活儿;-b 设置行宽

把那列折行读两遍,因为它是各家族之间安静的差别。Coreutils 和 BusyBox 默认在 76 处折行,OpenSSL 在 64 处折行,BSD 工具根本不折行。没有谁错,它们只是继承了不同的约定(MIME 说 76,PEM 说 64,BSD 工具则比折行这个习惯更古老)。当你的消费者在意时,显式设置行宽,永远别指望默认值。

文本先行:那个看不见的换行

在 shell 里编码文本,第一个陷阱就是:echo 会加一个换行。"hello" 这五个字母,一经过 echo 就变成六个字节,而第六个字节会悄悄溜进输出,无形且永久:

echo "hello" | base64

它输出 aGVsbG8K,最后一个字符编码的是那个换行。修复方法就是字节数重要的文本该用的那个:printf 带格式,不加任何装饰:

printf '%s' "hello" | base64

现在输出是 aGVsbG8=,正好五个字节,最后一个字符是填充号而不是活的字节。同一条规则适用于 here-string,它和 echo 一样会追加行尾换行:base64 <<< "hello" 又会给你 aGVsbG8K 版本。拿不准的时候,编码之前先问问自己:最后一个字节是什么。

对任何你想要单行的东西,加上 -w 0(或者下面 -w 0 的表亲们),它还会去掉命令本来要输出的行尾换行:

printf '%s' "hello world and more" | base64 -w 0

那是一行干净、不断行的文本,没有行尾换行,可以直接丢进 URL、JSON 值或配置文件,不需要任何额外仪式。

文件与折行宽度

文件是常见情况,而且每个实现都接受 FILE 参数,这让字节彻底远离 shell 的引号机器:

base64 -w 0 report.pdf > report.b64

不加 -w 0,输出就会按 76 字符折行而来,而这恰恰是 MIME 消费者想要的:

base64 report.pdf > report.mime.b64

宽度是一个你可以按消费者调节的旋钮。七十六是 RFC 2045 的 MIME 约定,六十四是证书和密钥使用的 PEM 约定,零则是为 URL 和 API 准备的一整条不断行的线:

base64 -w 64 key.bin | head -2

如果消费者住在 Windows 上并期待 CRLF 行尾,在折行之后转换,而不是之前:

base64 -w 76 attachment.bin | sed 's/$/\r/' > attachment.crlf.b64

对 OpenSSL 这条路,单行模式的等价物是 -A 标志,它同时压掉行尾换行:

openssl base64 -A < report.pdf

在发出一个折行文件之前,做个大小健全性检查不花什么成本,却能抓住多得惊人的错误(一个文件被编码了两次、一个文件用了错误的输入编码):

wc -c report.pdf
base64 report.pdf | wc -c

第二个数字应该是第一个的约 4/3,外加每条折行行一个换行字节。如果差得太离谱,停下来,看看你到底喂给编码器的是什么。

URL 安全 Base64:换掉两个讨厌鬼

标准字母表里的两个字符,+/,是问题儿童:URL 查询字符串里的 + 意味着空格,/ 可能被看成像路径分隔符,而且这两个字符一让字符串进入 URL、cookie 或文件名,就会被迫走百分号编码。RFC 4648 第 5 节用一个方言解决了这个问题:恰好把这两个字符换成 -_,并去掉填充,因为 URL 很少需要宣布精确的字节长度。

shell 里的配方是交换加修剪:过一遍 tr 换字母表,再过一遍去掉填充:

printf '\376\117\202' | base64 -w 0 | tr '+/' '-_' | tr -d '='

这三个字节通常编码成 /k+C,放在 URL 里很难看;这条流水线把它变成 _k-C,四个字符,想去哪都行。交换是按位置进行的,所以方向很容易搞混:编码走 tr '+/' '-_'(加号变连字符,斜杠变下划线),而反向 - 属于解码那一边 - 走 tr '_-' '/+'。方向搞混了不会报错,只会产出不同的字节,而这是最糟糕的一种要上线的 bug。

每当字符串要离开 shell 的控制,这种方言就很重要:JWT 各段、查询字符串里的令牌、cookie 或文件名里的值,以及任何会被另一个系统当作 URL 一部分来读的标识符。GNU 的 basenc 原生产出这种方言,填充仍在位:

printf '%s' "hello" | basenc --base64url

如果消费者要去掉填充的形态(多数如此),用 tr -d '=' 剥掉它。

在 shell 里铸造一个 JWT

在 API 世界,JSON Web Token 是 URL 安全 Base64 最显眼的消费者。一个紧凑 JWT 是三个用点号连接的 base64url 段:头部、载荷和签名,按 RFC 7515。前两个是纯 JSON;签名是前两段加一个点号连起来的文本的二进制摘要,而这正是 openssl 擅长的东西。

key="supersecretkey"
h=$(printf '%s' '{"alg":"HS256","typ":"JWT"}' | base64 -w 0 | tr '+/' '-_' | tr -d '=')
p=$(printf '%s' '{"sub":"42","name":"homer"}' | base64 -w 0 | tr '+/' '-_' | tr -d '=')
s=$(printf '%s' "$h.$p" | openssl dgst -sha256 -hmac "$key" -binary | base64 -w 0 | tr '+/' '-_' | tr -d '=')
printf '%s.%s.%s\n' "$h" "$p" "$s"

它输出一个紧凑的 HS256 JWT,任何平台上的任何标准库都会接受。注意分工:Base64 部分是字母表,openssl dgst -sha256 -hmac 部分是密码学,点号连接是格式。在脑子里把这三份活儿分开放,流水线就一目了然。

现场的三条告诫。第一,MAC 是在前两段加一个点号的 ASCII 文本上计算的,所以签名时各段必须已经处于最终的 base64url 形态;签名之后再折行或再补填充会毁掉令牌。第二,密钥留在令牌外面:签名证明谁签的,密钥让秘密保持秘密。第三,在 shell 脚本里铸造是测试和自动化工具,不是替代那个真正要签发和验证这些令牌的服务器,而且用 alg: none 铸造的令牌什么也证明不了。

Data URI:骑在字符串里的文件

RFC 2397 定义了 data: URL 方案,它的 Base64 形态让文件能住进 URL:data:,然后是一个可选的媒体类型,然后是载荷为 Base64 编码时的 ;base64,然后是一个逗号,然后是数据。省略媒体类型时,默认是 text/plain;charset=US-ASCII,这是一个值得知道的陷阱,因为大多数人想要的是图片或 JSON 文档,而不是 ASCII 文本。

printf 'data:text/plain;base64,%s\n' "$(printf '%s' "hi there" | base64 -w 0)"

它输出 data:text/plain;base64,aGkgdGhlcmU=,一个完整、自包含的 URL,浏览器会乐呵呵地显示它。对图片,同样的形状,配一个真正的媒体类型:

printf 'data:image/png;base64,%s\n' "$(base64 -w 0 icon.png)" > icon.uri

把结果粘进 HTML img 标签的 src 或 CSS 的 background,图片就跟着文档一起走,不需要第二次 HTTP 请求。坑全都和大小有关:RFC 自己说这个方案只对短值有用,浏览器有自己的 URL 长度限制,每个内联字节都要在图片本身大小之上再付 33% 的开销,而一个满是 data URI 的页面,对那些图片来说就没有任何缓存故事。对小图标和一次性的嵌入图形,它很美妙;对照片库,它是一笔税。

秘密、配置与环境变量

Base64 出现在配置和秘密工作里,理由很具体:它把任意字节 - 包括空格、引号和换行 - 变成一个字符串,能安然穿过 export、一行配置或一个 JSON 字段,不需要任何引号杂技。Kubernetes 是最显眼的例子:secret 的 .data 下的每个字段都是 Base64,所以在 shell 里创建一个 secret 就是编码而已:

kubectl create secret generic app --from-literal=password='s3cret'

API 服务器把密码以 czNjcmV0 的形式存在 .data 下,而任何能访问这个 secret 的节点都能用一次解码把它读回来。同样的招式对你的配置文件也管用:

export API_TOKEN_B64=$(printf '%s' "$API_TOKEN" | base64 -w 0)

或者,对一个应用启动时读取的文件:

printf 'token=%s\n' "$(printf '%s' "$API_TOKEN" | base64 -w 0)" >> app.conf

现在到了该上墙的那条警告:Base64 是编码,不是加密。RFC 4648 的安全章节对此说得直白,指出这种编码"在视觉上隐藏了否则很容易辨认的信息,例如密码,但不会提供任何计算上的保密性",而且正是这种误解在真实的安全事故中酿成过祸:有人把一段"受保护"的协议交互粘贴进 bug 报告,顺手就把凭据泄露了。如果值必须是秘密,就加密它(然后把密文 Base64 起来存储);如果 Base64 是你手里唯一的东西,那么从它离开屏幕的那一刻起,就把编码后的值当纯文本对待。

Unicode、字符集与底下的字节

编码器读的是字节,不是字符,而 shell 交给它的是 locale 和命令产出的任何字节。对 UTF-8 文本来说,这通常正是你想要的:héllo 里的 é 已经是两个字节 c3 a9,编码只是把它们带着走:

printf 'h\xc3\xa9llo' | base64

它输出 aMOpbGxv,另一边的 UTF-8 消费者能逐字节地把 héllo 拿回来。麻烦从源头不是 UTF-8 的时候开始。一个装着同一个单词的 Latin-1 文件里,é 是单个字节 e9,把这些字节原样编码,产出的文本就只有 Latin-1 消费者能读回来。先转换,后编码:

iconv -f ISO-8859-1 -t UTF-8 note.txt | base64 -w 0

还有两条字节层面的事实。UTF-8 BOM,文件开头的三个字节,编码后是 77u/,除非你先剥掉它,否则它会永远坐在你的解码输出开头:

sed '1s/^\xef\xbb\xbf//' file.txt | base64 -w 0

而 locale 从不改变编码本身,因为编码器是一台字节机器;它改变的只是你输入的内容。当输出看起来不对时,检查你喂进去的字节,而不是你跑的那个编码。

邮件、API 与上传

邮件是 Base64 学会礼仪的地方,而那些礼仪至今仍是约定。SMTP 历史上只承载 7 位 ASCII,所以附件以 Base64 形式出行,按 76 字符折行、带 CRLF 行尾,按 RFC 2045。为 MIME 部分产出那个精确形状,就是折行加行尾转换:

base64 -w 76 attachment.bin | sed 's/$/\r/' > attachment.mime

老门卫在嵌入式系统上仍在服役:BusyBox 的 uuencode-m 标志,产出 MIME Base64,包在熟悉的 begin-base64 帧里,而它的兄弟 uudecode 读得回来:

busybox uuencode -m photo.jpg < photo.jpg > photo.uu

API 和上传用 JSON 外衣装着同一个想法:二进制变成 JSON 字段里的 Base64 字符串,由 curl 携带。在 shell 变量里构建请求体,引号就能保持诚实:

body="{\"file\":\"$(base64 -w 0 upload.bin)\"}"
curl -fsS -X POST https://httpbin.org/post -H "Content-Type: application/json" -d "$body"

这里住着两个互操作陷阱。第一,查一下 API 想要哪套字母表:有的期望标准 Base64,有的期望 URL 安全方言,带着 + 字符的字符串发到 URL 安全的端点(或反过来)会验证失败,或者更糟,解码成错误的字节。第二,盯住双重编码,那个经典 bug:脚本编码了一个值,服务器又编码一次,往返需要两次解码才能解开。

当载荷变大

编码器和解码器一样,是一台流式机器:分块读、分块写,所以一个 10 GB 的 tar 包不需要 13 GB 的内存,命令在大输入上乐呵呵地跑上几分钟,内存占用平稳。大小数学是你唯一需要的规划工具:输出是每三个输入字节四个字符,外加每条折行行一个字节,所以一个 300 MB 的文件变成大约 400 MB 的文本。对任何文件做个快速现实检查:

base64 -w 0 big.bin | wc -c

当文本本身必须通过一个有大小限额的通道(邮件附件上限、工单系统、IM 消息)时,切分编码后的形态,永远别切原始二进制,这样每个块仍然是你可以粘贴、压缩或转发的普通文本:

base64 -w 0 big.bin | split -b 4000 - part_

那会产出一串 4000 字符的片段;接收方按顺序 cat 回去,解码一次。而当载荷可压缩时,先压缩再编码,因为 Base64 会在数据已有的冗余之上再加一层:一个项目目录的 tar 包,通常在 gzip 下先缩小好几倍,然后才轮到这 33% 的 Base64 附加费:

tar czf - project/ | base64 -w 0 > project.b64

速度不会是你的约束。这些编码器在现代机器上每秒能推过好几个 GB;一个 200 MB 的文件用 coreutils 和 OpenSSL 实现大约只要零点一秒,甚至 BusyBox - 常见实现里最慢的 - 也在零秒多内完成(在一台现代机器上实测 200 MB 大约四分之一秒,比 coreutils 慢几倍,但远谈不上瓶颈)。真实流水线里的瓶颈几乎总是网络,而不是编码。

咬人的小角色

编码侧的坑比解码侧的小一些,这才公平:

会发生什么 修复
echo 喂给编码器 行尾换行溜进输出,最后一个字符编码了它 字节数重要的文本用 printf '%s'
依赖默认折行 76、64 或零,取决于工具;单行消费者会被折行输入卡住 显式设置 -w 0(或消费者想要的宽度)
输出里的行尾换行 折行模式以换行结尾,被捕获时污染 URL 和 JSON 单行用 -w 0,或者通过 $(...) 捕获,它会剥掉换行
URL 里的 +/ 加号在查询字符串里被读成空格;两者都强制百分号编码 任何进入 URL 的东西都用 URL 安全方言
tr 方向搞反 交换产出有效但错误的字节,处处没有错误 编码是 tr '+/' '-_';解码是 tr '_-' '/+'
编码一个已编码的值 双重编码,需要两次解码才能解开 编码前先查源头是否已经是 Base64
输入里的 UTF-8 BOM 每个解码输出开头多出三个字节 先剥掉 BOM:sed '1s/^\xef\xbb\xbf//'
把真正的秘密存成 Base64 一条命令就能还原;RFC 记录过真实发生的凭据泄露事件 保密靠加密,Base64 只管传输形态
假设消费者的字母表 标准与 URL 安全不匹配,验证失败或解码错误 读 API 文档;按消费者要求的方言编码

让你保持安全的习惯

  • 点名字节数。文本用 printf '%s',文件用 FILE 参数,尺寸重要的东西发出去之前先做 wc -c 健全性检查。
  • 显式设置折行。URL 和 JSON 用 -w 0,MIME 用 -w 76,PEM 用 -w 64。永远别把行宽留给工具的默认值。
  • 为目的地选字母表。邮件和文件用标准,令牌和 URL 用 URL 安全,编码之前先查消费者的文档。
  • 先压缩再编码。对任何可压缩的载荷,先 gziptar czf;那 33% 的附加费加在你交给编码器的东西上。
  • 切文本,不切二进制。大小限额挡路时,split 编码后的形态,让每个块保持可粘贴安全,重组成序之后再单次解码。
  • 把 JWT 的三份活儿分开放。字母表、密码学、格式:编码各段,对连接后段落的 ASCII 文本签名,然后输出。调换顺序,令牌就坏了。
  • 永远别让 Base64 冒充加密。如果值是秘密,就加密它,再编码密文。如果它不是秘密,说出来,然后别再担心。

shell 里编码的简短历史

  • 1980 年,伯克利。Mary Ann Horton 在加州大学伯克利分校写下 uuencodeuudecode,让二进制文件能在 Unix 系统之间穿过邮件。这个名字"Unix 到 Unix 编码"就是这个格式的出生证明,而在接下来大约十年里,这就是 shell 用户用来编码的工具。
  • 拨号上网时代。UNIX 上的 uuencode 和 TRS-80 及 Apple II 上的 BinHex(Macintosh 落后一小步)用不同的字母表解决同一个问题,每一种都只信任自己终端能打印的字符。
  • 1993 年。MIME 在 RFC 1521(后来的 RFC 2045)里为邮件标准化了 Base64,连同 coreutils 默认值至今仍在沿用的 76 字符折行。
  • 2006 年之前的 Linux。没有 base64 命令。shell 脚本伸手拿 openssl base64uuencode -m、Perl 或 Python,而 OpenSSL 的习惯根深蒂固到野外一半的老单行命令仍以它开头。
  • 2006 年 8 月 15 日。coreutils 6.0 发行了 base64 命令,其 NEWS 文件把它记为"base64 encoding and decoding (RFC 3548) functionality,"单命令时代就此开始。几个月后的 2006 年 10 月,RFC 4648 把字母表家族正式化,包括本文反复伸手去拿的那个 URL 安全方言。
  • OS X 10.7。macOS 自带自己的 base64,BSD 风味,默认不折行 - 这就是为什么"直接跑 base64"在可移植脚本里需要一个平台检查。
  • 2024 年。coreutils 9.5 改变了解码器对待无填充和非规范输入的方式,实际效果是编码器白白获益:老版本 GNU 会拒绝的输出现在能干净地解码。这个格式的编码侧是稳定的一方;动起来的是解码器。
  • 2025 年。coreutils 的 Rust 重写(uutils)成为当前 Ubuntu 发行版的默认。同样的命令、同样的标志、新的引擎,还有从 C 版本继承来的同样的 76 字符默认值。

小小的奇迹

  • 格式的名字在每台机器上都成立。printf 'base64' | base64 在 GNU、uutils、BusyBox、OpenSSL 和 macOS 上都给出 YmFzZTY0。从 2006 年起一直如此,也将永远如此。
  • 一个什么也没有的文件编码成一面 A 墙。喂它三个 NUL 字节,输出是 AAAA,因为三个零值字节映射到字母表里四个零下标,而它们各自由 A 表示。一个以长长一串 A 开头的 .b64 文件,通常是原始文件里的零填充(NUL 字节),不是什么谜团。
  • 33% 的税不打折。每三个字节四个字符,没有压缩,没有第二次机会。唯一的出路是先压缩数据,这就是为什么 tar czf 才是大载荷流水线的真英雄。
  • 两个字符惹出了全部的 URL 麻烦。+/ 是字母表里仅有的、曾经需要替身的成员,而这个格式的一整个方言存在的意义就是送它们退休。62 和 63,字母表的最后两个位置。
  • 十一个字符,六十四比特。一个 YouTube 视频 ID 是 11 字符的 base64url 字符串,一个穿了 URL 外衣的 64 位数字,这就是它能在 URL 里穿行而不带一个百分号的原因。
  • Git 最著名的冒名顶替者。git diff --binary 里的二进制块看起来像 Base64,但那些 z 前缀的行是一种 base85 风格的自有方言。一眼就知道那不是你的字母表;而一次"grep 再解码"的绕道,就让你丢掉二十分钟。
  • 每个工具的折行都不同,而且是故意的。MIME 用 76,PEM 用 64,BSD 工具是零:三个默认值,三种继承来的约定,一个格式。行宽从来都是你选的;工具只是记住了不同的默认。
  • 编码器永远不会在你的数据上失败。和它的解码表亲不同,编码器没有无效输入、没有损坏、没有严格模式。它收字节,给字母,每次都是。这篇文章里的 bug 全在你交给它的字节,和你把它们送去的地方。

而当旅程指向反方向时 - 当一长串字母、数字,外加偶尔的连字符或下划线落到你的终端里,你需要把字节拿回来时 - 下面链接的那篇 Base64 解码相关文章会用同样的深度讲那个仪式,从无填充的 JWT 段,到解码器藏着的每一个换行陷阱。

最后更新: 2026-09-08

相关文章: Bash 中的 Base64 解码:完整指南