C# Base64解码 — 完整指南

·Game Developer & Unity Engineer·审阅者Emma Richardson·发布日期

直接在浏览器中使用免费的 Base64解码器,无需安装。

在线试用 Base64解码器 →

我工作过的每个 .NET 项目最终都需要解码 Base64——从 Kubernetes secret 中读取连接字符串、 处理 webhook 的二进制载荷,或者在调试时检查 JWT token。在 C# 中解码 Base64, 首选方法是 Convert.FromBase64String(),它返回一个 byte[], 再传入 Encoding.UTF8.GetString() 即可得到可读文本。如需快速验证而无需写代码, ToolDeck 的 Base64 解码器 可在浏览器中即时处理。本指南涵盖 Convert.FromBase64String()、 适用于 .NET 5+ 的基于 Span 的 TryFromBase64String()、 高性能 System.Buffers.Text.Base64 API、JWT 载荷提取、 文件与 API 响应解码、使用 CryptoStream 的流式处理, 以及 C# 开发者最常遭遇的四类错误。

  • Convert.FromBase64String(s) + Encoding.UTF8.GetString(bytes) 是标准的两步流程——适用于所有 .NET 版本。
  • Convert.TryFromBase64String() 对无效输入不抛出异常,将结果写入 Span<byte>——是 .NET 5+ 热路径的理想选择。
  • System.Buffers.Text.Base64.DecodeFromUtf8() 为 UTF-8 字节缓冲区提供零分配解码,适合性能敏感的服务。
  • JWT token 使用 Base64url(用 - 和 _ 代替 + 和 /)——调用 Convert.FromBase64String() 前必须对输入进行规范化。
  • CryptoStream 配合 FromBase64Transform 支持大文件的流式解码,无需将全部内容加载到内存。

什么是 Base64 解码?

Base64 编码将二进制数据转换为 64 个字符的 ASCII 字母表,使其能够在纯文本传输中存活—— JSON 字段、HTTP 头、电子邮件正文、XML 属性。每 3 个字节的输入变为 4 个 Base64 字符, 这就是 Base64 输出始终比原始数据大约 33% 的原因。解码则逆转这一过程。 末尾的 = 填充告诉解码器最后一组需要裁剪多少字节。 单个 = 表示最后一块有 2 个字节;== 表示只有 1 个字节。 Base64 不是加密——任何人都可以逆向还原。它的目的是安全传输, 而非保密性。

Before · text
After · text
cmVkaXM6Ly9jYWNoZS1wcm9kLmludGVybmFsOjYzNzkvc2Vzc2lvbi1zdG9yZQ==
redis://cache-prod.internal:6379/session-store

Convert.FromBase64String() — 标准解码方法

Convert.FromBase64String() 方法自 .NET Framework 1.1 时代就已存在。 无需 NuGet 包,除 System 外无需额外导入——直接调用即可获得 byte[]。 将 Base64 解码为 C# 字符串的两步流程始终不变:先用 Convert.FromBase64String() 获取字节, 再用 Encoding.UTF8.GetString() 将字节解释为文本。 需要注意的是,该方法返回原始字节而非字符串。你需要选择正确的 Encoding 将字节转换回文本,而这个选择比大多数人预期的更为关键。 编码不匹配会悄无声息地产生乱码——没有任何异常提醒你。

最简工作示例

C# (.NET 6+)
using System;
using System.Text;

// 存储在 Kubernetes secret 中的 Base64 连接字符串
string encoded = "cmVkaXM6Ly9jYWNoZS1wcm9kLmludGVybmFsOjYzNzkvc2Vzc2lvbi1zdG9yZQ==";

byte[] decodedBytes = Convert.FromBase64String(encoded);
string connectionString = Encoding.UTF8.GetString(decodedBytes);

Console.WriteLine(connectionString);
// redis://cache-prod.internal:6379/session-store

除非有特殊原因,始终使用 Encoding.UTF8。 .NET 运行时内部使用 UTF-16 表示字符串,但跨系统边界传输的数据 (API 响应、配置文件、密钥)通常编码为 UTF-8。 对包含多字节字符的数据使用 Encoding.ASCII 会悄悄地将其替换为 ?—— 不抛出异常,只是输出损坏。

往返验证

C# (.NET 6+)
using System;
using System.Text;

string original = "postgres://db-admin:Kx8!mQ@db-prod.us-east-1.internal:5432/orders";

// 编码
string encoded = Convert.ToBase64String(Encoding.UTF8.GetBytes(original));
Console.WriteLine(encoded);
// cG9zdGdyZXM6Ly9kYi1hZG1pbjpLeDghbVFAZGItcHJvZC51cy1lYXN0LTEuaW50ZXJuYWw6NTQzMi9vcmRlcnM=

// 解码
byte[] decoded = Convert.FromBase64String(encoded);
string recovered = Encoding.UTF8.GetString(decoded);

Console.WriteLine(recovered == original); // True

选择正确的编码

传给 GetString()Encoding 必须与数据原始编码一致。 选错了会得到乱码,且不会有任何异常——解码器会愉快地产生无意义的输出。 以下是实际使用中的规则:

  • Encoding.UTF8 — 安全的默认值,处理 ASCII 和所有 Unicode。除非明确知道其他情况,否则使用此项。
  • Encoding.ASCII — 仅用于纯 7 位 ASCII 数据。多字节字符会变成 ?
  • Encoding.Unicode — 在 .NET 中即 UTF-16LE。某些 Windows 内部字符串和 PowerShell 导出使用此编码。
  • Encoding.Latin1 — 旧式西欧编码,出现在老旧的 SOAP 服务和大型机集成中。
C# (.NET 6+)
using System;
using System.Text;

// 相同字节,不同编码——不同结果
byte[] decoded = Convert.FromBase64String("w7bDvMOk");

Console.WriteLine(Encoding.UTF8.GetString(decoded));    // öüä  (正确)
Console.WriteLine(Encoding.ASCII.GetString(decoded));   // ??????  (丢失)
Console.WriteLine(Encoding.Latin1.GetString(decoded));  // öüä  (乱码)

带错误处理的可复用解码辅助方法

由于 Convert.FromBase64String() 在输入有误时会抛出异常, 而去除空白字符的操作又频繁出现,我在大多数项目中会保留一个小辅助方法:

C# (.NET 6+)
using System;
using System.Text;

static class Base64Helper
{
    public static string? DecodeToString(string encoded)
    {
        if (string.IsNullOrWhiteSpace(encoded))
            return null;

        // 去除 .NET 解码器不接受的空白字符
        string cleaned = encoded
            .Replace("
", "")
            .Replace("
", "")
            .Replace(" ", "")
            .Trim();

        try
        {
            byte[] bytes = Convert.FromBase64String(cleaned);
            return Encoding.UTF8.GetString(bytes);
        }
        catch (FormatException)
        {
            return null; // 或抛出领域特定异常
        }
    }
}

// 使用
string? decoded = Base64Helper.DecodeToString("  cmVkaXM6Ly9jYWNoZQ==  \n");
Console.WriteLine(decoded); // redis://cache
注意:Convert.FromBase64String() 若输入包含 Base64 字母表以外的字符—— 包括空格、换行符以及 -_ 等 URL 安全字符——则抛出 FormatException。 上面的辅助方法会自动处理空白字符。

将 Base64 解码为非标准类型

解码器始终返回 byte[]。如何处理这些字节取决于原始数据的类型。 有时是存储为 16 个原始字节的 GUID,有时是序列化的 protobuf 消息, 有时是二进制格式的时间戳。以下是我最常用的转换方式。

Base64 转 GUID

C# (.NET 6+)
using System;

// 某些 API 以 22 字符 Base64 而非 36 字符十六进制字符串发送 GUID
string compactGuid = "C0HqetxMckKlZw4CssPUeQ==";
byte[] guidBytes = Convert.FromBase64String(compactGuid);
Guid recovered = new Guid(guidBytes);

Console.WriteLine(recovered);
// 7aea41c0-4cdc-4272-a567-0e02b2c3d479

Base64 转 System.Text.Json 反序列化 JSON

C# (.NET 6+)
using System;
using System.Text;
using System.Text.Json;

// 来自消息队列的 Base64 编码 JSON 载荷
string encoded = "eyJzZXJ2aWNlIjoicGF5bWVudC1nYXRld2F5IiwicmVnaW9uIjoiZXUtd2VzdC0xIiwicmVwbGljYXMiOjR9";

byte[] jsonBytes = Convert.FromBase64String(encoded);
string json = Encoding.UTF8.GetString(jsonBytes);
Console.WriteLine(json);
// {"service":"payment-gateway","region":"eu-west-1","replicas":4}

// 反序列化为 record
var deployEvent = JsonSerializer.Deserialize<DeployEvent>(jsonBytes);
Console.WriteLine($"{deployEvent!.Service} in {deployEvent.Region}");
// payment-gateway in eu-west-1

record DeployEvent(string Service, string Region, int Replicas);

Base64 转十六进制字符串

C# (.NET 5+)
using System;

// 以 Base64 存储的 SHA-256 哈希
string hashBase64 = "n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg=";
byte[] hashBytes = Convert.FromBase64String(hashBase64);
string hex = Convert.ToHexString(hashBytes);

Console.WriteLine(hex);
// 9F86D081884C7D659A2FEAA0C55AD015A3BF4F1B2B0B822CD15D6C15B0F00A08
警告:切勿使用 BinaryFormatter 反序列化不受信任的 Base64 数据。 它存在已知的远程代码执行漏洞,且在 .NET 8+ 中已被废弃。 如果编码内容来自外部来源,请使用 JSON 或 protobuf 解析, 而非 .NET 二进制序列化。

Base64 解码方法参考

.NET 在两个命名空间中提供了多种解码方法。Convert 类处理通用解码,而 System.Buffers.Text.Base64 则针对已在处理原始 UTF-8 字节缓冲区的高吞吐量场景。

方法
返回值
输入类型
描述
Convert.FromBase64String(string)
byte[]
string
将标准 Base64 字符串解码为字节数组;输入无效时抛出 FormatException
Convert.TryFromBase64String(string, Span<byte>, out int)
bool
string + Span<byte>
尝试将解码结果写入调用方分配的 Span;失败时返回 false 而非抛出异常(.NET 5+)
Convert.FromBase64CharArray(char[], int, int)
byte[]
char[] + offset + length
解码 char 数组的某个片段;适用于无需创建子字符串的缓冲区解析
Convert.TryFromBase64Chars(ReadOnlySpan<char>, Span<byte>, out int)
bool
ReadOnlySpan<char> + Span<byte>
基于 Span 的零分配解码,从 char span 中读取(.NET 5+)
System.Buffers.Text.Base64.DecodeFromUtf8(ROSpan<byte>, Span<byte>, out int, out int, bool)
OperationStatus
ReadOnlySpan<byte> + Span<byte>
高性能 UTF-8 字节解码;返回 OperationStatus 枚举(.NET Core 2.1+)
System.Buffers.Text.Base64.DecodeFromUtf8InPlace(Span<byte>, out int)
OperationStatus
Span<byte>
原地解码,覆盖输入缓冲区;零额外分配(.NET Core 2.1+)

Convert.TryFromBase64String() — 无异常解码

Try 模式是 .NET 中处理可能因用户输入失败的操作的标准方式。 自 .NET 5 起可用的 Convert.TryFromBase64String() 返回 bool 而非抛出 FormatException。 它将解码后的字节写入调用方提供的 Span<byte>, 这意味着对于小载荷可以使用栈分配内存,完全跳过堆。

C# (.NET 5+)
using System;
using System.Text;

string userInput = "eyJob3N0IjoiMTAuMC4xLjUwIiwicG9ydCI6ODQ0M30=";

// 对小载荷进行栈分配(< 1KB 是安全的经验法则)
Span<byte> buffer = stackalloc byte[256];

if (Convert.TryFromBase64String(userInput, buffer, out int bytesWritten))
{
    string result = Encoding.UTF8.GetString(buffer[..bytesWritten]);
    Console.WriteLine(result);
    // {"host":"10.0.1.50","port":8443}
}
else
{
    Console.WriteLine("Invalid Base64 input — skipping");
}

这种方式在请求中间件或验证管道中表现突出——在这些场景中, 错误输入是预期情况而非异常。在高并发场景下, 对每个格式错误的请求抛出并捕获 FormatException 会带来可观的开销——TryFromBase64String() 完全规避了这一问题。

在不解码的情况下验证 Base64 输入

API 控制器中的常见模式:在将输入传递到下游之前,检查其是否为有效的 Base64。 可以通过分配一个丢弃缓冲区来使用 TryFromBase64String() 作为验证器:

C# (.NET 5+)
using System;

static bool IsValidBase64(string input)
{
    // 计算最大解码大小
    Span<byte> buffer = stackalloc byte[((input.Length + 3) / 4) * 3];
    return Convert.TryFromBase64String(input, buffer, out _);
}

// 在 API 控制器中使用
Console.WriteLine(IsValidBase64("eyJob3N0IjoiMTAuMC4xLjUwIn0=")); // True
Console.WriteLine(IsValidBase64("not!!valid!!base64"));              // False
Console.WriteLine(IsValidBase64(""));                                // True(空字符串有效)
注意:输出缓冲区必须足够大。若缓冲区太小, 即使输入有效,TryFromBase64String() 也会返回 false。 为安全起见,将所需大小计算为 (inputLength / 4) * 3

从文件和 API 响应中解码 Base64

从磁盘读取 Base64 编码的文件

证书、加密 blob 和数据导出文件有时以 Base64 文本形式交付。 典型流程:以字符串形式读取文件,去除会引发 FormatException 的空白字符或换行符, 解码为字节,然后写出二进制结果。注意错误处理——文件 I/O 错误和 Base64 格式错误应分别捕获。

C# (.NET 6+)
using System;
using System.IO;

string inputPath = "tls-cert.pem.b64";
string outputPath = "tls-cert.pem";

try
{
    string encoded = File.ReadAllText(inputPath).Trim();
    // 去除换行符——.NET 解码器不接受换行符
    encoded = encoded.Replace("
", "").Replace("
", "");

    byte[] decoded = Convert.FromBase64String(encoded);
    File.WriteAllBytes(outputPath, decoded);

    Console.WriteLine($"Decoded {decoded.Length} bytes -> {outputPath}");
}
catch (IOException ex)
{
    Console.Error.WriteLine($"File error: {ex.Message}");
}
catch (FormatException ex)
{
    Console.Error.WriteLine($"Invalid Base64: {ex.Message}");
}

从 HTTP API 响应中解码 Base64 字段

云 API(Azure Key Vault、AWS Secrets Manager、GitHub Contents API)经常将二进制数据 作为嵌入在 JSON 中的 Base64 字符串返回。流程始终如一:发起 HTTP 请求,解析 JSON 响应, 提取 Base64 字段,然后解码。以下示例使用 HttpClient System.Text.Json——均内置于 .NET 6+。

C# (.NET 6+)
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer sk-prod-9f8e7d6c");

try
{
    var response = await client.GetAsync("https://api.example.com/secrets/db-password");
    response.EnsureSuccessStatusCode();

    string body = await response.Content.ReadAsStringAsync();

    // API 返回:{"name":"db-password","value":"cG9zdGdyZXM6eGs5bVAycVI=","version":3}
    using var doc = JsonDocument.Parse(body);
    string encodedValue = doc.RootElement.GetProperty("value").GetString()!;

    byte[] decoded = Convert.FromBase64String(encodedValue);
    string secret = Encoding.UTF8.GetString(decoded);

    Console.WriteLine($"Secret: {secret}");
    // Secret: postgres:xk9mP2qR
}
catch (HttpRequestException ex)
{
    Console.Error.WriteLine($"HTTP error: {ex.Message}");
}
catch (FormatException ex)
{
    Console.Error.WriteLine($"Base64 decode failed: {ex.Message}");
}
注意:为网络错误和 FormatException 分别设置 catch 块。 将它们合并在一起会让你难以判断是 API 返回了错误数据还是请求本身失败了。 在生产环境中,捕获到 FormatException 时记录原始 Base64 值 (或至少记录其长度和前 20 个字符)——这会让调试效率大幅提升。

命令行中的 Base64 解码

并非总需要编译项目。dotnet-script 工具和 PowerShell 都能用一行命令处理 Base64 解码。调试时需要快速检查, 这比创建控制台应用更快。

bash
# PowerShell(Windows 内置,Linux/macOS 也可用)
[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("eyJob3N0IjoiMTAuMC4xLjUwIn0="))
# {"host":"10.0.1.50"}

# Linux / macOS 原生 base64 命令
echo "eyJob3N0IjoiMTAuMC4xLjUwIn0=" | base64 --decode
# {"host":"10.0.1.50"}

# macOS 使用 -D 而非 --decode
echo "eyJob3N0IjoiMTAuMC4xLjUwIn0=" | base64 -D

# dotnet-script(安装:dotnet tool install -g dotnet-script)
echo 'Console.WriteLine(System.Text.Encoding.UTF8.GetString(Convert.FromBase64String("eyJob3N0IjoiMTAuMC4xLjUwIn0=")));' | dotnet-script eval

# 解码并用 jq 格式化输出 JSON
echo "eyJob3N0IjoiMTAuMC4xLjUwIiwicG9ydCI6ODQ0M30=" | base64 --decode | jq .

若要将编码字符串直接粘贴到浏览器中, ToolDeck 的 Base64 解码器 无需任何配置即可处理标准和 URL 安全两种变体。

高性能替代方案:System.Buffers.Text.Base64

自 .NET Core 2.1 起可用的 System.Buffers.Text.Base64 类 直接操作原始 UTF-8 字节 span,而非 .NET 字符串。这完全绕过了 字符串到字节的转换开销——无中间字符串分配,无 UTF-16 编码步骤。 当传入数据已经是来自请求体的 ReadOnlySpan<byte> 时, 我会在 ASP.NET Core 中间件中使用它。 为了传给 Convert.FromBase64String() 而创建 string 会毫无意义地使分配翻倍。在 .NET 8 的 BenchmarkDotNet 测试中, 对于 1 KB 以下的载荷,基于 Span 的路径大约快 2-3 倍, 而对于更大的输入,差距还会扩大,因为 GC 压力保持平稳。

C# (.NET 6+)
using System;
using System.Buffers.Text;
using System.Text;

// 模拟来自 HTTP 请求体的原始 UTF-8 字节
byte[] utf8Input = Encoding.UTF8.GetBytes("eyJob3N0IjoiMTAuMC4xLjUwIiwicG9ydCI6ODQ0M30=");

byte[] output = new byte[Base64.GetMaxDecodedFromUtf8Length(utf8Input.Length)];

OperationStatus status = Base64.DecodeFromUtf8(
    utf8Input, output, out int bytesConsumed, out int bytesWritten);

if (status == OperationStatus.Done)
{
    string result = Encoding.UTF8.GetString(output.AsSpan(0, bytesWritten));
    Console.WriteLine(result);
    // {"host":"10.0.1.50","port":8443}
    Console.WriteLine($"Consumed: {bytesConsumed}, Written: {bytesWritten}");
    // Consumed: 44, Written: 31
}
else
{
    Console.WriteLine($"Decode failed: {status}");
    // 可能的值:InvalidData, DestinationTooSmall, NeedMoreData
}

返回类型是 OperationStatus——一个包含四个值的枚举: DoneInvalidDataDestinationTooSmall NeedMoreData。最后一个值对于流式数据的部分解码很有用。 若要实现绝对零分配,将 new byte[] 替换为 ArrayPool<byte>.Shared.Rent()

DecodeFromUtf8InPlace — 覆盖输入缓冲区

C# (.NET 6+)
using System;
using System.Buffers.Text;
using System.Text;

// 输入缓冲区将被解码后的字节覆盖
byte[] data = Encoding.UTF8.GetBytes("c2VydmVyLWNvbmZpZw==");

OperationStatus status = Base64.DecodeFromUtf8InPlace(data, out int bytesWritten);

if (status == OperationStatus.Done)
{
    Console.WriteLine(Encoding.UTF8.GetString(data.AsSpan(0, bytesWritten)));
    // server-config
}
注意:DecodeFromUtf8InPlace 会修改输入缓冲区。如果之后还需要原始 Base64 字符串, 请勿使用它——数组的前 bytesWritten 个字节现在包含解码后的数据, 其余部分为垃圾数据。

需要注意的是:System.Buffers.Text.Base64 不直接处理 URL 安全 Base64。 如果输入使用 -_ 字符(例如 JWT token), 调用这些方法前仍需将其替换为 +/。 基于 Span 的 API 严格遵循 RFC 4648 标准字母表。 没有 DecodeFromUtf8Url 变体——考虑到 Base64url 在现代 API 中的普遍性, 这是一个令人意外的缺口。

带语法高亮的终端输出

Spectre.Console 库提供丰富的终端输出,包括 JSON 高亮—— 在构建解码 Base64 并显示结果的 CLI 工具时非常有用。 使用 dotnet add package Spectre.Console 安装。

C# (.NET 6+)
using System;
using System.Text;
using Spectre.Console;
using Spectre.Console.Json;

string encoded = "eyJob3N0IjoiMTAuMC4xLjUwIiwicG9ydCI6ODQ0MywibWF4Q29ubiI6MTAwfQ==";
byte[] decoded = Convert.FromBase64String(encoded);
string json = Encoding.UTF8.GetString(decoded);

// 在终端中以语法高亮格式输出
AnsiConsole.Write(new JsonText(json));
// 输出彩色 JSON:
// {
//   "host": "10.0.1.50",
//   "port": 8443,
//   "maxConn": 100
// }

这对于从远程服务获取并解码配置的 CLI 工具特别方便。 解码 Base64 配置、反序列化为 JSON、打印高亮输出——几行代码就能搞定。 Spectre.Console 还提供表格渲染、进度条和树状视图, 若需要在终端中展示更复杂的解码数据结构时同样适用。

警告:Spectre.Console 输出包含 ANSI 转义序列。不要将其管道到文件或从 API 返回—— 仅用于终端显示。

使用 CryptoStream 流式处理大型 Base64 文件

File.ReadAllText() 加载 500 MB 的 Base64 文件 再调用 Convert.FromBase64String() 会在堆上分配约 700 MB: 字符串本身(UTF-16,即文件大小的两倍)加上解码后的字节数组。CryptoStream + FromBase64Transform 的组合按块解码,使内存使用量保持恒定。

C# (.NET 6+)
using System.IO;
using System.Security.Cryptography;

string inputPath = "database-export.sql.b64";
string outputPath = "database-export.sql";

using var inputStream = new FileStream(inputPath, FileMode.Open, FileAccess.Read);
using var transform = new FromBase64Transform(FromBase64TransformMode.IgnoreWhiteSpaces);
using var base64Stream = new CryptoStream(inputStream, transform, CryptoStreamMode.Read);
using var outputStream = new FileStream(outputPath, FileMode.Create, FileAccess.Write);

base64Stream.CopyTo(outputStream);

Console.WriteLine($"Decoded {new FileInfo(outputPath).Length} bytes -> {outputPath}");

FromBase64TransformMode.IgnoreWhiteSpaces 标志可以处理 换行包装的 Base64(PEM 文件、电子邮件导出),无需手动清理。 若不使用此标志,输入中的换行符会引发 FormatException。 这是 .NET 中最接近 Java getMimeDecoder() 的等价物—— 解码时会静默跳过空白字符。

CryptoStream 这个名字容易误导——Base64 没有任何加密相关的内容。 Microsoft 将 FromBase64Transform 放在 System.Security.Cryptography 命名空间中, 是因为它实现了 ICryptoTransform 接口, 与 AES 及其他密码变换使用同一接口。这个流本身只是按块将数据传递给任意变换。 可以把它看作一个恰好放错了命名空间的通用流式变换管道。

适用于 ASP.NET Core 场景的异步流式处理

C# (.NET 6+)
using System.IO;
using System.Security.Cryptography;

async Task DecodeStreamAsync(Stream inputStream, string outputPath)
{
    using var transform = new FromBase64Transform(FromBase64TransformMode.IgnoreWhiteSpaces);
    using var base64Stream = new CryptoStream(inputStream, transform, CryptoStreamMode.Read);
    await using var outputStream = new FileStream(
        outputPath, FileMode.Create, FileAccess.Write, FileShare.None,
        bufferSize: 81920, useAsync: true);

    await base64Stream.CopyToAsync(outputStream);
}

// 在端点中使用:
// await DecodeStreamAsync(Request.Body, "uploaded-file.bin");
注意:在 ASP.NET Core 处理器中,将 CopyTo 改为 CopyToAsync。 Kestrel 默认禁止在请求线程上进行同步 I/O,这会抛出 InvalidOperationException

如何在 C# 中解码 Base64 JWT Token 载荷

JWT 由三个以点分隔的 Base64url 编码片段组成,中间片段即为载荷。 无需引入 JWT 库即可解码——以 . 分割,规范化 Base64url 字符, 修复填充,然后调用 Convert.FromBase64String()。 几乎所有人第一次都会在这里踩坑,因为 JWT 使用 -_ 代替 +/,并且省略了 = 填充。

C# (.NET 6+)
using System;
using System.Text;

static string DecodeJwtPayload(string token)
{
    string[] parts = token.Split('.');
    if (parts.Length != 3)
        throw new ArgumentException($"Invalid JWT: expected 3 segments, got {parts.Length}");

    // 取载荷(第二个片段)
    string payload = parts[1];

    // 将 URL 安全字符替换为标准 Base64 字符
    payload = payload.Replace('-', '+').Replace('_', '/');

    // 补齐到 4 的倍数
    switch (payload.Length % 4)
    {
        case 2: payload += "=="; break;
        case 3: payload += "=";  break;
    }

    byte[] bytes = Convert.FromBase64String(payload);
    return Encoding.UTF8.GetString(bytes);
}

// 使用真实形态的 token 进行测试
string token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"
    + ".eyJzdWIiOiJ1c3ItNjcyIiwiaXNzIjoiYXV0aC5leGFtcGxlLmNvbSIsImV4cCI6MTc0MTk1NjgwMCwicm9sZXMiOlsiYWRtaW4iLCJiaWxsaW5nIl19"
    + ".SIGNATURE_PLACEHOLDER";

Console.WriteLine(DecodeJwtPayload(token));
// {"sub":"usr-672","iss":"auth.example.com","exp":1741956800,"roles":["admin","billing"]}

简单说明:这只是读取载荷,不会验证签名。 对于生产环境的认证验证,请使用合适的库,如 Microsoft.IdentityModel.JsonWebTokens。 但对于调试、日志记录和测试断言,这种手动方式完全够用。

将解码后的 JWT 载荷解析为强类型对象

获得 JSON 字符串后,用 System.Text.Json 反序列化, 即可访问各个 claim 而无需字符串操作:

C# (.NET 6+)
using System;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;

record JwtPayload(
    [property: JsonPropertyName("sub")] string Subject,
    [property: JsonPropertyName("iss")] string Issuer,
    [property: JsonPropertyName("exp")] long Expiration,
    [property: JsonPropertyName("roles")] string[] Roles
);

// 在上面的 DecodeJwtPayload() 之后
string json = DecodeJwtPayload(token);
var claims = JsonSerializer.Deserialize<JwtPayload>(json)!;

Console.WriteLine($"User: {claims.Subject}");
Console.WriteLine($"Issuer: {claims.Issuer}");
Console.WriteLine($"Expires: {DateTimeOffset.FromUnixTimeSeconds(claims.Expiration):u}");
Console.WriteLine($"Roles: {string.Join(", ", claims.Roles)}");
// User: usr-672
// Issuer: auth.example.com
// Expires: 2025-03-14 12:00:00Z
// Roles: admin, billing

常见错误

这些错误我在生产 C# 服务中都踩过。前两个是我在代码审查中看到的 大多数 Base64 相关 bug 的根源。每个错误单独来看都显而易见, 但当你深陷一个大功能、Base64 解码只是管道中的一个步骤时, 很容易忽略它们。

忘记去除输入中的空白字符

问题: 从配置文件、环境变量或用户输入读取的 Base64 字符串往往带有尾部换行符。Convert.FromBase64String() 会拒绝 Base64 字母表以外的任何字符,包括 \r\n。

修复: 在解码前调用 .Trim() 或 .Replace() 去除空白字符。

Before · C#
After · C#
// 环境变量带有尾部换行符
string encoded = Environment.GetEnvironmentVariable("DB_PASS_B64")!;
byte[] decoded = Convert.FromBase64String(encoded);
// FormatException: The input is not a valid Base-64 string
string encoded = Environment.GetEnvironmentVariable("DB_PASS_B64")!;
string cleaned = encoded.Replace("\n", "").Replace("\r", "").Trim();
byte[] decoded = Convert.FromBase64String(cleaned);
Console.WriteLine(Encoding.UTF8.GetString(decoded));
对 URL 安全 Base64 使用 Convert.FromBase64String()

问题: JWT token 和某些 API 载荷使用 - 和 _(URL 安全字母表)。标准解码器只接受 + 和 /——遇到第一个 - 字符就会抛出 FormatException。

修复: 解码前将 - 替换为 +,将 _ 替换为 /。同时修复填充。

Before · C#
After · C#
// JWT 载荷——使用 URL 安全 Base64
string payload = "eyJzdWIiOiJ1c3ItNjcyIn0";
byte[] decoded = Convert.FromBase64String(payload);
// FormatException
string payload = "eyJzdWIiOiJ1c3ItNjcyIn0";
payload = payload.Replace('-', '+').Replace('_', '/');
switch (payload.Length % 4)
{
    case 2: payload += "=="; break;
    case 3: payload += "="; break;
}
byte[] decoded = Convert.FromBase64String(payload);
Console.WriteLine(Encoding.UTF8.GetString(decoded));
// {"sub":"usr-672"}
将二进制数据转换为字符串

问题: 对解码后的图片或 protobuf 字节调用 Encoding.UTF8.GetString() 会产生乱码。更糟的是,将该字符串转回字节会悄悄损坏数据,因为无效的 UTF-8 序列会被替换。

修复: 在整个管道中将二进制数据保持为 byte[]。只有在确知内容为文本时才调用 GetString()。

Before · C#
After · C#
byte[] decoded = Convert.FromBase64String(pngBase64);
string imageStr = Encoding.UTF8.GetString(decoded); // 损坏二进制数据
File.WriteAllText("image.png", imageStr); // 文件损坏
byte[] decoded = Convert.FromBase64String(pngBase64);
// 直接写入字节——无需字符串转换
File.WriteAllBytes("image.png", decoded);
未指定编码(依赖平台默认行为)

问题: 某些旧版 .NET Framework API 默认使用系统当前的 ANSI 代码页,不同机器上各不相同。代码页为 1252 的 Windows 服务器和使用 UTF-8 的 Linux 容器会从相同字节产生不同的字符串。

修复: 始终显式指定 Encoding.UTF8。切勿依赖平台默认值。

Before · C#
After · C#
byte[] decoded = Convert.FromBase64String(encoded);
// Encoding.Default 因平台而异
string result = Encoding.Default.GetString(decoded);
byte[] decoded = Convert.FromBase64String(encoded);
string result = Encoding.UTF8.GetString(decoded);
// 在 Windows、Linux、macOS 上结果一致

方法对比

.NET 提供的 Base64 解码方法比大多数开发者意识到的更多。下表涵盖了所有内置选项 以及两种最常见的第三方替代方案。"内存分配"列在高吞吐量服务中最为关键—— 每次调用都分配新 byte[] 的方法会在紧密循环中给 GC 带来压力。

方法
内存分配
错误处理
支持流式
自定义类型
需要安装
Convert.FromBase64String()
新建 byte[]
FormatException
否(.NET Framework 1.1+)
Convert.TryFromBase64String()
调用方 Span
返回 false
否(.NET 5+)
Convert.FromBase64CharArray()
新建 byte[]
FormatException
否(.NET Framework 1.1+)
Base64.DecodeFromUtf8()
调用方 Span
OperationStatus
部分支持
否(.NET Core 2.1+)
Base64.DecodeFromUtf8InPlace()
原地修改
OperationStatus
否(.NET Core 2.1+)
CryptoStream + FromBase64Transform
流式缓冲区
异常
否(.NET Framework 2.0+)
BouncyCastle Base64
新建 byte[]
异常
是(NuGet)

日常工作使用 Convert.FromBase64String()。验证用户输入的热路径使用 TryFromBase64String()。处理原始请求字节的 ASP.NET Core 中间件使用 Base64.DecodeFromUtf8()。大文件使用 CryptoStream + FromBase64Transform。只有在 BouncyCastle 已因其他加密操作 而存在于依赖树中时,才有理由使用它。

若要在不编译任何代码的情况下快速验证, 在线 Base64 解码器 比编写一次性控制台应用更快。

常见问题

如何在 C# 中将 Base64 字符串解码为文本?

调用 Convert.FromBase64String() 获取字节数组,然后传入 Encoding.UTF8.GetString()。编码方式必须与原始编码一致——UTF-8 是几乎所有现代系统的安全默认值。如果输入可能包含空白字符或换行符,请在解码前调用 .Trim() 或手动去除。

C# (.NET 6+)
using System;
using System.Text;

string encoded = "cG9zdGdyZXM6eGs5bVAycVI=";
byte[] bytes = Convert.FromBase64String(encoded);
string result = Encoding.UTF8.GetString(bytes);
Console.WriteLine(result);
// postgres:xk9mP2qR

Convert.FromBase64String() 和 Convert.TryFromBase64String() 有何区别?

FromBase64String() 在输入无效时抛出 FormatException。TryFromBase64String() 返回 bool 并将结果写入调用方提供的 Span<byte>,适用于希望避免异常开销的热路径。TryFromBase64String() 需要 .NET 5 或更高版本。

C# (.NET 6+)
using System;

string input = "maybe-not-valid-base64!!";
Span<byte> buffer = stackalloc byte[256];

if (Convert.TryFromBase64String(input, buffer, out int written))
    Console.WriteLine($"Decoded {written} bytes");
else
    Console.WriteLine("Invalid Base64 input");

如何在 C# 中解码 Base64url 编码的 JWT 载荷?

以点号分割 token,取第二个片段,将 - 替换为 +,将 _ 替换为 /,用 = 补齐到 4 的倍数,然后调用 Convert.FromBase64String()。JWT token 使用 URL 安全的 Base64 字母表,而 .NET 的标准解码器无法直接处理。

C# (.NET 6+)
string token = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c3ItNjcyIn0.SIG";
string payload = token.Split('.')[1];
payload = payload.Replace('-', '+').Replace('_', '/');
switch (payload.Length % 4)
{
    case 2: payload += "=="; break;
    case 3: payload += "="; break;
}
byte[] bytes = Convert.FromBase64String(payload);
Console.WriteLine(Encoding.UTF8.GetString(bytes));
// {"sub":"usr-672"}

在 C# 中解码 Base64 时如何选择正确的编码?

默认使用 Encoding.UTF8——它可处理 ASCII 和多字节 Unicode 字符。仅在确认数据是纯 7 位 ASCII 时才使用 Encoding.ASCII。仅在原始数据以 UTF-16 编码时才使用 Encoding.Unicode(在 .NET 中为 UTF-16LE),这种情况有时出现在 Windows 内部字符串和 PowerShell 导出中。

Convert.FromBase64String() 为何抛出 FormatException?

三个常见原因:输入包含空白字符或换行符(解码前需去除)、输入使用了 URL 安全字符(如 - 和 _,需分别替换为 + 和 /)、或者填充不正确(填充后总长度必须是 4 的倍数)。与 Java 不同,.NET 没有内置的 MIME 解码器来容忍空白字符——你必须自行清理输入,或使用带 IgnoreWhiteSpaces 模式的 CryptoStream + FromBase64Transform。

C# (.NET 6+)
// 修复空白字符
string cleaned = rawInput.Replace("\n", "").Replace("\r", "").Trim();
byte[] decoded = Convert.FromBase64String(cleaned);

在 C# 中可以流式解码大型 Base64 数据吗?

可以。使用 System.Security.Cryptography 中的 CryptoStream 配合 FromBase64Transform。读取时按块解码,无论文件大小如何,内存占用保持不变。如果输入包含换行符,传入 FromBase64TransformMode.IgnoreWhiteSpaces。在 .NET 6+ 中,也可以使用 IAsyncEnumerable 模式配合 Base64.DecodeFromUtf8() 进行手动分块处理,但 CryptoStream 对于文件到文件的解码更简洁。

C# (.NET 6+)
using System.IO;
using System.Security.Cryptography;

using var input = File.OpenRead("payload.b64");
using var transform = new FromBase64Transform();
using var cryptoStream = new CryptoStream(input, transform, CryptoStreamMode.Read);
using var output = File.Create("payload.bin");
cryptoStream.CopyTo(output);

相关工具

  • Base64 Encoder — 在浏览器中将文本或二进制数据编码为 Base64,可用于生成测试数据粘贴到 C# 单元测试中。
  • JWT Decoder — 一次性解码并检查所有三个 JWT 片段,支持逐字段载荷检查——当你只需读取 token 时,比编写 C# 辅助方法更快。
  • URL Decoder — 对 URL 编码字符串进行百分号解码,适用于 API 响应中混合了 Base64url 数据与百分号编码查询参数的场景。
  • JSON Formatter — 解码 Base64 JWT 载荷或 API 配置后,将 JSON 粘贴到这里进行格式化并验证结构。
AP
Alexei PetrovGame Developer & Unity Engineer

Alexei is a game developer who has shipped multiple titles using Unity and C#. He focuses on gameplay systems, runtime performance, and the serialisation and data-management patterns unique to game development. He writes about Unity scripting, C# async/await in game contexts, asset serialisation, binary data handling, and the intersection of game engineering and general software craftsmanship.

ER
Emma Richardson技术审阅者

Emma is a .NET developer and cloud engineer who builds production APIs and backend services with ASP.NET Core and Azure. She has worked on everything from microservice migrations to real-time SignalR applications. She writes about C# language features, the System.Text.Json and Newtonsoft.Json ecosystems, Azure integrations, and the architectural patterns that make .NET services scalable and maintainable.