编码和解码字符串
除非你确实需要 MIME 输出,否则请使用 strict_ 系列方法:
require "base64"
enc = Base64.strict_encode64("héllo wörld")
puts enc # aMOpbGxvIHfDtnJsZA==
dec = Base64.strict_decode64(enc).force_encoding("UTF-8")
puts dec # héllo wörld
Ruby 字符串自带编码信息,而解码后的字符串总是以 BINARY(ASCII-8BIT)返回,因为 Base64 并不知道这些字节代表什么。force_encoding("UTF-8") 会重新标记编码,而不复制数据。如果省略这一步,与 UTF-8 字面量的比较会失败,字符串方法也会把每个字节当作一个字符。
如果源字符串是其他编码(比如来自遗留文件的 Windows-1252),请在编码为 Base64 之前调用 .encode("UTF-8"),否则接收方看到的字节会和你预期的不同。
encode64 与 strict_encode64
encode64 是 MIME 风格的。它每 60 个字符插入一个换行符,并且总以换行符结尾:
Base64.encode64("hi") # => "aGk=\n"
Base64.encode64("x" * 60)
# => "eHh4...eHh4\neHh4eHh4eHh4eHh4eHh4\n" (60 chars per line)
Base64.strict_encode64("hi") # => "aGk="
这个结尾的 \n 是 Ruby 中最常见的 Base64 bug:它会破坏 HTTP 头、签名和 JSON 比较。注意 Ruby 在 60 个字符处换行,而 RFC 2045 允许最多 76 个字符,其他大多数语言也用 76。两者对电子邮件都有效;如果你需要恰好 76 个字符,MIME Base64 编码器 展示了那种格式。
带填充和不带填充的 URL 安全 Base64
Base64.urlsafe_encode64("<<???>>") # => "PDw_Pz8-Pg=="
Base64.urlsafe_encode64("<<???>>", padding: false) # => "PDw_Pz8-Pg"
Base64.urlsafe_decode64("PDw_Pz8-Pg") # => "<<???>>"
urlsafe_decode64 带不带填充的输入都接受;它会补上缺失的 =,然后严格解码。padding: 关键字参数从 Ruby 2.3 开始提供。关于 JWT 和其他令牌,见 Base64 与 Base64URL 的区别,或者试试 Base64URL 解码器。
读取 JWT 的声明(不验证签名):
require "json"
jwt = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0In0.sig"
header, payload, _sig = jwt.split(".")
JSON.parse(Base64.urlsafe_decode64(header)) # => {"alg" => "HS256"}
JSON.parse(Base64.urlsafe_decode64(payload)) # => {"sub" => "1234"}
输出格式 {"alg" => "HS256"} 是 Ruby 3.4 新的 Hash#inspect 风格;旧版本打印的是 {"alg"=>"HS256"}。
编码和解码文件
请以二进制模式读写,否则 Windows 的换行转换和编码转换可能会改变字节:
puts Base64.strict_encode64(File.binread("hello.txt")) # aGVsbG8K
File.binwrite("copy.txt", Base64.strict_decode64("aGVsbG8K"))
分块处理大文件
Base64 模块没有流式 API,但只要每块都是 3 字节的倍数,对分块调用 pack("m0") 就没有问题:
File.open("big.bin", "rb") do |src|
File.open("big.b64", "wb") do |dst|
while (chunk = src.read(3 * 1024 * 64)) # 192 KiB, divisible by 3
dst.write([chunk].pack("m0"))
end
end
end
在一个 300 KB 的测试文件上,结果与 strict_encode64(File.binread(...)) 完全相同。用其他任何块大小,输出中间都会出现 = 填充。
直接使用 pack 和 unpack1
这个模块只是一个便利层。下面这些写法是等价的,而且不需要 require:
["hi"].pack("m0") # => "aGk=" (same as strict_encode64)
["hi"].pack("m") # => "aGk=\n" (same as encode64)
"aGk=".unpack1("m0") # => "hi" (same as strict_decode64)
校验与错误
对于任何不规范或缺少填充的 Base64,strict_decode64 都会抛出 ArgumentError,错误信息为 invalid base64。我们试了六种输入,全都抛出了这个错误:
| 输入 | 问题 |
|---|---|
PDw_Pz8-Pg== | URL 安全字符 |
aGk | 缺少填充 |
aGk=!! | 填充后有无关字符 |
a | 不可能的长度 |
aGl= | 填充位不为零 |
"aGVs\nbG8K" | 换行符 |
def base64?(s)
Base64.strict_decode64(s)
true
rescue ArgumentError
false
end
非 strict 的 decode64 从不报错。它会跳过一切看不懂的内容,结果得到的是乱码而不是错误:
Base64.decode64("aGk=!!") # => "hi"
Base64.decode64("PDw_Pz8-Pg==") # => "<<\x0F\xCF\xC3\xE0" (should be "<<???>>")
只有在处理带换行的 MIME 正文、并且信任数据来源时,才使用 decode64。字符串解不出来时,Base64 校验工具 会指出问题所在,Base64 常见错误 解释了常见原因。
Ruby 3.4 与 Bundler
Ruby 3.4 把 base64 从默认 gem 移到了捆绑 gem。在 Bundler 之外,一切照旧。在 bundle exec 下,如果 Gemfile 中没有它,就会失败:
warning: base64 was loaded from the standard library, but is not part of the
default gems starting from Ruby 3.4.0.
...
cannot load such file -- base64 (LoadError)
在 Gemfile 或 gemspec 中加上 gem "base64",或者把调用替换为 Ruby 核心自带的 pack("m0") / unpack1("m0")。
常见问题
为什么 Base64.encode64 会在字符串里加上 \n?
encode64 生成的是 RFC 2045 格式的输出,每 60 个字符以及末尾都有一个换行符。HTTP 头、JSON 和 URL 请用 strict_encode64,它从不添加换行符。
为什么在 Ruby 3.4 上报 cannot load such file -- base64?
从 Ruby 3.4 开始,base64 是捆绑 gem(bundled gem),而不再是默认 gem。直接运行 ruby 仍然能加载它,但在 Bundler 下必须在 Gemfile 中加上 gem "base64"。也可以不 require,直接用 [data].pack("m0")。
在 Ruby 中如何解码没有填充的 Base64?
Base64.urlsafe_decode64 会自己补上缺失的填充。对于标准字母表,先追加 "=" * (-s.length % 4),再调用 strict_decode64。