先说结论:C# 和 Ja va 算出来的 Base64 结果不一样,这事儿其实跟 Base64 编码本身没啥关系。问题出在它们哥俩在处理 JSON 字符串时,对斜杠符 `/` 的态度截然不同。Ja va 的 `JSONObject.toString()` 默认会把它转义成 `\/`,而 .NET 的 `System.Text.Json.JsonSerializer` 则选择无视它。输入流的字节都不同了,最后 Base64 出来的结果能一样才怪。
在折腾 JWT、API 签名这些跨语言对接的场景里,保证 Base64 结果一致是基本要求。表面上看,两边步骤一样:都是先 UTF-8 编码,再 Base64 一把梭。但深究起来,问题的根源其实在更早的环节——也就是生成 JSON 字符串的那一刻。简单说,就是 `claimSet.toString()` 和 `JsonSerializer.Serialize()` 这两个方法的行为不一致,才埋下了这颗雷。
问题的根源到底在哪?
为什么会出现这种情况?我们来拆解一下两个语言的具体表现:
Ja va(以 org.json.JSONObject 为例):
它的 `toString()` 方法比较“保守”,会自动把 U+002F (`/`) 这个字符转义成 `\/`。所以,最终生成的 JSON 字符串里,`partnerUrl` 的值会变成 `"https:\/\/test.com\/testapply\/abc\/signup"`。 因为多了个反斜杠(ASCII 0x5C),字节数组就变长了,最后 Base64 编码的结果自然会更长,甚至末尾可能多出一个 `=` 来补位。C#(以 System.Text.Json 为例):
.NET 这边则比较“现代”,默认情况下,`JsonSerializer.Serialize()` 是**不会**对 `/` 进行转义。它生成的字符串就是原汁原味的 `"https://test.com/testapply/abc/signup"`,更紧凑,也更符合 RFC 7159 的推荐实践。
验证起来也很简单,把两边输出的 Base64 字符串解码后一对比,问题就一目了然了:
# Ja va 输出的 Base64 解码后(字符串里还带着转义符)
echo "eyJwYXJ0bmVyVXJsIjoiaHR0cHM6XC9cL3Rlc3QuY29tXC90ZXN0YXBwbHlcL2FiY1wvc2lnbnVwIn0=" | base64 -d
# 结果:{"partnerUrl":"https:\/\/test.com\/testapply\/abc\/signup"}
# C# 输出的 Base64 解码后(干干净净,没有转义)
echo "eyJwYXJ0bmVyVXJsIjoiaHR0cHM6Ly90ZXN0LmNvbS90ZXN0YXBwbHkvYWJjL3NpZ251cCJ9" | base64 -d
# 结果:{"partnerUrl":"https://test.com/testapply/abc/signup"}
怎么解决?有两种思路
既然知道了病根,那对症下药就简单了。主要是两个方向,看你需要对接哪一方。
方案一:让 C# 去迁就 Ja va(适合对接老旧的 Ja va 系统)
如果你的上游 Ja va 服务无法修改,那我们只能在 .NET 这边想办法,强制它也对 `/` 进行转义。
注意,`JsonSerializerOptions` 里的 `EscapeHtml` 选项只管 `<`, `>`, `&` 这些,对 `/` 无效。正确的做法是配置 `Encoder`:
using System.Text.Encodings.Web;
using System.Text.Json;
var options = new JsonSerializerOptions
{
Encoder = Ja vaScriptEncoder.Create(
UnicodeRanges.All, // 覆盖所有字符
new[] { '/' } // 明确要求转义 '/'
),
WriteIndented = false
};
var claimSets = new Dictionary
{
{ "partnerUrl", "https://test.com/testapply/abc/signup" }
};
string claimSetsJson = JsonSerializer.Serialize(claimSets, options);
byte[] bytes = Encoding.UTF8.GetBytes(claimSetsJson);
string base64 = Convert.ToBase64String(bytes);
// 输出就会跟 Ja va 那边一样了
⚠️ 一个小提醒:`Ja vaScriptEncoder.Create(...)` 里只需要传入 `new[] { '/' }` 就行。这个 API 在 .NET 6 及以上版本都支持,如果你的项目版本比较老,可能需要自定义一个 `TextEncoder`。
方案二:修改 Ja va 端(更推荐,符合现代规范)
长远来看,我更推荐从 Ja va 这边下手,把 `/` 的自动转义关掉。毕竟不转义是更现代、更主流的做法,生成的 JSON 体积也更小。
如果你用的是 `org.json` 库,2023年10月以后的版本(比如 20231013)已经支持关闭转义了:
// Ja va 2023+ 版本支持
JSONObject claimSet = new JSONObject();
claimSet.put("partnerUrl", "https://test.com/testapply/abc/signup");
String claimSetJson = claimSet.toString(0, 0, false); // 第三个参数 false 就是关闭转义
当然,更彻底的办法是换用 Jackson 或 Gson 这类更可控的库,它们默认就不会转义 `/`:
ObjectMapper mapper = new ObjectMapper(); String json = mapper.writeValueAsString(claimSet); // Jackson 默认行为
几点关键总结
- ✅ Base64 编码本身没有语言差异,差异永远来自于**输入给它的字节流是否一致**。
- ✅ JSON 序列化器对 `/` 的默认转义策略,是个常见的“坑”,跨语言对接时必须显式对齐。
- ✅ 生产环境的最佳实践是:**通信双方都关闭对 `/` 的转义**。这既符合 RFC 规范,又能让数据包更小,解析起来也更高效。
- ❌ 千万别凭肉眼觉得两个字符串“看起来一样”就完事了,必须逐字节对比,或者解码后验证,这才是最稳妥的。
通过统一序列化配置,让 C# 和 Ja va 生成完全一致的 Base64 字符串,才能确保跨语言签名、JWT 验证这些场景的可靠性,避免在联调时踩坑。
