Ruby 中的 Base64 解码:完整指南
在你和原始数据之间,隔着一堵字符墙:大小写字母、数字,也许还有一个加号、斜杠或连字符,末尾可能还停着一个等号。你的编辑器完全不知道这是什么文件类型。你的数据库把它塞进了一个文本列。它出现在 HTTP 头里、URL 里、YAML 键里,或者一张带着 .b64 附件的支持工单里。你一眼就认出了它 - Base64 - 现在你需要把字节拿回来。在 Ruby 里,这个需求离你只有一条 require 语句和一个方法调用那么远。
万一这个格式对你还很陌生,这里是 30 秒速成版。Base64 每三个字节重写一次原始数据:每三个字节变成四个取自 64 符号字母表的字符;当输入不能被三整除时,会追加一两个 = 字符作为填充,让输出永远落在四的倍数上。解码是反方向的旅程 - 四个字符进,三个字节出 - 所以结果永远比输入小,大约是输入的四分之三。本站首页已经把这个格式的每个细节都讲了一遍,所以这份指南把精力花在它该在的地方:这份工作的 Ruby 一侧。
好消息:每一个 Ruby 安装都自带完整的解码工具箱。Base64 模块什么都不用装,它的三个解码器小到你可以一口气读完它们的全部源码。提醒一句:你第一时间想到的那个解码器,恰好也是最不吭声的那个,这对邮件来说是美妙的品质,对安全来说却是糟糕的品质。读完这份指南,你会确切知道每个解码器接受什么输入,如何把它交回来的字节变成 Ruby 愿意让你使用的文本,以及面对 Ruby 开发者真正会解码的每一种载荷该怎么办 - JWT、认证头、data URI、邮件正文、PEM 护甲、文件、配置数据块,还有超大的那些。
认识工具箱
一切从一条 require 开始。没有安装步骤,没有平台怪癖,没有需要编译的原生扩展:
require "base64"
puts Base64::VERSION
# => 比如,原版 Ruby 3.3 上是 0.2.0
下面这张表就是工具箱里解码半边的全部成员,按你使用每个方法的频率排序:
| 解码器 | 对表外字符的处理 | 填充规则 | 出问题时怎么办 |
|---|---|---|---|
Base64.decode64(str) |
忽略所有不在标准字母表里的东西,包括换行和空格 | 怎么都行,连错误的填充都接受 | 什么都不会发生 - 它从不抛异常,只是返回它能解出来的部分 |
Base64.strict_decode64(str) |
拒绝标准字母表之外的任何字符 | 必须存在且完全正确 | 抛出 ArgumentError |
Base64.urlsafe_decode64(str) |
接受 URL 安全字母表,也接受标准字母表,其余一律拒绝 | 可选,但如果存在就必须正确 | 抛出 ArgumentError |
如果你想知道工具在底层到底干了什么:这个模块的整个解码半边,只是核心 pack/unpack 机制上两个模板的薄薄一层封装,而那套机制是用 C 实现在 Ruby 核心里的:
# 整个模块的解码半边,浓缩版
def decode64(str)
str.unpack1("m")
end
def strict_decode64(str)
str.unpack1("m0")
end
m 模板是宽容的读者,m0 是严格的那位,就这一个字符的差别,解释了前两个解码器之间全部的性情差距。因为重活是在核心速度下完成的,模块保持纯 Ruby 身份,却能在个位数毫秒里嚼完几兆字节。
decode64:变色龙
Base64.decode64 是那个对一切都点头的解码器。喂它一个干净的载荷,它就解码;喂它一团满是换行的 MIME 风格数据,它耸耸肩;喂它一个根本不是 Base64 的字符串,它把能挤出来的东西都交还给你,连一个警告都不会有:
require "base64"
Base64.decode64("aGVsbG8gd29ybGQ=")
# => "hello world"
Base64.decode64("Zm9vCmJh\ncgptYW4=\n")
# => "foo\nbar\nman"
第二行就是它的全部个性浓缩在一个例子里。解码器跳过所有不属于标准字母表的部分 - 换行、空格、偶尔冒出来的控制字符 - 然后解码剩下的部分。这正是 MIME Base64 应有的行为,所以任何穿过邮件的载荷,decode64 都是合适的工具。
另一面正是它危险的原因。因为解码器从不抱怨,它也永远不会告诉你输入错了:
Base64.decode64("not base64 at all!")
# => 十字节看起来完全像模像样的垃圾
Base64.decode64("====")
# => ""
第一个例子找到碰巧是有效字母表字符的那些字符,解码它们,交回一堆你说不定会直接写进文件的字节。第二个例子对四个填充字符组成的字符串返回一个空字符串。不抛异常,不记日志。如果你的输入不可信,这种沉默就是你想要关掉的功能 - 而接下来的两个解码器正是为此而生。
还有一个值得知道的怪癖,因为它正是那种能在生产环境里躲上好几个月的东西:解码在第一个 = 字符处停止。填充之后的任何东西都不是错误;它们只是永远不会被读取:
Base64.decode64("aGVsbG8=Zm9vYmFy")
# => "hello" "Zm9vYmFy" 那部分对解码器来说是隐形的
strict_decode64:守门员
Base64.strict_decode64 是那个拿着记录夹的解码器。它只接受标准字母表(A 到 Z、a 到 z、0 到 9、加号、斜杠),它要求任何填充都必须分毫不差,只要有一条规则被违反,它就拒绝产出哪怕一个字节:
Base64.strict_decode64("aGVsbG8gd29ybGQ=")
# => "hello world"
Base64.strict_decode64("aGVsbG8gd29ybGQ")
# => 抛出 ArgumentError
Base64.strict_decode64("Zm9vCmJh\ncgptYW4=")
# => 抛出 ArgumentError
最后一行才是关键:同一个载荷,decode64 乐呵呵地解出来了,现在却因为一个换行抛出了异常。缺填充、多填充、连字符、下划线、空格 - 统统都是罪行,整个载荷要跟着一同完蛋:
begin
Base64.strict_decode64("aGVsbG8")
rescue ArgumentError => e
puts e.message
end
# => invalid base64
守门员甚至会给那些你根本想不到要查的格式角落执勤。当一个 Base64 字符串以填充结尾时,最后一个字符里有些比特永远不会被使用,而 RFC 规定,合规的编码器必须把这些比特置零。Ruby 会去验证:
Base64.strict_decode64("QQ==")
# => "A"
Base64.strict_decode64("QR==")
# => 抛出 ArgumentError(填充比特不是零)
如果解码器马虎一点,第二个字符串会解出和第一个一样的字节。但 Ruby 不马虎。实际上,这使 strict_decode64 成为任何不是你亲手编码的输入的正确默认选择:它会把拼写错误、截断和错误的字母表变成响亮且可捕获的异常,而不是悄无声息的损坏。
urlsafe_decode64:外交官
Base64.urlsafe_decode64 是为那些在 + 和 / 是保留字的地方旅行的载荷而存在的:URL、令牌、数据库标识符。它在内部把 URL 安全字母表(连字符和下划线)转回标准字母表,归一化填充,然后把结果交给严格解码器:
Base64.urlsafe_decode64("SGVsbG8gd29ybGQ")
# => "Hello world"
Base64.urlsafe_decode64("SGVsbG8gd29ybGQ==")
# => 抛出 ArgumentError(15 个字符需要 1 个填充字符,而不是 2 个)
第一个例子展示了它最有用的特性:不带填充的输入没问题。如果字符串没有填充且长度不是四的倍数,解码器会替你把缺少的 = 字符补上 - 而这正是 URL 安全 Base64 最大的消费者 JSON Web Token 产出的形式。不过,如果填充已经存在,它就必须是正确的,就像严格解码器一样。
有一个怪癖,文档里没有大声嚷嚷:这位外交官两种语言都会说。因为该方法在做严格解码之前会先改写连字符和下划线,所以它也接受来自标准字母表的字符串:
Base64.urlsafe_decode64("aGVsbG8=")
# => "hello" 标准字母表也被接受
这种宽容很方便,但也意味着你不能用这个方法来判断一个载荷来自哪个字母表。如果这对你很重要,就在解码前自己检查一遍字符。
而且和 decode64 不同,外交官对空白字符毫不留情。URL 安全载荷里任何位置的换行都会引发 ArgumentError,所以如果你的输入来自被折行的文件,先剥掉换行。
字节不是文本:编码步骤
这里有一个连经验丰富的开发者都会栽跟头的步骤,因为 Ruby 让它显形了。解码后的 Base64 字符串总是被贴上 ASCII-8BIT 编码(也叫 BINARY)的标签,不管原始数据是 PNG、JWT 载荷,还是一封 UTF-8 的情书:
bin = Base64.decode64(Base64.strict_encode64("h\u{e9}llo"))
puts bin.encoding
# => ASCII-8BIT
puts bin.bytes
# => [104, 195, 169, 108, 108, 111]
如果载荷是二进制的 - 图片、zip 文件、哈希 - 就让它保持原样,用 File.binwrite 写出去。不转换,不打问。如果载荷是文本,这些字节几乎可以肯定是 UTF-8,你需要告诉 Ruby:
text = Base64.decode64(payload)
text.force_encoding("UTF-8")
if text.valid_encoding?
puts text
else
puts "not valid UTF-8 after all"
end
这两个调用干的是不同的活。force_encoding 只是给字节重新贴标签;valid_encoding? 随后验证它们确实构成合法的 UTF-8。按这个顺序执行,因为先验证一个 BINARY 字符串,它没有任何东西可验证。还有一个值得记一辈子的小型比较陷阱:Ruby 只在一个 BINARY 字符串和一个 UTF-8 字符串都是纯 ASCII 时才认为它们相等,所以在拿解码后的文本和原文比较之前,先重新贴标签:
decoded = Base64.decode64("aMOpbGxv")
puts decoded == "h\u{e9}llo"
# => false 同样的字节,不同的标签
decoded.force_encoding("UTF-8")
puts decoded == "h\u{e9}llo"
# => true
JWT:不用密钥读令牌
一个 JSON Web Token 就是三段 Base64 字符串用点号钉在一起:头部、载荷、签名。前两段是 URL 安全、不带填充的 Base64 编码的 JSON 文档,这意味着任何看到令牌的人都能读它 - 包括你,而且完全不用任何库:
require "base64"
require "json"
token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIn0.dW5zaWduZWQ"
header_part, payload_part = token.split(".")[0, 2]
JSON.parse(Base64.urlsafe_decode64(payload_part))
# => {"sub"=>"1234567890", "name"=>"Alice"}
真正干活时你会用 jwt gem,它负责真正保护你的那部分 - 签名 - 以及声明校验:
# Gemfile: gem "jwt"
require "jwt"
token = JWT.encode(
{ sub: "1234567890", name: "Alice", exp: Time.now.to_i + 3600 },
"my-secret-key",
"HS256"
)
payload, header = JWT.decode(token, "my-secret-key", true, algorithm: "HS256")
puts payload["name"]
# => Alice
这里有两点安全提醒,因为两者都曾给人造成过真实事故。第一,载荷不是加密的;解码它是阅读,不是破解,而签名是唯一保护,所以永远不要把解码后的载荷当作可信输入。第二,像上面那样把 JWT.decode 里的算法钉死。省略它会让令牌自己的头部决定如何验证,而正是这一丝灵活性,被著名的 JWT 算法混淆攻击利用。
Basic 认证:藏在明处的密码
网络上最古老的认证头就是 Base64 本身。HTTP Basic 认证把凭据编码成 user:password,放在 Basic 这个词后面 - 而这个头会随每个请求同行,所以它会出现在你将来调试的每一份日志里。解码它是一个剥离再切分的工作:
require "base64"
header_value = "Basic YWxpY2U6czNjcjN0IQ=="
b64 = header_value.sub("Basic ", "")
decoded = Base64.decode64(b64)
user, password = decoded.split(":", 2)
puts user
# => alice
puts password
# => s3cr3t!
split 里的上限 2 很重要:密码里合法地包含冒号,而你永远只想在第一个冒号处切分。Ruby 自己的标准库在 Net::HTTP 里以相反方向构建这个头,直接使用核心 pack 模板:
require "net/http"
request = Net::HTTP::Get.new("https://example.org/api")
request.basic_auth("alice", "s3cr3t!")
puts request["Authorization"]
# => Basic YWxpY2U6czNjcjN0IQ==
还有那个即使显而易见也必须说出口的安全提醒:Base64 是翻译器,不是锁。Basic 认证只在 HTTPS 之上才可接受。这种编码存在,是为了让凭据能作为可打印文本在电路上行驶,而不是为了保密。
Data URI:不是文件的图片
一个 data URI 把一个完整的文件藏在 URL 里面:一个媒体类型、单词 base64、一个逗号,然后是编码后的字节。浏览器在 img 标签和 CSS 里渲染它们,单文件 HTML 应用喜爱它们,因为不用发第二个请求。在 Ruby 里构建一个只要一行:
require "base64"
png = File.binread("logo.png")
data_uri = "data:image/png;base64,#{Base64.strict_encode64(png)}"
解码它是反方向,有两个让人栽跟头的细节。逗号是分隔符,所以只切分一次;而媒体类型那部分可以是任何东西,包括什么都没有:
data_uri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
media_part, b64 = data_uri.split(",", 2)
puts media_part
# => data:image/png;base64
bytes = Base64.strict_decode64(b64)
File.binwrite("restored.png", bytes)
这里用 strict_decode64,不要用 decode64:data URI 的载荷是一行干净的数据,如果它损坏了,你要的是响亮的错误。还要记住这笔尺寸税 - 你内联的每一张图片都会膨胀大约三分之一 - 所以 data URI 非常适合 favicon、小 logo 和字体,用在英雄大图上则是馊主意。
Email:六十字符的行与 mail gem
Base64 是为邮件发明的,伤疤还看得见。SMTP 是为七位文本的短行设计的,所以 MIME Base64 把输出折成短行,而合规的解码器必须忽略换行。Ruby 的 decode64 正是这个行为,所以被折行的 MIME 正文是易如反掌的食物:
body = "Zm9vCmJh\ncgptYW4=\n"
Base64.decode64(body)
# => "foo\nbar\nman"
你很少需要手写那个。mail gem 替你完成整个 MIME 工作:附件自动进行 Base64 编码,行在 60 个字符处折行,稳稳地待在 MIME 的 76 字符限制之内,正确的头也会附上:
# Gemfile: gem "mail"
require "mail"
message = Mail.new do |m|
m.from = "dev@example.org"
m.to = "ops@example.org"
m.subject = "Binary report"
m.add_file("report.bin")
end
puts message.encoded
# 附件部分带着 Content-Transfer-Encoding: base64 头
同样的技巧也藏在邮件头里。一个非 ASCII 的主题行会以 RFC 2047 编码词的形式到达:一个字符集、字母 B,以及问号之间的 Base64。手工解码它是一场小小的字符串外科手术:
header_value = "=?UTF-8?B?w7wgc2VjcmV0cw==?="
charset, kind, b64 = header_value.sub(/\A=\?/, "").sub(/\?=$/, "").split("?")
text = Base64.decode64(b64).force_encoding(charset)
puts text
# => ü secrets
PEM:穿着护甲的密钥与证书
密钥和证书的一生大部分时间都穿着 PEM 护甲:一行 BEGIN、一块 Base64,再加一行 END。这套护甲来自 20 世纪 80 年代 - Privacy-Enhanced Mail 才是整个 Base64 谱系的起点 - 但今天你的 .crt 和 .key 文件穿的就是它。
手工解码 PEM 文件,就是剥掉护甲,让宽容的解码器把换行嚼过去:
require "base64"
pem = File.read("server.key")
body = pem.lines
.reject { |line| line.start_with?("-----") || line.strip.empty? }
.join
key_bytes = Base64.decode64(body)
实际使用时,你通常会跳过手工步骤,把整个 PEM 字符串交给 OpenSSL,它会自己读护甲:
require "openssl"
key = OpenSSL::PKey.read(File.read("server.key"))
puts key.class
# => OpenSSL::PKey::RSA,或者这个密钥实际是什么
唯一值得知道的互操作细节:PEM 行经典长度是 64 个字符,而解码器无论如何都忽略换行,所以折成 60 个字符一行,或者一整行巨长,都能同样解码。
文件与 .b64 约定
Base64 世界里最常见的文件格式,就是一个带 .b64(有时是 .base64)扩展名的纯文本文件,里面装着一个编码后的载荷。读它是一次三步往返:
require "base64"
encoded = File.read("payload.b64")
bytes = Base64.decode64(encoded)
File.binwrite("payload.bin", bytes)
输出时用 File.binwrite - 解码后的 PNG 或 zip 是二进制的,在会转换行尾的平台上用文本模式写会毁掉它。如果你的 .b64 文件来自一个会折行的工具,decode64 会免费替你处理换行。如果你想验证而不是容忍,就用二进制模式读文件,在严格解码前剥掉换行:
encoded = File.binread("payload.b64")
clean = encoded.delete("\r\n")
bytes = Base64.strict_decode64(clean)
在 Windows 上,二进制读取很重要,因为文本模式会在你看之前就把 CRLF 行尾重写成 LF - 这正是你不想发生在待验证字符串里的那种篡改。
URL 安全 Base64:在链接里旅行的载荷
这是解码器视角下的 URL 安全变体,因为你在这里做出的选择,会改变你会伸手去拿三个解码器中的哪一个。URL 安全 Base64(RFC 4648,第 5 节)换掉了 URL 不喜欢的两个字符 - + 变成 -,/ 变成 _ - 而且通常也丢掉填充。在 Ruby 里,你会在查询参数、cookie 值、API 标识符、YouTube 风格的视频 ID 里遇到它,当然还有 JWT。
下面是三个解码器在同一批输入上的表现,因为差异恰恰是 Bug 的出生地:
| 输入 | decode64 | strict_decode64 | urlsafe_decode64 |
|---|---|---|---|
aGVsbG8=(标准,带填充) |
"hello" |
"hello" |
"hello" |
aGVsbG8(无填充) |
"hello" |
ArgumentError |
"hello" |
SGVsbG8gd29ybGQ-(最后一组里有连字符) |
"Hello world"(少了一个字节!) |
ArgumentError |
12 个字节,正确答案 |
aGVsbG8=\n(末尾换行) |
"hello" |
ArgumentError |
ArgumentError |
aGVs!bG8=(多余的感叹号) |
"hello" |
ArgumentError |
ArgumentError |
第三行才是咬人的那行。一个 URL 安全载荷如果用标准解码器来解,会悄悄丢掉最后一个字节,什么都不抛,因为 decode64 根本无视连字符。如果一个载荷可能来自 URL,就用 urlsafe_decode64 解码它。
一个实用的提醒:如果你需要把一个 URL 安全载荷搬到一个只懂标准字母表的上下文(某个库、某个外部系统),经典互操作技巧 - 自己翻译字母表并补上填充 - 只要三行:
def standardize_urlsafe(b64)
b64 = b64.tr("-_", "+/")
b64 += "=" * ((4 - b64.length % 4) % 4)
b64
end
Base64.strict_decode64(standardize_urlsafe("SGVsbG8gd29ybGQ"))
# => "Hello world"
你很少需要它 - urlsafe_decode64 已经替你补填充了 - 但它是你在别人代码里应当认出的模式,也是当对方期望标准字母表时你应当拿起来的模式。
配置、环境变量与数据库
每当二进制数据必须住进一份文本文档,Base64 就会出现在配置里。.env 文件、YAML 配置,或者一个 JSON 设置数据块,都无法安全地携带原始字节,所以字节被编码,而你应用里的某个东西必须在启动时解码它们:
require "base64"
b64 = ENV.fetch("APP_LOGO")
bytes = Base64.decode64(b64)
File.binwrite("logo.png", bytes)
YAML 值得特别提一句,因为这个格式有原生的二进制标签。当你 dump 一个 BINARY 字符串时,Psych 把它写成 !binary 标量,里面装的是 Base64,而加载时你的字节完好无损地回来 - 完全不用手工编码:
require "yaml"
yaml_text = YAML.dump({ "logo" => File.binread("logo.png") })
puts yaml_text.lines.first(2)
# => "---"
# => "logo: !binary |-"
data = YAML.load(yaml_text)
puts data["logo"].encoding
# => ASCII-8BIT
在数据库里,经验法则是:如果你的数据库有真正的二进制类型,就用它。把 Base64 塞进 TEXT 列,是当存储层只说字符串时你拿起来的模式 - 某些文档型数据库、JSON 形状的 API,或者一个你改不动的遗留 schema - 代价是列上三分之一的尺寸税,外加一份纪律:在每个边界上,进的时候解码,出的时候重新编码。
大输入,稳内存
这个模块是基于缓冲区的:一次解码调用一次性读完整字符串,并返回整个结果。标准库里没有流式解码器,所以面对大载荷的诚实建议是规划内存。好消息是,解码只会让东西变小 - 输出至多是输入的四分之三 - 所以输入字符串是你唯一的大分配。
如果一个 payload 大到让你担心,你可以按四个字符一组来解码,因为 Base64 的每组四个字符是自包含的,而最后一个不完整的组自带它的填充:
require "base64"
def decode_in_chunks(b64)
b64.scan(/.{1,4}/).reduce("") do |result, group|
result + Base64.strict_decode64(group)
end
end
restored = decode_in_chunks(Base64.strict_encode64("a" * 1_000_000))
puts restored.length
# => 1000000
这对干净、未折行的输入有效 - 也就是 strict_decode64 执行的同一套规则 - 因为一个孤零零的末尾组只有在填充在场时才合法。对于真正巨大的文件,比如多 GB 的归档,模式是:把文件分片读取,解码每个分片,把字节流式写到磁盘,让内存里一次只驻留一个分片。
终端一行流
在 shell 里解码东西不需要脚本文件。Ruby 可以随用随 require 这个模块:
ruby -rbase64 -e 'puts Base64.decode64(ARGV[0])' "aGVsbG8gd29ybGQ="
# => hello world
而对于文件,传文件路径而不是 payload 本身:
ruby -rbase64 -e 'print Base64.decode64(File.read(ARGV[0]))' payload.b64 > payload.bin
这里藏着两个坑。第一,如果你通过 echo 或任何文本命令来管道,一个末尾换行会跟着进来,strict_decode64 会因为它抛异常 - 改用 decode64,或者对输入 chomp:
echo "aGVsbG8gd29ybGQ=" | ruby -rbase64 -e 'print Base64.strict_decode64(STDIN.read.chomp)'
第二,二进制输出坚持用 print 而不是 puts,因为 puts 会加上自己的换行,毁掉你恢复文件末尾。
Ruby 开发者真实踩过的坑
- decode64 从不抛异常。垃圾进,垃圾出。如果你的输入不可信,而你默默接受了损坏的字节,这个 bug 会在几周后以一个损坏的文件形式出现,而不是出现在解码那一行。对任何不是你亲手编码的东西,默认用严格解码器。
- strict_decode64 与末尾换行。文本文件、echo 管道和复制粘贴都爱以一个换行收尾,而严格解码器会因此抛
ArgumentError。先chomp输入 - 或者用二进制模式读取并删掉换行。 - 忘了编码步骤。解码后的字符串是 BINARY,直到你另作说明。在把结果当文本之前,先强制 UTF-8(并检查合法性),否则你一旦把它和 UTF-8 字符串混用,立刻就是乱码加
Encoding::CompatibilityError。 - BINARY 和 UTF-8 比较。同样的字节,不同的标签,
==说 false - 除非字符串碰巧是纯 ASCII。比较前先重新贴标签。 - URL 安全输入走了错误的解码器。
decode64会悄悄丢掉连字符和下划线,所以一个 URL 安全载荷会短一个字节地回来,而且是坏的,连错误都没有。用urlsafe_decode64。 - 填充之后的数据是隐形的。
decode64在第一个=处停下。这对 MIME 很好,但对于抓出被截断后又由别的工具重新填充的载荷,糟透了。 - 非规范填充被悄悄接受。像
QR==这样的字符串带着填充比特,一个正经编码器本该把它们置零;decode64乐呵呵地解码它,而strict_decode64拒绝它。永远没有任何东西会告诉你你的编码器在撒谎。 - Windows 上的文本模式文件读取会在你看到之前重写行尾。当你打算验证
.b64文件时,用二进制模式读。
解码侧的好习惯
- 按数据来源选解码器:任何不可信的东西用
strict_decode64(并 rescueArgumentError作为你的无效输入分支),URL 里出生的载荷用urlsafe_decode64,只有真正宽容的格式(比如 MIME 正文)才用decode64。 - 字节解码的这一刻,就决定它的身份:二进制(保留 ASCII-8BIT,用
File.binwrite写)或文本(force_encoding到 UTF-8,然后使用前valid_encoding?)。 - 永远不要解了就信。JWT 载荷之所以可读,恰恰因为它是 Base64;签名才决定它是不是真的。配置文件里的一个 Base64 字符串是数据,不是证明。
- 写校验器时,拿无聊的用例测它:空字符串、无填充输入、折行输入、URL 安全输入,以及错误填充。正是这些用例把三个解码器区分开。
Base64 在 Ruby 里的简史
Base64 模块加入 Ruby 标准库已经超过十五年,而它的发货方式比你想象的变化更大:
- 2008 年,Ruby 1.8.7:模块随附
encode64、decode64,外加两个已不存在的方法 -b64encode(按选定行宽折行)和decode_b(RFC 2047 邮件头解码)。老书甚至一些老 gem 还在引用它们,如今调用其中任何一个都是NoMethodError。 - 2009 年,1.9 线:
strict_encode64、strict_decode64、urlsafe_encode64和urlsafe_decode64登场,两个遗留方法退役(1.9.1 在 2009 年 1 月已经同时带上这两项变化)。 - 2015 年,Ruby 2.3:
urlsafe_encode64获得padding:关键字,让你可以为令牌和 URL 输出不带填充的结果。 - 2020 年,Ruby 3.0:base64 从标准库中抽出,成为自己的 gem,版本 0.1.0,放在
ruby/base64仓库下。它作为默认 gem 发货,所以require "base64"依然开箱即用。 - 2023 年,Ruby 3.3:版本 0.2.0 加入
Base64::VERSION和一套丰富得多的文档。 - 2024 年,Ruby 3.4:gem 从默认 gem 重新归类为捆绑 gem。实际后果:在 Ruby 3.4 及以后基于 Bundler 的项目里,把
gem "base64"写进 Gemfile(或用gem install base64安装)。 - 2025 年,Ruby 4.0:版本 0.3.0 落地,除其他维护外,加入了 RBS 类型签名。
贯穿这一切,有一个事实从未改变:这个模块就是几十行纯 Ruby,坐在核心 pack 和 unpack 模板之上。没有 C 扩展,没有依赖,没有需要构建的东西 - 而在 rubygems.org 上的下载量以亿计。
给好奇者的 Ruby 小趣闻
- 模块的解码半边是两个单行方法体,
str.unpack1("m")和str.unpack1("m0"),加上 urlsafe 变体,后者是在严格版之上做字符替换和填充修正。你可以删掉 require 自己写一个。 - Ruby 自己的
Net::HTTP连 Basic 认证都不用Base64模块 - 它直接调用pack模板:["user:pass"].pack("m0")。 - Rails 的签名 cookie 和加密 cookie 在底层都是 Base64 字符串:ActiveSupport 的消息编解码器对普通 cookie 选
strict_encode64,对 URL 安全的签名 ID 选带padding: false的urlsafe_encode64。你八成早就不知不觉解码过其中一个了。 - 每个摘要类都有一个
base64digest方法 -Digest::SHA256.base64digest("hello")- 为必须活在文本里的校验和准备的一行流。 - YAML 的
!binary标签就是 Base64。用 Psych dump 一个 BINARY 字符串,这个格式会悄悄替你完成编码。 decode64不在乎你的行是 60、64 还是 76 个字符,或者一整行巨长。m模板跳过换行,所以折行与未折行的输入解码结果完全一致。
继续前行
现在你拥有了完整的解码工具箱:一个宽容的读者处理 MIME 形状的团块,一个严格的守门员处理一切不可信的东西,一个 URL 安全的外交官处理令牌和链接,还有那个把结果字节变成 Ruby 允许你使用的文本的编码步骤。反方向 - 决定把字节喂给 Ruby 三个编码器中的哪一个,并控制字母表、填充和换行 - 带着它自己的一套惊喜,从一个没人要的末尾换行开始。街道的那一侧,在下文链接的 Base64 编码文章里被深入覆盖。
最后更新: 2026-09-08
相关文章: Ruby 中的 Base64 编码:完整指南