PowerShell 中的 Base64 解码:完整指南
某一行日志、某个配置文件或某条错误信息里,你会撞上它:一长串字母和数字,偶尔掺着一个加号或斜杠,末尾还停着一两个透着可疑的等号。它看起来像噪音。其实不是。它是 Base64,而你已经知道自己想要什么:它藏着的那个东西。
Base64 是一次翻译,既不是压缩,也不是锁。它把任意字节序列重写成可打印文本,每三个输入字节变成四个字符(所以编码后的数据比原始数据大约 33%),使用的是一套 64 个字符的字母表,再加等号作尾部填充。本站首页已经完整讲过字母表、比特运算和各种变体,所以这篇文章把时间花在 PowerShell 真正见功夫的地方:你要调用的那一个 .NET 方法、它强制执行的规则,以及真实工作中十几处让 PowerShell 解码变得有意思的角落。
这个方法和它的契约
PowerShell 并不自带任何 Base64 cmdlet。干活的是 .NET 某个类上的一个方法,这个类从 2003 年的 .NET Framework 1.1 起就是框架的一部分,比 PowerShell 自己发布还早三年:
$bytes = [System.Convert]::FromBase64String("SGVsbG8sIFdvcmxkIQ==")
[System.Text.Encoding]::UTF8.GetString($bytes)
# Hello, World!
这就是全部的 API:一个字符串进去,一个字节数组出来。它在每个操作系统上的每一种 PowerShell 里都能用,Windows PowerShell 5.1 可以,运行在 Windows、Linux 和 macOS 上的 PowerShell 7 也可以,因为它就是 .NET。这份契约短到可以背下来,所以这里把它整理成一张表:
| 输入 | 你得到什么 |
|---|---|
$null |
空数组,不报错。PowerShell 在调用前悄悄把 $null 变成了空字符串 |
| 一个空字符串 | 空数组,不报错 |
| 有效负载 | 一个 byte[],绝不可能是字符串,哪怕数据是文本 |
| 无效负载 | 一个 FormatException,外面给你包了一层 MethodInvocationException |
在写任何错误处理之前,先听一句警告:那个 FormatException 只有一句消息,却涵盖三种不同的罪。字母表之外的字符、超过两个填充字符、或者藏在填充里头的非空白字符,产出的都是完全相同的那句话。当你看到它时,消息不会告诉你犯的是哪一种,所以你得回去读你的输入:
try {
[System.Convert]::FromBase64String("SGV!G8s=")
}
catch {
$real = $_.Exception.InnerException
$real.GetType().Name
# FormatException
$real.Message
}
而"长度必须是四的倍数"这条规则,有一个第一次踩到就会让人意外的边缘。四个字符不带任何填充是完全合法的;它只意味着最后一个字符里多余的比特会被丢弃。三个字符不是四的倍数,会被拒绝:
[System.Convert]::FromBase64String("SGVs").Count
# 3:四个字符不带填充没问题
[System.Convert]::FromBase64String("SGV")
# FormatException:三个字符不是四的倍数
解码器接受什么,拒绝什么
解码器对字母表很严格,对某一件特定的事情却很宽容。合法字符是那 64 个 Base64 数字(A 到 Z、a 到 z、0 到 9,加号和斜杠),以及作尾部填充的等号。恰好有四个空白字符会被忽略,无论出现在哪里、出现多少次:制表符、换行符、回车符和空格。官方 .NET 文档按它们的 Unicode 名字把它们一一列出,这说明这是一条有文档保证的约定,而不是碰巧没出事。
在实践中,这是一种超能力。MIME 是让 Base64 成名的邮件编码,它把编码后的行折在 76 个字符处,所以一份穿过邮件、工单或日志文件的负载,通常都会碎成许多行到达。解码器不在乎。原样粘贴就行:
$wrapped = "VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZy4gQmFzZTY0IHRleHQg`r`n" +
"YXJyaXZlcyB3cmFwcGVkIGF0IHNldmVudHktc2l4IGNvbHVtbnMgaW4gbWFpbCwgc28gdGhlIGRl`r`n" +
"Y29kZXIgbXVzdCBub3QgY2FyZS4="
[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($wrapped))
# The quick brown fox jumps over the lazy dog. Base64 文本到达时
# 在邮件里按 76 列折行,所以解码器必须毫不在意。
其余一切不是字母表字符的东西,都是硬停止。野外最常见的肇事者是不换行空格(从网页上粘贴出来的文本最爱带它)和字节顺序标记 BOM(文件以错误编码读取时跟着你甩不掉的隐形标记)。对这个方法来说,两者都不是空白,所以两者都会抛异常:
try {
[System.Convert]::FromBase64String("SGVs`u{00A0}G8=")
}
catch {
$_.Exception.InnerException.GetType().Name
# FormatException
}
这种严格是故意的,不是脾气坏。RFC 4648 是 2006 年把 Base64 写成规范的那份标准,它说实现必须拒绝字母表之外的字符,除非协议明确允许宽容,因为一个悄悄吞下外来字符的解码器,可以被变成一条隐蔽信道,用来把数据夹带过任何只检查字母表的关卡。.NET 的解码器遵循严格规则,而你通常也希望它这么做。
字节数组不是字符串
这个方法故意停在字节数组这一步。那些字节意味着什么,是只有你能做的第二个决定,而猜错它是 PowerShell Base64 工作里最著名的错误。默认假设 UTF-8 对互联网上几乎一切都正确,往返也只要两个调用:
$bytes = [System.Convert]::FromBase64String("SGVsbG8sIFdvcmxkIQ==")
[System.Text.Encoding]::UTF8.GetString($bytes)
# Hello, World!
你真正会去拿的几种编码,以及每种猜错时会发生什么:
| 编码 | 什么时候用它 | 猜错会怎样 |
|---|---|---|
UTF8 |
Web API、JSON、JWT、一切现代的东西。安全的默认值 | Latin-1 或 UTF-16 文本回来时是一堆乱码 |
Unicode(UTF-16LE) |
负载来自 Windows 工具链、一个注册表值,或者一个发货前就编码好的 .NET 字符串 | 每个字符都带上一圈空隙,因为在本该读两个字节的地方你只读了一个 |
ASCII |
经典的 HTTP Basic 凭据,以及其他保证 7 位的协议 | 数值超过 127 的字符统统变成问号 |
Latin1 |
早于 UTF-8 的欧洲遗留文本 | 多字节的 UTF-8 序列被拆成几个错误的字母 |
Default |
几乎永远不要。它是这台机器的系统代码页 | 你的脚本在每种 Windows 区域设置下表现都不同 |
经典失败是把 UTF-8 文本当 UTF-16 解码。字节是真的,方法很满意,结果依然是垃圾:
# "SGk=" 是 "Hi" 的 UTF-8 字节
[System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String("SGk="))
# 一个不可读的字符:2 个 UTF-8 字节被当作一个 2 字节 UTF-16 单元读取
实用的规则:如果解码出来的文本看起来每个字符都带着一圈看不见的空隙,或者像另一种字母表,你就是错了一个编码。去问数据是在哪里产生的;拿不准时,信任 UTF-8,但用眼睛确认前几个字符。并且在解码之前就定好编码,而不是等乱码出现在日志里才补救。
base64url:在 URL 里安分守己的字母表
在你将来会碰到的每一个 API 令牌、JWT 和嵌在 URL 里的标识符中,你都会遇到 Base64 的一个表亲。标准 Base64 的加号和斜杠在 URL 里只有先做了百分号编码才合法,而等号填充又长得像字段分隔符。于是 RFC 4648 定义了一套对 URL 和文件名都安全的字母表:同样是 64 个字符,只是加号变成了连字符,斜杠变成了下划线。填充通常被整个丢掉,因为数据的长度已经让填充变得多余。RFC 特意强调这种变体应该叫 base64url,而不只是 "base64",本节其余部分也遵循这个叫法。
.NET 确实为它配了一个专用类 System.Buffers.Text.Base64Url,在 .NET 9 中加入,快速的编码和解码方法完全围绕 ReadOnlySpan<T> 参数构建。多亏方法绑定器现在会做隐式的数组/字符串到 span 的转换,当前的 PowerShell(7.4 及以后,只要它运行在自带这个类的 .NET 版本上)今天就能直接调用这些接收 span 的重载,所以 [System.Buffers.Text.Base64Url]::DecodeFromChars("--__AQI") 可以不做任何铺垫就直接工作。但这并非一直如此:Windows PowerShell 5.1 和较旧的 PowerShell 7.x 版本根本无法绑定到 span 参数,而且这个类在 .NET 9 之前压根不存在,所以任何必须在 5.1、较旧 7.x 或 .NET 9 之前的主机上运行的脚本,仍然需要那个可移植的版本:交换这两个字符,然后在把文本交给标准解码器之前补回填充。要补的填充就是让长度成为四的倍数所需的那个量:
$token = "--__AQI" # base64url,无填充
$standard = $token.Replace("-", "+").Replace("_", "/")
$pad = 4 - ($standard.Length % 4)
if ($pad -eq 4) { $pad = 0 }
$standard = $standard.PadRight($standard.Length + $pad, "=")
$bytes = [System.Convert]::FromBase64String($standard)
$bytes -join ","
# 251,239,255,1,2
那小段代码里住着两个坑。第一,填充算术:长度已经是四的倍数的负载不需要填充,-eq 4 守卫就是让表达式保持诚实的东西。第二,方向:当你只解码时,要做的是补上填充并交换字符;你永远不会从标准 Base64 输入上移除填充,因为标准解码器期望它就在那里。如果来源是 JWT 或 API 令牌,它就是无填充的 base64url,上面的配方正是你要的形状。
不开密钥打开一个 JWT
一个 JSON Web Token 是三段用点号连接的 base64url:头部、载荷、签名。前两段是纯 JSON,而 Base64 不是加密,所以任何拿着令牌的人都能读出这两段。这是特性,不是缺陷:令牌本来就是设计成供检查的,让它在伪造面前站得住的是签名。PowerShell 把这个窥视变成三行:
$jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
$parts = $jwt.Split(".")
function Decode-UrlSegment([string]$segment) {
$standard = $segment.Replace("-", "+").Replace("_", "/")
$pad = 4 - ($standard.Length % 4)
if ($pad -eq 4) { $pad = 0 }
$standard = $standard.PadRight($standard.Length + $pad, "=")
return [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($standard))
}
Decode-UrlSegment $parts[0] | ConvertFrom-Json | ConvertTo-Json -Compress
Decode-UrlSegment $parts[1] | ConvertFrom-Json
# name 属性:
(Decode-UrlSegment $parts[1] | ConvertFrom-Json).name
# John Doe
有三件事要记住。第三段,签名,同样是 base64url,但它解码出来是签名的二进制字节,不是文本,所以别指望在那里看到漂亮的 JSON。头部通常只是告诉你哪个算法签了这个令牌(HS256、RS256 等等),而写着 none 的头部是一面红旗,不是什么便利。另外,读载荷不等于信任载荷:base64 让你看到声明,只有签名让它们可信。如果你的工作是接受令牌,就用签发方的密钥验证签名;如果你的工作是调试某一个令牌,上面的代码就是你所需要的一切。
文件、PEM 与通往字节的长途
最常见的文件形态是一个装着更大东西的 Base64 的文本文件:一个备份块、一个下载的二进制、一个序列化对象。往返只要四行,而现代的输出读法是一个真正的字节数组,不是一次文本猜测:
$encoded = Get-Content -Path ./payload.b64 -Raw
$encoded = $encoded.Trim()
$bytes = [System.Convert]::FromBase64String($encoded)
[System.IO.File]::WriteAllBytes("./payload.bin", $bytes)
$bytes.Length
# 文本承载了多少字节
读回原始二进制,正是 PowerShell 6 及更新版本发挥价值的地方。-AsByteStream 参数读取原始字节,配上 -Raw 能一次性交给你一个货真价实的 byte[]:
$bytes = Get-Content -Path ./photo.png -AsByteStream -Raw
$encoded = [System.Convert]::ToBase64String($bytes)
Set-Content -Path ./photo.b64 -Value $encoded -NoNewline
$bytes.Length
# 原始大小,还没交 33% 的文本税
省掉 -Raw,你得到的是一串独立的字节对象(捕获起来是 Object[]),拿来检查没问题,但传给期望数组的 .NET 方法就错了。而且 Windows PowerShell 5.1 根本没有 -AsByteStream,所以在 5.1 上可靠的读法是 [System.IO.File]::ReadAllBytes(),它处处可用。
PEM 是你在每张证书和每把私钥上见过的那个带护甲的表亲:一个标准 Base64 主体,通常折在 64 个字符处,夹在 -----BEGIN ... 和 -----END ... 两行之间。护甲是文本;主体是负载。剥掉护甲,把各行连起来,解码:
$pem = Get-Content -Path ./certificate.pem -Raw
$body = ($pem -split "`n") | Where-Object { $_ -notmatch "^-----" } | ForEach-Object { $_.Trim() }
$der = [System.Convert]::FromBase64String(($body -join ""))
$der.Length
# 证书的二进制 DER 大小
既然标准解码器反正忽略空白,-join "" 属于双保险而不是硬性要求,但在脚本里明写它剥掉了什么,会让它在每台机器、每种行尾约定下行为一致。反方向,把 DER 字节包成 PEM,就是 Base64 编码器外加两行文本,姐妹站的编码文章完整演示了 64 列折行。
证书与 Windows 工具箱
证书是日常工作里最重的 Base64 居民,而 PowerShell 能装下整个家族。PFX 文件是证书加私钥的二进制捆绑包,也是你在配置文件和部署脚本里最常发现以 Base64 文本形式躺着的那个格式。把它解码回一张活证书,用 .NET 类型只要一行,而且在 PowerShell 7 里跨平台可用:
$bytes = [System.Convert]::FromBase64String($pfxText)
$password = ConvertTo-SecureString "secret" -AsPlainText -Force
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($bytes, $password)
$cert.Subject
# CN=example.org
$cert.NotAfter
# 它何时不再有效
PowerShell 7 还自带 Get-PfxCertificate,它带一个 -Password 参数直接从磁盘读取 PFX 文件,所以对于磁盘上的文件,你可以完全跳过手动解码。一张裸证书(不带密钥)更简单:DER 字节直接进同一个 X509Certificate2 类型,连密码都不用。
语言之外,有两个原生工具值得知道。在 Windows 上,certutil -decode infile.b64 outfile 以文件进/文件出的语义解码一个 Base64 文件(加 -f 可以覆盖),这让它在普通命令提示符里成了快速修复的首选。它的兄弟 certutil -encode 有一个值得记住的标志:-unicodetext 会在 Base64 编码之前把输入文本转成 UTF-16,把一整个编码决定藏进了一个开关里。在 Linux 和 macOS 上,经典工具是 base64 -d,它解码一个文件或标准输入,默认跳过换行;在 GNU coreutils 上,如果负载还带着来自 Windows 邮件的空格、制表符或 CRLF,就再加 -i。
装在 Base64 信封里的命令
PowerShell 从 1.0 版本起就有一个内置的理由要说 Base64:宿主自己的 -EncodedCommand 参数。你交给 pwsh 一个 Base64 字符串,它把字节按 UTF-16LE 解码,结果作为命令执行。官方用途,引自文档原话:提交那些需要复杂引号或花括号的命令,而不用和外层 shell 的引用规则搏斗:
$command = "Write-Host encoded-hello"
$encoded = [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($command))
# VwByAGkAdABlAC0ASABvAHMAdAAgAGUAbgBjAG8AZABlAGQALQBoAGUAbABsAG8A
pwsh -NoProfile -EncodedCommand $encoded
# encoded-hello
仔细读第二行,因为所有人都在这里翻车:负载必须是 UTF-16LE,也就是 [System.Text.Encoding]::Unicode。如果你改成用 UTF-8 编码命令,PowerShell 会乐呵呵地按 UTF-16LE 解码,然后执行一条由乱码组成的命令,而它产生的错误消息正是这个失误的完美画像:
$wrong = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($command))
pwsh -NoProfile -EncodedCommand $wrong
# 错误:一屏乱码,"The term ... is not recognized..."
安全团队在意 PowerShell 里的 Base64,正是出于同一套机制。一个传给 -EncodedCommand 的长而不透明的令牌,是自动化工具的常见形态,这也正是终端防护产品会在这些负载运行之前先解码它的原因:Base64 没有任何一面能把命令从解码器眼里藏住,它只是把命令藏起来,不让读进程列表的人看见。如果你为自己的自动化生成编码命令,就把源命令和令牌放在一起,因为凌晨三点,令牌本身不会替你解释自己。
输入巨大时的解码
对日常尺寸来说,单方法路线就是最快的。一个五兆字节的二进制会变成大约六百九十万个字符的字符串,解码这个字符串在现代机器上只要个位数毫秒。.NET 文档自己的备注说,FromBase64String 就是为处理"包含全部数据的单个字符串"而设计的,这没错,而且一直到非常大的限度都没问题,因为这个方法直接在字符串上工作,没有有意义的额外拷贝。
当负载大到你不愿意把它整个装进一个字符串,或者它是以流的形式到达(一个下载、一个套接字、一个巨大的日志),文档里的工具就是包在 CryptoStream 里的 System.Security.Cryptography.FromBase64Transform:你喂给它 Base64 文本,读出解码后的字节,任何时刻活着的只有一个小小的缓冲区。注意 TransformStream,C# 里为它准备的帮手,是一个扩展方法,而 PowerShell 看不到扩展方法,所以直接实例化 CryptoStream:
$inputStream = [System.IO.File]::OpenRead("./payload.b64")
$transform = [System.Security.Cryptography.FromBase64Transform]::new()
$stream = [System.Security.Cryptography.CryptoStream]::new(
$inputStream, $transform, [System.Security.Cryptography.CryptoStreamMode]::Read)
$destination = [System.IO.File]::Create("./payload.bin")
$buffer = New-Object byte[] 65536
while (($read = $stream.Read($buffer, 0, $buffer.Length)) -gt 0) {
$destination.Write($buffer, 0, $read)
}
$destination.Dispose()
$stream.Dispose()
$inputStream.Dispose()
对九成工作来说,简单路线仍然是对的:用 Get-Content -Raw 读入整个文本文件,修剪它,解码它,写出字节。当文件大到内存装起来不自在,或者数据正一块一块地到达时,才去拿流式版本。也别试图按行循环、逐行解码:Base64 的四个字符一组不理会你的换行,一行如果在组的中间被截断,它自己就解不出来。读入整个文本,然后一次解码。
吃掉整个下午的坑
- 字符集猜测。把 UTF-8 当 UTF-16 读,或把 Latin-1 当 UTF-8 读,产出的都是信心十足的乱码。从数据来源决定编码,默认 UTF-8,并在信任其余内容之前先看前几个解码出来的字符。
- 来自网页的隐形字符。从页面或富文本邮件粘贴来的不换行空格或字节顺序标记 BOM,对解码器来说都是外来字符,都会抛出通用的
FormatException。解码之前,先让输入经过.Trim()和一个非打印字符检查。 - 填充混淆。标准 Base64 到达时末尾带着
=或==;来自令牌的 base64url 则一个都没有。把其中一个喂进为另一个写的配方,是 API 工作里最常见的静默损坏,而 base64url 一节里的长度检查就是那道守卫。 - 一句消息,三宗罪。因为
FormatException的消息同时涵盖坏字符、过量填充和脏填充,只记录消息的 catch 块只会让你绕圈子。把输入的长度和第一处问题区域也记下来。 - 期待拿回字符串。结果永远是字节数组。你一开始直接把它当字符串格式化,得到的就是一串数字,而不是文本。用显式编码转换,只一次,在最后。
- 5.1 的文件默认值。Windows PowerShell 5.1 用系统的 ANSI 代码页读取无 BOM 的文件,而 PowerShell 7 假设 UTF-8。如果你的脚本在 5.1 上读取那个 Base64 文本文件,而文件是 UTF-8、负载周围还有非 ASCII 内容,损坏在解码器看到它之前就已经发生了。
- 把 Base64 当锁。它是翻译。用 Base64 写成的密码、令牌或秘密,是穿着戏服的明文,地球上每一个解码器(包括这篇文章)一行就能打开它。
让脚本保持诚实的习惯
- 解码之前先修剪外部输入。一个
.Trim()消除的生产事故比任何错误处理都多。 - 来源不受信任时,先校验再解码:剥掉那四个允许的空白字符之后,字符串应该只匹配字母表字符,外加至多两个尾部等号。一个快速的正则检查,能把神秘异常变成一条干净的"输入被拒绝"消息。
- 把字节一直当字节,直到最后一步。只解码一次,把
byte[]交给需要它的文件 API 或编码器,然后才用一次深思熟虑的编码转换成文本。 - 记录长度,不记录负载。输入的大小和解码输出的大小,几乎能告诉你一次解码失败的全部信息,而日志里不必粘进任何可能敏感的数据。
- 对任何要过网络的东西,在它被解码的同一行代码里记下来:它用的是哪套字母表,标准还是 base64url,以及哪种填充约定。那条备注的消费者是未来的你。
PowerShell 是如何继承它的解码器的
PowerShell 里的 Base64,最短的真实历史是:PowerShell 自己从没写过。你用的那个方法 Convert.FromBase64String,2003 年随 .NET Framework 1.1 一起发布,而自 2006 年 11 月的 1.0 版起,每一个 PowerShell 都只是把它所运行的 .NET 暴露出来。这个项目在开发期间叫 Monad,2003 年 10 月首次在专业开发者大会上公开展示,等它发布时,它所包装的那对 .NET 编码器-解码器已经三岁、每天有人在用了。
这个格式本身在 shell 发布的同一年被标准化。RFC 4648 发表于 2006 年 10 月,就是它把字母表、填充规则、严格解码的预期和 base64url 变体固定了下来,而它今天描述的,恰好就是 FromBase64String 实现的行为。2016 年 8 月,PowerShell 以 PowerShell Core 之名开源并走向跨平台,解码器跟着一起搬上了 Linux 和 macOS,毫发未改,因为没有任何东西需要改。
唯一真正的附加物,是 PowerShell Gallery 上社区维护的 Microsoft.PowerShell.TextUtility 模块,它的 ConvertFrom-Base64 cmdlet 包装的是同一个 .NET 方法,外加一个 -AsByteArray 开关和一个按 UTF-8 解码的文本默认值。如果你喜欢 cmdlet 的形态,用 Install-Module -Name Microsoft.PowerShell.TextUtility 安装它,但有一个提醒:该模块如今已归档、不再积极维护,这也是内置方法仍然是新脚本首选推荐的又一个原因。
值得记住的事实
- 解码器忽略输入中任何位置的制表符、换行符、回车符和空格。一百行折行,解码起来和一整行长得完全一样。
$null和空字符串都解成空数组,毫无怨言,这让FromBase64String在边缘地带格外宽容。- 那句唯一的
FormatException消息涵盖三种不同的失败模式。它响起来的时候,答案在输入里,不在消息里。 "SABpAA=="是字符串Hi用 PowerShell 自己的内部编码 UTF-16LE 写成的样子。它比同样两个字母的 UTF-8 编码长一倍,而这个比例,就是你在任何 Base64 里读 Windows 原生文本时的指纹。-EncodedCommand从 PowerShell 第一个发布版就存在,其负载被规定必须是 UTF-16LE,而不是 UTF-8。用错的编码去编码,shell 会开开心心地执行你的乱码。- .NET 较新的基于 span 的 Base64 帮手(包括
Base64Url类),在较旧的 PowerShell 版本里够不着,因为 span 是 byref 类类型,方法绑定器绑不了它们。现在变了:当前的 PowerShell(7.4+,运行在足够新、能带出这个类的 .NET 版本上)会把数组或字符串参数对着ReadOnlySpan<T>参数解析得毫无怨言,所以直接调用今天就能工作。两字符交换的价值,在于它也是能在 Windows PowerShell 5.1 和更老主机上跑的那个版本,而不是仅剩的一条路。 - 不带
-Raw的Get-Content -AsByteStream给你一串字节对象,不是字节数组。加上-Raw,类型就恰好是 .NET 方法所期望的。
绕一圈的大路
这篇文章里的所有内容,都是关于拿到一个 Base64 字符串、把你的数据取回来。镜像操作,把数据变成 Base64,看起来像一行命令就能搞定,直到你撞上这些事实:PowerShell 的字符串不是字节,UTF-16 会让你的体积翻倍,折行有两种惯例宽度,而 base64url 输出需要它自己的两字符手术。那个方向有它自己的完整处理,有它自己的陷阱和自己的历史,在姐妹站上那篇相关文章《PowerShell 中的 Base64 编码》里,本页下方就是它的链接。
最后更新: 2026-09-07