上传文件
  1. base64hub
  2. 在代码中使用 Base64
  3. Ruby
代码

Ruby 中的 Base64

Ruby 的 Base64 模块只是 Array#pack 和 String#unpack1 的一层薄封装。主要的陷阱是非 strict 方法会插入换行符。下面的输出均来自 Ruby 3.4。

编码和解码字符串

除非你确实需要 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。

继续阅读