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

R 中的 Base64 编码:完整指南

你有一批需要出行的字节,而路上只许走文本。一张必须住进 JSON 字段的 JPEG。一份必须躺在环境变量里的证书。一张必须随自包含 HTML 报告同行的图。Base64 就是这一切的打包胶带:任何字节序列都会变成一串 64 个无害字符组成的字符串,扛得住你能扔给它的任何文本通道。本站首页已经完整覆盖了这个格式,所以这里是简版:三个字节进去,四个字符出来,取自 A 到 Z、a 到 z、0 到 9,外加 +/,当货物除不干净时,末尾还有一点 = 填充。

R 专属的转折:base R 根本不带任何 Base64 编码器。基础包里并没有等着你的 base64_encode(),也没有一行就能搞定的内置函数。你来挑一个包,而生态确实给了你选择,各有各的速度、各有各的折行习惯、各有各的填充观点。读完这篇文章,你会知道每种情况下该伸手拿哪个编码器,以及哪一个会悄悄地干点编码以外的事。

编码器格局

五个包干着编码,它们分成日常主力、加密近亲和小专家。下面是这套阵容,截至 2026 年:

版本(2026) 编码入口 折行习惯 何时选它
base64enc 0.1-6 base64encode() linewidthnewline,全由你定 日常字符串,MIME 折行
openssl 2.4.2 base64_encode() 64 字符一行,LF 换行,尾部换行 PEM 文件,现成的加密栈
b64 0.1.7 encode()encode_file() 从不折行;按需 b64_chunk()b64_wrap() 速度、向量、URL 安全引擎
base64 2.0.2 encode() 默认 64 字符一行,外加尾部换行 文件到文件的杂活,报告图片
base64url 1.4 base64_urlencode() 从不折行、无填充,进字符串 URL 安全字符串

还有三个编码器藏在你可能已经加载的包里。jsonlite 导出 base64_enc()base64url_enc(),所以如果你已经在解析 JSON,手里可能已经有一个编码器了。jose 为 JWT 工作导出 base64url_encode()。而老将 RCurl 包还背着一个封装 libcurl 的 base64(),工作正常,属于上一个时代。最后,base64 包如今在罐头上自述为兼容封装,把新应用引向 base64encopenssljsonlite

环境准备

如果机器上还没有 R,你的操作系统会自带:Debian 和 Ubuntu 上是 r-base,Fedora 上是 R,macOS 上是 Homebrew 或官方安装器,Windows 上是安装器。然后是这些编码器,直接从 CRAN 装:

install.packages("base64enc")
install.packages("openssl")
install.packages("b64")
install.packages("base64url")

安装翻车的两个地方都在编译期。openssl 会针对你系统的 OpenSSL 编译,所以一台光板 Linux 机器要先装开发头文件(sudo apt install libssl-dev),或者在 Debian 和 Ubuntu 上用 sudo apt install r-cran-openssl 干脆跳过编译。b64 是一个用 extendr 封装的 Rust 引擎,所以源码构建需要 Rust 工具链(sudo apt install cargo 会连 rustc 一起拉进来)。Windows 和 macOS 从 CRAN 拿到的是预编译二进制,以上这些都不适用。

第一次编码

编码生涯的百分之九十,装得进三行,用的正是解码端当冒烟测试的那个著名字符串:

library(base64enc)
packed <- base64encode(charToRaw("Man"))
packed
#> [1] "TWFu"
identical(packed, "TWFu")
#> [1] TRUE

那场仪式里有三件事要留意。第一,输入是一个原始向量charToRaw() 是从你的 R 字符串到被打包字节的桥,表里的日常编码器 - base64enc、openssl、b64 - 收的都是 raw。第二,输出的方向和解码器正好相反:单个字符字符串,因为编码结束在边界另一侧的文本端。第三,看长度数学现场演示:三个字节进去,四个字符出来,货物正好除得尽,不需要填充。当货物除不尽时,一个或两个 = 字符落在末尾。

而因为一个你不信任的编码器比没有更糟,这是证明两个方向互相吻合的往返:

text <- "Hello, world!"
packed <- base64encode(charToRaw(text))
packed
#> [1] "SGVsbG8sIHdvcmxkIQ=="
identical(text, rawToChar(base64decode(packed)))
#> [1] TRUE

字符串、字节和文件名陷阱

现在说陷阱,因为每个 R 开发者都掉进去过一回。base64encode()字符型参数当作文件名,而不是要编码的文本。传给它一个字符串,它就去找那个文件:

base64encode("Man")
#> Warning in file(what, "rb") :
#>   cannot open file 'Man': No such file or directory
#> Error: cannot open the connection
base64encode(charToRaw("Man"))
#> [1] "TWFu"

警告就是破绽:它试着执行了 file("Man", "rb"),意思是"打开一个叫 Man 的文件用于原始读取"。所以对 base64encode() 来说,纪律就是一个条件反射:charToRaw() 先行,永远。如果你确实指的就是文件,那正是这个函数在做的事,输出就是文件字节打包后的结果,而那有时恰恰是你想要的。

b64 对同一个问题持不同立场:它的 encode() 直接接受字符向量,把每个元素当 UTF-8 文本处理,而且还向量化:

b64::encode("Man")
#> [1] "TWFu"
b64::encode(c("Man", "M"))
#> [1] "TWFu" "TQ=="

这条边界还有两个边缘值得知道。空输入会被用三种不同的方式编码,取决于你问谁:

base64encode(raw(0))
#> character(0)
b64::encode("")
#> [1] ""
base64url::base64_urlencode("")
#> [1] ""

base64enc 回给你一个零长度的字符向量,而不是空字符串,所以那些期望拿到字符串、结果收到 character(0) 的下游代码会在意想不到的地方失败。字符集的决策也住在编码端:charToRaw() 按字符串当前穿着的编码打包它,所以 UTF-8 文本以 UTF-8 字节出行,这正是线路另一端所期望的。表情符号也不例外,因为现代 R 把 U+FFFF 以上的码位存成真正的 UTF-8:

emoji <- "\U0001F600"
nchar(emoji, type = "bytes")
#> [1] 4
round_trip <- base64decode(base64encode(charToRaw(emoji)))
identical(emoji, rawToChar(round_trip))
#> [1] TRUE

折行:MIME、PEM 和你自己的宽度

长的 Base64 字符串会被拆成行,因为世界上最古老的文本通道有列宽限制,而 MIME 一直没顾得上忘掉它们。你会遇到的两种历史折行是MIME,每 76 字符断行,行间 CRLF,和PEM,每 64 字符断行。每个编码器对该用哪一种(或者不用)都有自己的想法,所以这一节就是让你在编码之前先选好合同。

先做尺寸数学,因为折行的核心是知道输出会多长。每三个字节出来四个字符,编码形式的长度就是一个简单的上取整:

nchar(base64encode(charToRaw(strrep("a", 100))))
#> [1] 136
4 * ceiling(100 / 3)
#> [1] 136

把这笔数学揣进口袋,来看编码器。base64enc 最灵活:默认输出一整行不断行,linewidth 参数则还你一行行的向量:

long <- charToRaw(strrep("R", 100))
base64encode(long, linewidth = 76)
#> [1] "UlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJS"
#> [2] "UlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUg=="
base64encode(long, linewidth = 76, newline = "\r\n")
#> [1] "UlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJS\r\nUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUg=="

100 字节在宽度 76 下是两行,默认是向量,加上 newline 就是一根 CRLF 连接的字符串。没有尾部的空元素:114 字节,恰好编码成两行 76,回来就是两行,不是三行。

openssl 站在另一个极点。linebreaks = TRUE 按 64 字符折行,用普通 LF 换行,并且在最末尾再加一个换行:

wrapped <- openssl::base64_encode(charToRaw(strrep("a", 100)), linebreaks = TRUE)
nchar(wrapped)
#> [1] 139          # 136 个数据字符外加 3 个换行
nchar(strsplit(wrapped, "\n", fixed = TRUE)[[1]])
#> [1] 64 64 8

把算术数一遍:136 个数据字符,两个内部断行,一个尾部断行,合计 139。而这个尾部断行对你能写出的最自然的行数统计是隐形的,因为 strsplit() 会丢掉尾部的空片段,于是向量说三行,字符串却带着四行。如果你哪天拿 OpenSSL 的折行输出和 MIME 的折行输出做 diff,发现字符数对不上,这个幽灵就是元凶。

b64 完全不折行;它把两个操作分开给你,块宽有一条规则,必须是 4 的倍数,因为引擎拒绝把一个 Base64 分组拦腰切断:

enc <- b64::encode(strrep("a", 100))
ch <- b64::b64_chunk(enc, 76)
b64::b64_wrap(ch, "\r\n")
#> [1] "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh\r\nYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ=="
b64::b64_chunk(enc, 75)
#> Error: Chunk size must be a multiple of 4.

而面向文件的 base64 包跟着 OpenSSL 走:64 字符一行,一个尾部换行,默认开启:

writeBin(charToRaw(strrep("b", 200)), "big.bin")
base64::encode("big.bin", "big.b64")
con <- file("big.b64")
lines <- readLines(con)
close(con)
nchar(lines)
#> [1] 64 64 64 64 12  0

最后那个零就是尾部换行,被 readLines() 抓成一个空的最后一行。这里把整个场地一次看全:

编码器 宽度 换行 尾部换行
base64encode(x) 一行
base64encode(x, linewidth = 76, newline = "\r\n") 76 CRLF
openssl::base64_encode(x, linebreaks = TRUE) 64 LF
b64::encode(x) 一行 无(用 b64_chunk()b64_wrap() 折行)
base64::encode(in, out) 64 LF
base64url::base64_urlencode(x) 一行

URL 安全 Base64

标准 Base64 把字母表的最后两个位置给了 +/,而这两个字符正是 URL 不喜欢的:加号变成 %2B,斜杠变成 %2F,填充里的每个 = 变成 %3D。URL 安全变体(RFC 4648 第 5 节定义)把这两个字母换成 -_,通常还会丢掉填充,所以本该随处粘贴的令牌保持了随处可粘贴。R 有三扇门通向它。

b64 的引擎最全:所谓引擎,就是一套配置好的字母表加填充策略,这个包自带的四个正是你需要的。把同样的三个字节分别喂给标准引擎和 URL 安全引擎,看字母表干活:

bytes <- as.raw(c(0xfb, 0xef, 0xbe))
b64::encode(bytes)
#> [1] "++++"
b64::encode(bytes, engine("url_safe"))
#> [1] "----"
b64::encode(as.raw(0x4d), engine("url_safe"))
#> [1] "TQ=="
b64::encode(as.raw(0x4d), engine("url_safe_no_pad"))
#> [1] "TQ"

四个引擎,"standard""standard_no_pad""url_safe""url_safe_no_pad",同一个引擎对象两个方向都能用,这让你的代码保持对称。无填充变体只在填充真的会出现时才有区别,就像上面那个一个字节的例子。

专门的 base64url 包是单一用途的门:进字符串,出 URL 安全字符串,从不折行,从不填充:

base64url::base64_urlencode("hello world")
#> [1] "aGVsbG8gd29ybGQ"

jose 导出它自己的 base64url_encode() 用于 JWT 工作,解码端按你期望的那样返回原始向量。一条规则管住三扇门:你编码用的字母表,就是解码时必须用的字母表。把字符串 "----" 交给标准解码器,它会在第一个连字符上失败;解码的姊妹篇覆盖了每个解码器如何面对脏输入或不匹配的输入。

JWT:制作令牌

JSON Web Token 是 URL 安全 Base64 变成日常工具的地方。JWT 是三段用点号连起来的 base64url:一段描述签名方式的头、一段 JSON 声明(claims)的负载,还有一段把两者绑在一起的签名。在编码端,你要构建全部三段,而 jose 包把整套仪式都做了。它的 API 围绕 jwt_* 函数构建(jwt_claim()jwt_encode_hmac()jwt_decode_hmac()jwt_split());2.0 版本(2026 年 4 月)新增了 ED25519 支持,并让 typ 头字段变为可选:

library(jose)
claim <- jwt_claim(iss = "app", sub = "1234567890", exp = Sys.time() + 3600)
token <- jwt_encode_hmac(claim, "0123456789abcdef")
sp <- jwt_split(token)
sp$header
#> $typ
#> [1] "JWT"
#>
#> $alg
#> [1] "HS256"
names(sp$payload)
#> [1] "iss" "sub" "exp" "iat"

注意 jwt_claim() 在幕后干了什么:iat(签发时间)默认是当前时间,所以它不请自来地出现在负载里;而 exp 默认什么都没有,这意味着一个没有过期时间的令牌,这通常不是你想要的。有意识地设置 exp,剩下的交给签名。jwt_split() 是检查工具:头、作为命名列表的负载、原始签名,不涉及任何验证,正是这篇文章对里解码端描述的那个偷看步骤。

验证端才是 jose 挣回工钱的地方。jwt_decode_hmac() 检查签名并强制执行时间声明,它的拒绝方式很具体:

round <- jwt_decode_hmac(token, "0123456789abcdef")
round$sub
#> [1] "1234567890"
jwt_decode_hmac(token, "ffffffffffffffff")
#> Error: HMAC signature verification failed!
old <- jwt_claim(sub = "1234567890", exp = Sys.time() - 3600)
expired <- jwt_encode_hmac(old, "0123456789abcdef")
jwt_decode_hmac(expired, "0123456789abcdef")
#> Error: Token has expired on 2026-08-30 05:09:36

成功时,声明以普通列表的形式回来,所以 round$sub 之类的东西直接可用。一个未来的 nbf(早于)声明会换来它专属的拒绝,Token is not valid before ...,而 HMAC 令牌不会被非对称的 jwt_decode_sig() 解码,它回答 Unsupported algorithm: HMAC,要的是公钥。最后两句提醒。第一,负载是编码过的,不是加密的:任何人都能读到每条声明,所以秘密不该住进去。第二,如果你在 jose 之后加载 httr2,注意遮蔽警告:httr2 导出它自己的 jwt_claim()jwt_encode_hmac()jwt_encode_sig(),为 OAuth 客户端凭据而造,exp 默认五分钟后,它们会在这个会话的剩余时间里遮蔽 jose 的版本。

文件:二进制进,文本出

编码文件是解码端文件工作的镜像,而 b64 包的入口最清楚,同时也是最快的,因为它从不构建一个巨大的中间字符串:

writeBin(charToRaw("file payload bytes"), "payload.bin")
enc <- b64::encode_file("payload.bin")
enc
#> [1] "ZmlsZSBwYXlsb2FkIGJ5dGVz"
cat(enc, file = "payload.b64")
readLines("payload.b64")
#> Warning in readLines("payload.b64") :
#>   incomplete final line found on 'payload.b64'
#> [1] "ZmlsZSBwYXlsb2FkIGJ5dGVz"

警告是伪装的功能:cat() 不加尾部换行,所以文件在半行处结束,readLines() 会告诉你这件事。当这个文件将由 b64::decode_file() 消费时,记住你站在这个事实的哪一边,因为它对尾部换行会 panic;解码文章有完整故事。用 cat()writeBin(charToRaw(enc), path) 写,这条边缘就保持钝的。

base64 包是纯粹的文件到文件选项,一对相匹配的函数,默认 OpenSSL 式折行:

base64::encode("payload.bin", "payload2.b64")
readLines("payload2.b64")
#> [1] "ZmlsZSBwYXlsb2FkIGJ5dGVz" ""

它返回输出路径,对记录日志很方便。而当你想看看机器内部,手动流水线对表里任何编码器都管用:把文件读成 raw、编码、写出文本,完事:

bytes <- readBin("payload.bin", what = "raw", n = file.size("payload.bin"))
enc <- base64encode(bytes)
writeLines(enc, "payload3.b64")
identical(enc, readLines("payload3.b64"))
#> [1] TRUE

Data URI 与自包含文档

data: URI 方案(RFC 2397)是编码端最显眼的消费者:一个携带自己内容的文档,一个 MIME 类型、"base64" 这个词和负载,全塞进一个属性。PNG 的魔法字节让这个模式一望可知:野外的每个 Base64 PNG 都以同样的字符开头,因为文件头 89 50 4E 47 永远编码成同一个样子:

png_head <- as.raw(c(0x89, 0x50, 0x4e, 0x47))
uri <- paste0("data:image/png;base64,", base64encode(png_head))
uri
#> [1] "data:image/png;base64,iVBORw=="
tag <- paste0("<img src=\"", uri, "\" />")

如果你曾经 grep 一个 HTML 文件里的 iVBOR 来找嵌入图片,指纹有效的道理就在这里。R Markdown 报告、单文件仪表盘和爬取的页面都用同样的形状,构建一个也是同样的套路:把文件读成 raw、编码,粘在 MIME 前缀后面。代价是前面那笔尺寸数学,比原文大三分之一,永远坐在你的 HTML 里,所以让嵌入的图片保持精瘦。

API 与 Web 请求

这篇文章对的解码端遇到的是递给你 Base64 的 API;这一端遇到的是索要 Base64 的 API。套路每次都一样:编码字节、把字符串放进 JSON 主体、发出去。现代的 HTTP 客户端是 httr2

library(httr2)
req <- request("https://httpbin.org/post")
req <- req_body_json(req, list(file = packed, name = "man.txt"))
req
#> <httr2_request>
#> POST https://httpbin.org/post
#> Body: JSON data
res <- req_perform(req)
res$status_code
#> [1] 200

三句上路前的提醒。请求构造函数叫 request(),从 httr2 第一次发布起就是这个名字。当你读取响应时,响应体以原始向量形式到达,所以解析前先 rawToChar()。API 可能说的是方言:有些要 URL 安全字母表,有些要剥掉填充,而 JSON API 对字符串值里的 = 毫无怨言,所以填充问题只有当 Base64 走在路径或查询参数里时才变成 URL 问题。拿不准时,读 API 的示例,而不是读你脑子里的规范。

数据库、配置与环境

数据库里的 Base64 是夹带通过文本列的二进制,而编码端就是把数据块打包好再放进去。这是通过 DBIRSQLite 对 SQLite 的往返:

library(DBI)
library(RSQLite)
db <- dbConnect(SQLite(), ":memory:")
dbExecute(db, "CREATE TABLE files (name TEXT, payload TEXT)")
stored <- base64encode(charToRaw("stored in a database"))
dbExecute(db, paste0("INSERT INTO files VALUES ('note.txt', '", stored, "')"))
row <- dbGetQuery(db, "SELECT * FROM files")
rawToChar(base64decode(row$payload))
#> [1] "stored in a database"

替代方案是把字节原生地存成 BLOB,那就完全不需要 Base64,该列回到 R 时是一个原始向量。把 Base64 装进 TEXT 的变体存在的理由是可移植性:你能用文本编辑器查看它、对它做 diff,每种语言都不需要二进制驱动就能读它。同样的理由也把它带进配置,那里证书或秘密以字符串的形式存在 YAML、JSON 或环境变量里:

Sys.setenv("API_CERT" = base64encode(charToRaw("LTSSECRET")))
Sys.getenv("API_CERT")
#> [1] "TFRTU0VDUkVU"

一句提醒该住在这里,因为配置文件是秘密的住所:Base64 是编码,不是加密。配置文件里的 Base64 值,任何能读这个文件的人都读得懂。它能扛住传输和文本编辑器;它不保护任何东西。

邮件

邮件是 76 字符折行的出生地,MIME 附件至今还穿着它:Base64 内容,每 76 字符断行、行间 CRLF,装在一个声明 Content-Transfer-Encoding: base64 的部分里。恰好产出那个形状的编码器,是把折行参数设成 MIME 合同的 base64encode()

body <- "The quick brown fox jumps over the lazy dog, and then it came back down again."
mime_part <- base64encode(charToRaw(body), linewidth = 76, newline = "\r\n")
strsplit(mime_part, "\r\n", fixed = TRUE)[[1]]
#> [1] "VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZywgYW5kIHRoZW4gaXQg"
#> [2] "Y2FtZSBiYWNrIGRvd24gYWdhaW4u"

R 没有一等公民级的邮件客户端,但无论你手动构建或检查 MIME 部分、生成 .eml 夹具,还是从中解析附件,这个道理都成立:这就是 Base64 必须有的形状,而这篇文章对的解码端展示了宽松解码器在回程中拆开它。

大负载与字符串天花板

R 字符串有 2^31 - 1 字节的硬天花板,而因为编码形式比原文大约大三分之一,一个约 1.5 GB 原始数据的文件会把它的一行编码顶过墙去。实用的做法和 base64enc 自 2022 年长向量版本起提供的一样:让输出保持一行行的样子,而不是一根字符串:

big <- raw(10 * 1024 * 1024)
packed <- base64encode(big, linewidth = 76)
length(packed)
#> [1] 183961
sum(nchar(packed))
#> [1] 13981016

十 MB 的零变成最多 76 字符的 183961 行,一个完全普通的向量,你可以逐行写出,或流经管道,而不用抱着一根巨大的字符串。对数据框的情形,一整列二进制值,b64 是速度冠军:它的 Rust 引擎一次向量化调用就编码整列,这和逐行循环差距悬殊。如果你要产出一整列编码值,自己跑一个快速的 system.time() 对比;逐行循环和一次向量化调用之间的差距,通常大到足以影响性能。

命令行

不是什么都需要完整的 R 会话。经典 Unix 工具天生会说 Base64,编码在所有平台上都不带标志,解码在 Linux 上用 -d,在 macOS 和 BSD 上用 -D

echo -n "Hello, world!" | base64
#> SGVsbG8sIHdvcmxkIQ==
echo -n "SGVsbG8sIHdvcmxkIQ==" | base64 -d
#> Hello, world!

注意第一行的 -n:没有它,echo 会贡献一个换行,输出编码的就是 14 个字节而不是 13 个,以 o= 结尾而不是 IQ==。GNU 的 base64 也会替你折行(-w 76),当管道通向某个期望 MIME 形状输入的环节时,这很顺手。一行 Rscript 也能用你脚本里同样的包干同样的活:

Rscript -e 'library(base64enc); writeLines(base64encode(charToRaw("Man")))'
#> TWFu

快速检查和管道交给 shell;结果需要住进数据框、文件或报告时,用 R。还有,别把几 MB 的字符串粘进终端:命令行参数会先撞上 ARG_MAX,远早于 Base64 撞上它的天花板,所以改用文件管道传过去。

值得知道的坑

这是份简短清单,列出编码端咬 R 开发者的方式,全部是这个生态的原生问题,而不是 Base64 本身的问题:

  • 字符串就是文件名。base64encode("Man") 会试图打开一个叫 Man 的文件。警告点名了文件;错误说连接失败。用 base64encode() 时先 charToRaw()
  • 空输入有三种编码方式。base64encode(raw(0)) 返回 character(0),而 b64::encode("")base64url::base64_urlencode("") 返回 ""。下游的字符串代码不期望一个零长度向量。
  • openssl 折行后还加一行幽灵。linebreaks = TRUE 按 64 用 LF 断行,再追加一个 strsplit() 会悄悄丢掉的尾部换行,于是天真的行数统计和字符数统计相差一行。
  • 折行是合同。MIME 是 76 加 CRLF,PEM 是 64,JSON API 通常什么都不要。选另一端期望的形状,因为容忍一种折行风格的解码器会拒绝另一种。
  • URL 安全字母表两端必须一致。URL 安全编码的 "----" 在标准解码器里会在第一个连字符上失败,而字符串一旦住进 URL,填充就变成了 %3D
  • b64_chunk 只收 4 的倍数。任何其他宽度都会换来 Chunk size must be a multiple of 4.,因为 Base64 分组不能拦腰切断。
  • 尾部换行能让解码器 panic。你为 b64::decode_file() 写的文件必须以无换行结尾;用 cat(),别用 writeLines()
  • JWT 时间声明会被强制执行。jwt_decode_hmac() 拒绝过期令牌和未来的 nbf 声明,而如果你第二个加载 httr2,它会遮蔽 josejwt_* 函数。
  • 负载是可见的。JWT、data URI 或配置文件里的 Base64 声明,任何人都读得懂。编码不是加密。
  • 字符串天花板是墙,不是建议。每个 R 字符串约 1.5 GB 原始数据处,一行编码就不再装得下;大输出保持一行行的样子,或者流式处理。

最佳实践

  • base64enc::base64encode() 时,编码前先用 charToRaw() 转换。如果你需要直接的字符输入和向量化,b64::encode() 就是那个把字符串当字符串的包。
  • 日常工作默认用 base64enc::base64encode(),想要速度、真正的向量化或 URL 安全引擎时伸手拿 b64openssl 已经在项目里且你想要 PEM 形状的输出时用它。
  • 按通道选折行,而不是按包:JSON 和 URL 什么都不用,MIME 用 76 加 CRLF,PEM 式块用 64,并保持一致,让管道的解码端知道该期待什么。
  • 凡要住进 URL、JWT 或文件名的东西,用无填充的 URL 安全字母表;邮件用标准字母表。
  • 对 JWT,显式设置 exp(和 iat),信任令牌前用 jwt_decode_hmac() 验证,并记住每条声明都是公开文本。
  • 用往返测试你的编码路径,identical(text, rawToChar(base64decode(base64encode(charToRaw(text)))));它就一行,一次抓住字母表、填充和字符集三类错误。
  • 对文件,宁可 b64::encode_file()base64::encode(),也别把整个文件读进一根字符串;当严格解码器要读你的输出时,用 cat() 写。
  • 让打包胶带保持诚实:Base64 让字节出行,它不使字节保密,也不使字节变小。保密是目标就先加密,尺寸是目标就先压缩。

Base64 如何走进 R

这个格式到来时,R 离它的应用还很远。1987 年它为隐私增强邮件(Privacy-Enhanced Mail)协议标准化(RFC 989),1993 年被 MIME 采用(RFC 1521,之后是 1996 年定稿的 RFC 2045,76 字符折行至今由它定义),2003 年在 RFC 3548 里被整理过,2006 年 RFC 4648 赋予它现代形态,新增了 URL 安全字母表、无填充选项,还有它更小的兄弟 Base32。R 的故事始于 2012 年 9 月,Simon Urbanek 的 base64enc 登上 CRAN,并悄悄成为"这个怎么 Base64"的默认答案,一当就是十多年,2015 年新增 checkUTF8(),2022 年支持长向量。加密世界从 openssl 进来,那是 Jeroen Ooms 长期维护的系统 OpenSSL 封装,它的 base64_encode() 从此一直是 PEM 形状的选项。老的 base64 包,也是 Ooms 的作品,2024 年 10 月被明确地以兼容封装的名义重新发行,它自己的描述如今把新应用引向别处。然后 b64 在 2024 年 1 月到来,一个用 extendr 构建的 Rust 引擎,带来了向量化和一整队字母表;2026 年 4 月 jose 发布 2.0 版本,给 jwt_* API 加上 ED25519 支持,让签名令牌成为一等公民。结果是一个工具箱,每个任务对应一个编码器:日常字符串、PEM 块、速度、URL、文件和令牌。

趣味角

因为一本完整的指南应该以微笑收尾:

  • 互联网上每个 Base64 PNG 都以同样的字符开头:魔法头 89 50 4E 47 编码成 iVBOR,所以 grep 一个 HTML 文件里的那个指纹,就能找到每张嵌入图片。格式有指纹,而它的是前缀。
  • base64encode("Man") 不会编码单词 Man。它去找一个叫 Man 的文件,警告打不开,然后放弃。Base64 生态里最 R 专属的一个陷阱,就藏在参数列表里,明晃晃的。
  • OpenSSL 折行的字符串总是以尾部换行结尾,所以 PEM 块的最后一行从来不是文件的最后一行。这句折行的话,句尾自带句号,由不得你。
  • 空字符串有三种不同的编码:base64enc 返回零个字符串的向量,b64base64url 返回空字符串。R 遇见了无,R 给出三个答案。
  • jose 写 JWT 头时 typalg 前面,而多数手写示例把 alg 放前面。JSON 不在乎键的顺序,JWT 验证也知道,但你的字符串 diff 不知道。
  • MIME 的 76 字符限制是 1993 年关于消息行长的决定,穿过四份 RFC,住进你发过的每一封邮件附件。你今天设置的这个折行,早在 R 还有彩色图之前就被争论过了。
  • b64 会解码你从未见过的字母表,BinHex、IMAP 修改版 UTF-7、bcrypt、crypt 和 URL 安全那一双,各配一个引擎。同样那段打包你 JSON 字段的 Rust 代码,也能拆开一份 1980 年代的 Macintosh 附件。

总结

按任务挑编码器:日常字符串用 base64enc,通道有形状时配上 linewidthnewline;想要 PEM 式 64 字符输出,或 openssl 已在项目里,就用它;想要速度、向量或 URL 安全引擎时用 b64;文件杂活和 URL 安全字符串交给小专家 base64base64url。用 base64enc::base64encode() 编码前先 charToRaw() 转换,按通道而不是按包选折行,凡要住进 URL 或 JWT 的都用 URL 安全字母表,用 jose 给令牌签名和验证,用往返测试每一条路径。格式本身只在两个地方不留情面,字母表和换行,而编码器之间的区别,主要在于它们对你搞错其中一项时有多诚实。而当另一个方向响起 - 当一个字符串到达,你需要把它拆开、弄清字节意味着什么、并且熬过那些沉默失败的解码器 - 姊妹篇会详细讲解 R 中的 Base64 解码。

最后更新: 2026-09-08

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