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

PowerShell 中的 Base64 编码:完整指南

你手里有一个字符串、一个文件、一张证书或一个令牌,而线的另一头想要的是长长一串字母加数字:可打印,能粘进邮件、URL 或配置文件,没有一个二进制字节会弄坏传输。这就是 Base64。它是一次翻译,既不是压缩,也不是锁:三个输入字节变成四个输出字符,所以你发出去的文本比它的起点大约 33%,用的是一套 64 个字符的字母表,再加等号作尾部填充。

本站首页已经详细讲过字母表、比特运算和各种变体。这篇文章讲的是编码方向,从 PowerShell 这一侧讲起:你要调用的那一个 .NET 方法、那个让每个写第一份脚本的人都栽进去的缺失步骤、因协议而异的折行约定、URL 安全的字母表,以及那几项在 PowerShell 里编码会奖励细心、惩罚大意的真实工作。

这个方法和那个缺失的步骤

PowerShell 并不自带任何 Base64 cmdlet。干活的是一个方法,它从 2003 年的 .NET Framework 1.1 起就是 .NET 框架的一部分,比 PowerShell 自己发布还早三年:

$bytes = [System.Text.Encoding]::UTF8.GetBytes("Hello")
[System.Convert]::ToBase64String($bytes)
# SGVsbG8=

这就是全部的 API:一个字节数组进去,一个字符串出来,在每个操作系统上的每一种 PowerShell 里都一样,因为它就是 .NET。那个缺失的步骤就是例子里的第一行,初学者的第一个小时都丢在这里。这个方法不接受你的字符串。它接受字节,而"我的字符串意味着哪些字节"是一个只有你能回答的编码问题。下面是你在 PowerShell 里真正能调用的那些重载的契约:

你传什么 你得到什么
byte[] 一整行标准 Base64,长度需要时带 = 填充
byte[]InsertLineBreaks 同样的数据,每 76 个字符折行,行间 CRLF
byte[]、偏移、长度 只编码你请求的那一片数组
一个字符串,比如 "Hello" 一个转换异常。PowerShell 自己无法把字符串变成字节数组
$null 一个 ArgumentNullException,外面给你包了一层 MethodInvocationException

注意那张表里缺了什么:没有任何重载写着"把这段文本编码了"。在 PowerShell 里编码文本永远是两步。你先定编码,再产出字节,然后 Base64 方法才进场。在脚本里把这两个决定看得清清楚楚、分得明明白白,因为第二步是看不见的,而 bug 住在第一步里。

编码文本:先定编码

对任何要穿越现代互联网的东西来说,安全的默认值是 UTF-8。Web API、JSON、JWT、浏览器或服务器在过去十年里写下的任何东西,都期望 Base64 底下是 UTF-8 字节,而这个两步模式就是你要养成的习惯:

$text = "Hello, PowerShell!"
$bytes = [System.Text.Encoding]::UTF8.GetBytes($text)
$encoded = [System.Convert]::ToBase64String($bytes)
# SGVsbG8sIFBvd2VyU2hlbGwh

当你伸手去拿另一种编码时,你多半是在伺候一个遗留系统,下面的表是实用指南:

编码 什么时候用它 选错会怎样
UTF8 Web API、JSON、JWT、一切现代的东西。默认之选 另一头的解码器看到的是一堆乱码,而不是你的文本
Unicode(UTF-16LE) 消费方是编码 .NET 字符串的 Windows 或 .NET 组件,或者是 -EncodedCommand 你的负载比消费方预期的长一倍,而且处处是惊喜
ASCII 经典的 7 位协议,比如 HTTP Basic 凭据 数值超过 127 的字符在编码发生之前就被替换掉了
Latin1 早于 UTF-8 的欧洲遗留系统 每字符一个字节,而每个非 Latin-1 字符都变成问号

一个调试技巧双向都好用:Base64 的填充和长度告诉你编码了多少字节,解码后文本的样子告诉你它来自哪个两字节世界或单字节世界。一个大小出奇地规整、正常字符和看起来空白的字符交替出现的负载,通常是穿着 UTF-8 戏服的 UTF-16,或者反过来。

UTF-16 的惊喜

PowerShell 内部用 UTF-16 存储字符串,这个事实在一个具体的、非常常见的地方泄漏进 Base64 工作:你在为一个本身就是 .NET 或 Windows 组件的消费方写 Base64,于是用 Unicode 编码,因为 .NET 字符串就是 UTF-16。对某些消费方这是正确的直觉,对其余一切则是让体积翻倍的错误。同样四个可见字符,两种编码:

$same = "Café"
[System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($same))
# Q2Fmw6k=  5 个字节
[System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($same))
# QwBhAGYA6QA=  8 个字节

同样的文本,两倍的大小,而且这两个字符串不能互换:一个期望其中一种、收到另一种的消费方不会大声失败;它只会读出一堆垃圾。让你免于被这件事咬到的规则是:把编码当作协议的一部分,而不是一个本地细节。如果接收方是浏览器、REST API 或现代服务器,除非文档另说,它就是 UTF-8。如果它是通过 -EncodedCommand 的 PowerShell 宿主本身,或者一条只用 Windows 的流水线里的 .NET 字符串,它就是 UTF-16LE。协议没说的情况下,去问另一头它将用什么去调 GetString,因为那才是真正起决定作用的问题。

数字、字节和其他一切

这个方法声明要接收字节数组,但 PowerShell 的类型转换对什么算字节数组相当宽容,知道它的边缘能让你免于惊讶:

[System.Convert]::ToBase64String([byte[]](1, 2, 3, 250, 251))
# AQID+vs=
[System.Convert]::ToBase64String([char[]]"Café")
# Q2Fm6Q==  每字符一个字节,取字符的数值
[System.Convert]::ToBase64String([int[]](72, 101, 108, 108, 111))
# SGVsbG8=
[System.Convert]::ToBase64String(123)
# ew==  期望整个数组的地方,单个数字也被接受

那一块里有两个边缘值得注意。字符数组按每字符一个字节转换,取的是字符的数值;对拉丁文本,这正是使用这招的遗留系统所期望的,而对拉丁文本之外的任何东西,它会悄悄产出错误的字节。大于 255 的整数则是那个会大声失败的边缘:PowerShell 的字节转换用异常拒绝 0255 之外的值,所以 256 会在转换处停住脚本,而不是悄悄损坏你的数据。如果你的来源是数字,就显式地转换:[byte[]](1, 2, 3) 说的就是它想说的。

把一个字符串传给这个方法,你会因为另一个原因得到同样响亮的处理:没有办法知道一个字符串意味着哪些字节,所以 PowerShell 的转换引擎放弃了。传给它 $null,.NET 在动手之前就抛异常。两者都是正确的行为,也都是第一节那个两步模式成为唯一值得拥有的模式的原因。

折行:76、64 和没有

默认情况下,编码器产出一整行长,不管你给它多少数据。对几兆字节的文件来说没问题。但对人要读、要粘进邮件、要在源码控制 diff 里比较的数据,一堵八百万字符的墙就是实际问题,而惯例是折行。PowerShell 和它服务的协议知道三种宽度,它们不能互换:

宽度 谁期望它 行尾
76 个字符 MIME、邮件和大多数文本传输。InsertLineBreaks 的默认值 CRLF
64 个字符 PEM 文件:证书、私钥和 -----BEGIN 家族的其余一切 惯例上是 LF
不折行 API、令牌、配置文件,以及一切由机器处理负载的地方 根本没有行

内置折行只差一个参数,而邮件风格的负载想要的就是它:

$text = "The quick brown fox jumps over the lazy dog. Base64 output arrives wrapped at different widths depending on who is reading it."
$wrapped = [System.Convert]::ToBase64String(
  [System.Text.Encoding]::UTF8.GetBytes($text),
  [Base64FormattingOptions]::InsertLineBreaks)
# 每行 76 个字符,行间 CRLF,正是 MIME 期望的样子

PEM 是内置折行的例外,因为 OpenSSL 和整个 -----BEGIN 生态圈都折在 64 个字符处,而没有任何 .NET 标志能产出那个宽度。这个循环很短,它就是标准配方:

$der = [System.IO.File]::ReadAllBytes("./certificate.der")
$b64 = [System.Convert]::ToBase64String($der)
$lines = for ($i = 0; $i -lt $b64.Length; $i += 64) {
  $b64.Substring($i, [Math]::Min(64, $b64.Length - $i))
}
$pem = @("-----BEGIN CERTIFICATE-----") + @($lines) + @("-----END CERTIFICATE-----")
Set-Content -Path "./certificate.pem" -Value ($pem -join "`n")

宽度之所以重要,是因为 Base64 的四个字符一组不理会换行,所以解码器可能完全忽略换行,也可能强制执行换行。本站使用的解码器忽略它们,但严格的消费方不少,证书和邮件的世界里尤其多,它们把意外的换行当作外来字符,拒绝整个负载。当你选择一个宽度时,你是在和消费方签一份合同,值得在脚本里写一行注释,写明你签约的对象是谁。

base64url:两个字符和一个填充决定

标准 Base64 的加号和斜杠在 URL 里只有先做了百分号编码才合法,而等号填充读起来像字段分隔符。于是 RFC 4648 定义了一套对 URL 和文件名都安全的字母表:同样是 64 个字符,只是加号变成了连字符,斜杠变成了下划线,填充通常被丢掉,因为数据的长度已经让填充变得多余。你处理过的每一个 API 令牌和 JWT 都写在这种变体里,而标准坚持称它为 base64url,而不是简单的 base64。

PowerShell 的标准编码器产出标准字母表,所以转成 base64url 就是两次字符交换加一个填充决定:

$bytes = [System.Text.Encoding]::UTF8.GetBytes("Париж encoded 大阪")
$standard = [System.Convert]::ToBase64String($bytes)
$standard
# 0J/QsNGA0LjQtiBlbmNvZGVkIOWkp+mYqg==  标准字母表,含填充
$url = $standard.Replace("+", "-").Replace("/", "_").TrimEnd("=")
$url
# 0J_QsNGA0LjQtiBlbmNvZGVkIOWkp-mYqg  URL 安全形式,填充已去除

在 base64url 的世界里,去掉填充是安全的,因为消费方会从字符串的长度重新算出填充本应是什么。但这并非处处成立,所以把决定说明确:令牌、JWT 段和 URL 内嵌就丢掉填充;任何喂给严格的标准字母表消费方的东西就保留填充;并写下你选了哪个。.NET 运行时确实为这套字母表带了一个专用类 System.Buffers.Text.Base64Url(.NET 9 加入),方法围绕 ReadOnlySpan<T> 参数构建。当前的 PowerShell(7.4 及以后,一旦它运行在自带这个类的 .NET 版本上)其实可以直接调用它们 - [System.Buffers.Text.Base64Url]::EncodeToString($bytes) 今天就能工作,因为方法绑定器现在会隐式地把数组参数转换成 span - 但只要脚本必须在 Windows PowerShell 5.1、较旧的 PowerShell 7.x 版本、或运行在 .NET 9 之前的运行时上的主机上跑,该伸手拿的还是那两字符交换,而且它在上述每一个版本里都管用。

铸造一个 JWT

JSON Web Token 是 base64url 的招牌现实用途,它也是对整条编码流水线的好完整测试,因为一个 JWT 是三段用点号连接的编码段:头部、载荷和签名。前两段是 base64url 的紧凑 JSON,第三段是对前两段精确文本做哈希的二进制输出。下面是一个在 PowerShell 里从头到尾构建的完整 HS256 令牌:

$header = @{ alg = "HS256"; typ = "JWT" } | ConvertTo-Json -Compress
$payload = @{ sub = "1234567890"; name = "John Doe"; iat = 1516239022 } | ConvertTo-Json -Compress
function UrlEncode64([byte[]]$bytes) {
  $standard = [System.Convert]::ToBase64String($bytes).TrimEnd("=")
  return $standard.Replace("+", "-").Replace("/", "_")
}
$left = (UrlEncode64 ([System.Text.Encoding]::UTF8.GetBytes($header))) + "." + (UrlEncode64 ([System.Text.Encoding]::UTF8.GetBytes($payload)))
$hmac = [System.Security.Cryptography.HMACSHA256]::new([System.Text.Encoding]::UTF8.GetBytes("secret"))
$signature = UrlEncode64 ($hmac.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($left)))
$jwt = $left + "." + $signature
# eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6IjEyMzQ1Njc4OTAiLCJuYW1lIjoiSm9obiBEb2UifQ.6MWZy9doHbfyomJd4soTRUQft7PmRM2EyxxT3SLoiyE
#  在 PowerShell 7.4 上;各段内部的键顺序 - 以及由此而来的签名 - 可能因版本而异

看看签名段:一长串 base64url 字母表,加号在那里会显示为连字符,斜杠显示为下划线。这个例子里有三件事能帮你躲开生产事故。第一,签名是算在精确的 JSON 文本上的,键顺序和空白都算数,所以你签名的 JSON 和验证时对照的 JSON 必须逐字节相同。PowerShell 的 ConvertTo-Json 替你决定键顺序,而那不是你控制得了的,所以别在签名和检查之间手工重排令牌的各段或重新格式化它们。第二,-Compress 不是表面的事:一个头部或载荷里含有一个空格的令牌,永远不会通过一个合规实现的验证,因为标准形态是紧凑的。第三,时间戳 iat 是自 Unix 纪元起的秒数,从 Get-Date 直接构建的载荷不做转换就会差出好几年。解码方向,窥探别人铸造的令牌,在姐妹站的相关文章里讲了。

文件与字节流

文件是最常见的负载,流水线也很短。把文件按字节读入,编码,写出文本。要紧的两行是读(必须是字节读)和写(通常不能加行尾换行):

$bytes = [System.IO.File]::ReadAllBytes("./photo.png")
$encoded = [System.Convert]::ToBase64String($bytes)
Set-Content -Path "./photo.b64" -Value $encoded -NoNewline
$encoded.Length
# 你即将发出去的文本大小

两条实用备注。第一条是算术:Base64 让一切都变大,对 10 兆字节的文件,你发出去的文本大约 13.4 兆字节。如果传输有尺寸限制,或者这段文本要进邮件正文或 URL,就在编码之前算账,别等错误出来才算。第二条是行尾换行:Set-Content 默认会加一个,本站使用的解码器和大多数现代解码器会忽略它,但有些严格的消费方不会。-NoNewline 花不了你任何东西,却把问题整个消掉。

PowerShell 6 及更新版本提供了第二种读法,而且留在语言内部:Get-Content -AsByteStream -Raw 一次调用就把文件作为单个字节数组返回,是 .NET ReadAllBytes 的利落替代,对这个用途来说行为完全一致。在 Windows PowerShell 5.1 上(它没有 -AsByteStream),.NET 读法是唯一选项,也是那个在 shell 每个版本上行为都一样的选项。

证书:从 PEM 和 PFX 到文本

证书是日常运维里最重的编码居民,因为部署喜欢把它们当文本来搬。PEM 证书是护甲行之间折好行的 Base64 主体,而折行那一节的配方就是整个导出:

$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new([System.IO.File]::ReadAllBytes("./certificate.der"))
$cert.Subject
# CN=example.org
$b64 = [System.Convert]::ToBase64String($cert.RawData)
# 证书二进制形式的一整行长

PFX 格式是另一匹工作马:一个二进制文件,把证书和它的私钥装在一起,这也是为什么你最常发现它作为 Base64 文本躺在部署脚本和配置存储里。编码一份 PFX 就是上一节那个朴素文件流水线,而在 PowerShell 7 里读回它是单个 cmdlet 的事:

$pfxBytes = [System.IO.File]::ReadAllBytes("./certificate.pfx")
$pfxB64 = [System.Convert]::ToBase64String($pfxBytes)
# 捆绑包的文本形式,可直接放进配置文件
Get-PfxCertificate -FilePath "./certificate.pfx" -Password (ConvertTo-SecureString "secret" -AsPlainText -Force)
# 活证书,无需手动解码

一句安全话,直说,因为 Base64 会诱出相反的错误假设:一个 Base64 的 PFX 就是一个文本形式的私钥。编码改变的是秘密的形状,没有改变它的一分保密性,所以粘进聊天窗口、工单或提交的 Base64 PFX,就是粘进聊天窗口、工单或提交的私钥。对文本形式给予和二进制形式完全相同的对待,并且比起让其中任何一种形式自己待着,优先把它交给证书存储或密钥管理器。

Basic 认证、Data URI 和那些老习惯

Base64 比给它命名的那份标准文档更老。1996 年的 MIME 家族 RFC 把它放进了邮件,HTTP Basic 认证把它放进了早期 web 上的每一次头部交换,客户端至今仍然把凭据对编码成一个 Base64 字符串:

$credential = [System.Text.Encoding]::UTF8.GetBytes("alice:s3cret!")
[System.Convert]::ToBase64String($credential)
# YWxpY2U6czNjcmV0IQ==
# 发送形式:Authorization: Basic YWxpY2U6czNjcmV0IQ==

这里标准字母表才是对的那一套,加号和斜杠都在,因为头部不是 URL,不需要那套安全字母表。同一个机制也出现在 data URI 里,那是文档把自己的二进制内联进来的方式,形状就是一个字面前缀加上字节的标准 Base64:

$dataUri = "data:application/octet-stream;base64," + [System.Convert]::ToBase64String([byte[]](1, 2, 3, 250, 251))
# data:application/octet-stream;base64,AQID+vs=

这两个习惯值得知道的理由,与其说是你将会造出的东西,不如说是你将会遇见的东西:当一个头部或一个链接里出现一长串 Base64 时,这两个格式是你第一个该检查的,而且两者都只差一次普通解码就能说出自己是什么。这正是这个格式的意义,也是本站解码一侧存在的原因。

编码命令与 Windows 工具箱

PowerShell 从 1.0 版本起就带着一个内置的编码理由:宿主自己的 -EncodedCommand 参数。你交给 pwsh 一个 Base64 字符串,它把字节按 UTF-16LE 解码,结果作为命令执行。文档里说的用途,是与外层 shell 的引用规则打架的命令,而编码这一侧只要两行:

$command = "Write-Host 'Hello from the encoded side'"
$encoded = [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($command))
# VwByAGkAdABlAC0ASABvAHMAdAAgACcASABlAGwAbABvACAAZgByAG8AbQAgAHQAaABlACAAZQBuAGMAbwBkAGUAZAAgAHMAaQBkAGUAJwA=
pwsh -NoProfile -EncodedCommand $encoded
# Hello from the encoded side

仔细读那行编码,因为这是所有人搞错的地方:负载必须是 UTF-16LE,也就是 Unicode 编码,而不是 UTF-8。用错的那个编码去编码,宿主仍然会按 UTF-16LE 解码你的字节,然后执行一条由乱码组成的命令,产生的错误正是这个失误的完美画像。解码文章完整讲了这个失败,而这一侧的修复只有一个词:Unicode

语言之外,每个原生工具都带着自己安静的编码决定。在 Windows 上,certutil -encode infile outfile.b64 产出带 PEM 所期望护甲行的标准 Base64 文件,-f 覆盖已存在的输出,而值得记住的标志是 -unicodetext,它让 certutil 以 Unicode 写出输出文件(按微软的文档:"以 Unicode 写出输出文件")- 一个开关藏着一个编码决定。在 Linux 上,经典工具是 base64 -w 0 file,其中 -w 0 才是承重的那部分:没有它,GNU base64 会折在 76 个字符处,你想要的是一行,交到你手里的却是一个 MIME 风格的文件。在 macOS 上,BSD 版本不需要这样的标志,因为它默认就输出一整行不断开的输出。

输出巨大时的编码

对日常尺寸来说,全读全编的流水线就是既快又简单的那一个,也是正确的那一个,直到文件大到内存装起来不自在,或者数据正从一个下载或一个套接字一块一块地到达。那时文档里的工具就是这对流式组合:包在 CryptoStream 里的 System.Security.Cryptography.ToBase64Transform,你写进原始字节,Base64 文本出来,任何时刻活着的只有一个小小的缓冲区:

$source = [System.IO.File]::OpenRead("./photo.png")
$destination = [System.IO.File]::Create("./photo.b64")
$transform = [System.Security.Cryptography.ToBase64Transform]::new()
$stream = [System.Security.Cryptography.CryptoStream]::new($destination, $transform, [System.Security.Cryptography.CryptoStreamMode]::Write)
$buffer = New-Object byte[] 65536
while (($read = $source.Read($buffer, 0, $buffer.Length)) -gt 0) {
  $stream.Write($buffer, 0, $read)
}
$stream.Dispose()
$source.Dispose()
$destination.Dispose()

与一次性方法的一个差别值得写下来:流产出的是一整行连续不断的输出,完全不折行,不管输入多大。忽略空白的解码器不在乎,但如果最终去处是 PEM 文件,之后要拿折行那一节的 64 列循环再过一遍结果。而在 C# 里,标准形态就是 C# 文章里看到的同一个 ToBase64Transform + CryptoStream 模式;PowerShell 直接驱动它,如上所示。

编码负载出岔子的地方

  • 编码了字符串,而不是字节。ToBase64String("Hello") 抛出转换异常,这是方法在告诉你:第一个决定,也就是编码,还没有做。在脚本里把它变可见,错误就消失了。
  • 承诺 UTF-8 的地方给了 UTF-16。负载比预期长一倍,消费方读到的是垃圾。编码是协议的一部分,而现代互联网上几乎每一条线,协议都写着 UTF-8。
  • 5.1 的读取。Windows PowerShell 5.1 在你的脚本看到文件之前,就用机器的 ANSI 代码页读取了无 BOM 的文本文件,所以一个 UTF-8 源文件可能在编码步骤之前就损坏了。在 5.1 上,用显式的 UTF-8 读取文本,并检查结果的前几个字符。
  • 错误的折行宽度。MIME 要 76,PEM 要 64,API 要没有,而严格的消费方把意外的换行当作外来字符。从消费方选宽度,并在注释里写明。
  • 填充站错了交换的边。对 base64url 令牌,丢掉等号是正确的;对期望标准填充的消费方,则是错的。字母表交换和填充决定是两个选择,不是一件事。
  • 重新格式化你签过名的东西。JWT 签名覆盖精确的 JSON 文本,键顺序和空白都算数。重排声明或加一个空格,令牌就不再能通过验证,而且错误消息离原因远得很。
  • 超过 255 的值。把整数转成字节会抛异常,而不是回绕,所以 256 会在转换处停住脚本。如果你的源数据是数字,就显式转换,让错误成为你看得见的错误。
  • 相信了那身戏服。Base64 不是加密,也不是压缩:它是让数据长出三分之一的翻译。用 Base64 写成的秘密是明文里的秘密,用 Base64 写成的文件是还需要多 33% 空间的文件。

可信任编码者的规矩

  • 刻意地产出字节。任何编码脚本的第一行,应该是一个显式的 GetBytes 或一次字节读取,永远不是一种"PowerShell 会帮我把字符串转成正确字节"的期望。
  • 在做出选择的那段代码旁边,用注释点名字母表和宽度:标准还是 base64url,折在 76、折在 64 还是根本不折。读脚本的人是六个月后翻开它的那个人,而那个人就是你。
  • 写文本文件用 -NoNewline,除非消费方明确期望一个行尾换行;行尾(LF 还是 CRLF)则按消费方文档期望的样子选。
  • 边搭边测往返:编码、解码、比较字节。三十秒的 Compare-Object 扫过两个字节数组,就能一次性抓住编码错误、折行错误和字节序错误,趁原因还新鲜。
  • 记录大小,不记录负载。编码前的字节数和编码后的字符数应该坐在大约 1.33 的比例上,当它们不坐在那里时,大小对不上就告诉你该往哪里看,而日志里从头到尾没有数据本身。

PowerShell 是如何继承它的编码器的

PowerShell 里的 Base64 编码,最短的真实历史是:PowerShell 自己从没写过。你调用的那个方法 Convert.ToBase64String,2003 年随 .NET Framework 1.1 一起发布,而自 2006 年 11 月的 1.0 版起,每一个 PowerShell 都只是把它所运行的 .NET 暴露出来。这个项目在开发期间叫 Monad,2003 年 10 月首次在专业开发者大会上公开展示,等到发布时,它所包装的那个 .NET 编码器已经三岁、肩上扛着 web 流量了。

这个格式在 shell 发布的同一年被标准化。RFC 4648 发表于 2006 年 10 月,它固定了字母表、填充规则、解码的严格性和 base64url 变体,而它今天描述的,恰好就是这对 .NET 方法实现的行为。在它之前的 1996 年 MIME RFC 已经把 76 字符折行带进了邮件,这就是为什么那个宽度至今仍是 InsertLineBreaks 的默认值。2016 年 8 月,PowerShell 以 PowerShell Core 之名开源并走向跨平台,编码器跟着一起搬到了 Linux 和 macOS,分毫未改,因为没有任何东西需要改。

后来发生的变化,大多发生在 .NET 里,而且大部分在 PowerShell 够不着的地方。运行时在最近几个版本里获得了更快的、基于 span 的 Base64 帮手,包括 Base64Url 类和带 Try 前缀的解码方法。Span 是 byref 类类型,较旧的 PowerShell 版本是真的完全绑不了它们,但当前 PowerShell 的方法绑定器现在会做隐式的数组到 span 转换,所以在足够新的主机上,这些捷径从脚本里就能调用。对所有更老环境的社区答案是 PowerShell Gallery 上的 Microsoft.PowerShell.TextUtility 模块,它的 ConvertTo-Base64 包装的是同一个 .NET 方法,外加一个 -Text 参数(UTF-8 默认)和一个用于 76 列折行的 -InsertBreakLines 开关。如果你喜欢 cmdlet 的形态,用 Install-Module -Name Microsoft.PowerShell.TextUtility 安装它,并记住该模块已归档、不再积极维护,这也是内置方法仍然是新脚本首选推荐的又一个原因。

要记住的数字和名字

  • 每三个输入字节变成四个输出字符,所以编码后的数据比原始数据大约 33%,而填充从来不会超过两个等号。
  • 默认输出是一整行不断开的行。InsertLineBreaks 折在 76 个字符、行间 CRLF,那是 1996 年的 MIME 约定。PEM 要 64,而没有任何内置标志能产出那个宽度。
  • base64url 是标准 Base64 把加号和斜杠换成了连字符和下划线,填充通常被丢掉,它是每一个 JWT 和 API 令牌所用的字母表。
  • "Café" 用 UTF-8 是五个字节,用 UTF-16LE 是八个。同样可见的文本,两倍的大小,而且这两种编码在线上不能互换。
  • -EncodedCommand 从 PowerShell 第一个发布版就存在,其负载必须是 UTF-16LE,而不是 UTF-8。这一侧最常见的失误,修复它只需要一个词:Unicode
  • certutil -encode 能把一个编码决定藏进 -unicodetext,而 GNU base64 需要 -w 0 才能给你一行,而不是 76 列折行。
  • .NET 基于 span 的 Base64 帮手(包括 Base64Url)曾经从 PowerShell 里够不着,因为 span 是 byref 类类型,旧的方法绑定器绑不了。当前的 PowerShell(7.4+,运行在足够新、能带出这个类的 .NET 运行时上)会把数组参数对着 span 参数解析得毫无怨言,所以直接调用今天就能工作 - 但那两字符交换仍然是那个在每个版本里都管用的配方,新老皆然。
  • 单个字节 123 编码成 ew==:这是那条规则的尽可能小的例子 - 输出的长度告诉你输入的长度。

把箭头翻过来

这篇文章里的所有内容,都是关于把你手里的数据变成一个 Base64 字符串。镜像操作,拿到一个字符串、把你的数据取回来,有它自己的一串问题:一个忽略四种空白的解码器、一句涵盖三宗罪的错误消息、一个可以窥探的 JWT、一个可以拆开的证书,还有一个需要解释的 -EncodedCommand。那个方向有它自己的完整处理,有它自己的陷阱和自己的历史,在姐妹站上那篇相关文章《PowerShell 中的 Base64 解码》里,链接就在下方。

最后更新: 2026-09-08

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