HttpUtility.ParseQueryString 還原字串中文編碼問題
2 |
在 .NET 裡要解析 URL 參數字串(QueryString,例如: a=1234&b=ABCD),自己拆字串就遜掉了,呼叫 HttpUtility.ParseQueryString() 才是王道,這是我很多年前就學到的知識。
最近再有個新發現,ParseQueryString() 所傳回的結果表面上是個 NameValueCollection,但骨子裡則是內部型別 – HttpValueCollection,它有個特異功能,ToString() 被覆寫成可將 Name/Value 再組合還原成 QueryString,所以我們可以用它解析 QueryString,增刪修改參數再 ToString() 轉回 QueryString,十分方便。
不過,試著試著踩到一顆地雷,它的 ToString() 處理中文編碼有問題:
static void TestParseQueryString()
{
var urlQuery = "a=123&b=%E4%B8%AD%E6%96%87";
var collection = HttpUtility.ParseQueryString(urlQuery);
Console.WriteLine($"Query: {urlQuery}");
Console.WriteLine($"ParseQueryString: a={collection["a"]},b={collection["b"]}");
Console.WriteLine($"ToString: {collection.ToString()}");
}
實測解析內含 UrlEncode 中文參數再還原,可發現 HttpValueCollection.ToString() 傳回的不是 UTF-8 編碼 UrlEncode,而是過時 %uxxxx 格式 ! (延伸閱讀:【茶包射手日記】勿用 UrlEncodeUnicode 與 escape )
HttpValueCollection 原始碼也證實這點,註解提到是為了向前相容才繼續使用被標為過時(Obsolete)的 UrlEncodeUnicode 方法,而程式碼埋了一段偵測 AppSettings.DontUsePercentUUrlEncoding 設定改用 UrlEncode 的邏輯:
// HttpValueCollection used to call UrlEncodeUnicode in its ToString method, so we should continue to
// do so for back-compat. The result of ToString is not used to make a security decision, so this
// code path is "safe".
internal static string UrlEncodeForToString(string input) {
if (AppSettings.DontUsePercentUUrlEncoding) {
// DevDiv #762975: <form action> and other similar URLs are mangled since we use non-standard %uXXXX encoding.
// We need to use standard UTF8 encoding for modern browsers to understand the URLs.
return HttpUtility.UrlEncode(input);
}
else {
#pragma warning disable 618 // [Obsolete]
return HttpUtility.UrlEncodeUnicode(input);
#pragma warning restore 618
}
}
換言之,在 config 加入以下設定即可讓 HttpValueCollection 改用 UrlEncode:
<appSettings>
<add key="aspnet:DontUsePercentUUrlEncoding" value="true" />
</appSettings>
實測成功!
不過,若是在共用函式或公用程式庫使用 HttpValueCollection,要求開發者修改 config 配合太擾民。故還有另一種解法,先用 ParseQueryString() 解讀為 NameValueCollection 後用 Uri.EscapeDataString() 以標準 UTF-8 編碼重新組裝:
static void TestParseQueryString()
{
var urlQuery = "a=123&b=%E4%B8%AD%E6%96%87";
var collection = HttpUtility.ParseQueryString(urlQuery);
Console.WriteLine($"Query: {urlQuery}");
Console.WriteLine($"ParseQueryString: a={collection["a"]},b={collection["b"]}");
//使用EscapeDataString重新編碼
var fixedResult = string.Join("&",
collection.AllKeys.Select(key => key + "=" + Uri.EscapeDataString(collection[key])).ToArray());
Console.WriteLine($"ToString: {fixedResult}");
}
這樣也能修正問題,報告完畢。
Comments
# by billhong
var fixedResult = Uri.EscapeUriString(HttpUtility.UrlDecode(collection.ToString())); 這個解法似乎沒有考慮 query parameter 若有包含需要被 url encode 的符號這種 cases 雖然解決了 中文字編碼的問題,但原本沒問題的部分卻出錯了 e.g. 原始 url 是 "a=%23123" (url 的 query parameter 代入 a=#123 其中 # 是需要被 url encode 的部分) 執行 var collection = HttpUtility.ParseQueryString("a=%23123"), collection 有正確的 url decode 得到 key="a" 和 value="#123" 但接下來重組回 url => Uri.EscapeUriString(HttpUtility.UrlDecode(collection.ToString())) 會得到 "a=#123" 不是原本正確的 url encode 的 "a=%23123"
# by Jeffrey
to billhong, 同意,應該用 Uri.EscapeDataString() 才夠嚴謹,已修改程式寫法,感謝指正。