問題發生點在於我有個 Controller 裡的 Action 如以下
[HttpGet("[controller]/[action]/{id:int}/{myUrl?}")]
public async Task<IActionResult> Handler(long id, string myUrl)
{
/* 略 */
return Content("OK");
}
使用的 ASP.NET Core 版本為 2.2,首先我在本地端伺服器 Kestrel 執行網址如下
https://localhost:5001/Test/Handler/100/https%3A%2F%2Flocalhost%3A5001%2FSomePath%2F20
參數 id 使用 100
參數 myUrl 使用 UrlEncode 過的網址(https://localhost:5001/SomePath/20)
測時結果正常可呼叫到我預期的 Handler Action,接著我把該程式發佈到 IIS 上後,在執行一次同樣的路徑,卻跳出了 404 找不到該路徑的錯誤頁面如下…
經過一段時間在網路上亂試亂寫的結果後,才知道原來 IIS 會自動先將網址做 UrlDecode 的動作,造成 Client 端向 Server 端要求的路徑在 Route 裡匹配不到, 則我以為 IIS 端收到交給應用程式處理的網址為
https://mydomain.com/Test/Handler/5/https%3A%2F%2Fmydomain.com%2FSomePath%2F20
但實際上 IIS 端收到交給應用程式處理的網址確是
https://mydomain.com/Test/Handler/5/https://mydomain.com/SomePath/20
原定的 Handler Action 裡只有處理兩個參數而已,而先被 UrlDecode 過的網址要求已經不止兩個參數了…看了很多文章都是設定 web.config 檔裡的 rewrite 區段,由於有點複雜且只適用於 IIS,所以個人傾向不採用選擇了其它解決方式,而產生了這篇文章。
採取的解決方式為只要將路由設定裡的 {myUrl?}
更改為 {**myUrl}
即 catch-all 將剩下的路徑視為單一個參數,但在微軟官方裡的文章有提到加一個星號或兩個星號都可以,差異是在於單星號會將參數裡的斜線(/)編碼,然後雙星號只有在 ASP.NET Core 2.2 版本之後才開始支援。
初步大致上了解兩者使用情境的差別,但還是想知道兩者在系統實際運行上表現的方式,所以做了一些簡單的測試,讓之後忘記時還可以回來參考。
測試 Action 如下,然後分別在 Kestrel 和 IIS 兩邊進行觀察
單星號(*) Action
[HttpGet("[controller]/[action]/{id:int}/{*myUrl}")]
public async Task<IActionResult> Handler(long id, string myUrl)
{
/* 略 */
return Content("OK");
}
雙星號(**) Action
[HttpGet("[controller]/[action]/{id:int}/{**myUrl}")]
public async Task<IActionResult> Handler2(long id, string myUrl)
{
/* 略 */
return Content("OK");
}
1. 比較使用 Url.Action() 生成的網址
Kestrel (單星號*):
Input
1. Url.Action(nameof(Handler), "Test", new { id = 5, myUrl="https%3A%2F%2Fmydomain.com%2FSomePath%2F20"}, Request.Scheme, Request.Host.Value)
2. Url.Action(nameof(Handler), "Test", new { id = 5, myUrl="https://mydomain.com/SomePath/20"}, Request.Scheme, Request.Host.Value)
Output
1. https://localhost:5001/Test/Handler/5?myUrl=https%253A%252F%252Fmydomain.com%252FSomePath%252F20
2. https://localhost:5001/Test/Handler/5?myUrl=https%3A%2F%2Fmydomain.com%2FSomePath%2F20
IIS (單星號*):
Input
1. Url.Action(nameof(Handler), "Test", new { id = 5, myUrl="https%3A%2F%2Fmydomain.com%2FSomePath%2F20"}, Request.Scheme, Request.Host.Value)
2. Url.Action(nameof(Handler), "Test", new { id = 5, myUrl="https://mydomain.com/SomePath/20"}, Request.Scheme, Request.Host.Value)
Output
1. https://mydomain.com/Test/Handler/5?myUrl=https%253A%252F%252Fmydomain.com%252FSomePath%252F20
2. https://mydomain.com/Test/Handler/5?myUrl=https%3A%2F%2Fmydomain.com%2FSomePath%2F20
Kestrel (雙星號**):
Input
1. Url.Action(nameof(Handler2), "Test", new { id = 5, myUrl="https%3A%2F%2Fmydomain.com%2FSomePath%2F20"}, Request.Scheme, Request.Host.Value)
2. Url.Action(nameof(Handler2), "Test", new { id = 5, myUrl="https://mydomain.com/SomePath/20"}, Request.Scheme, Request.Host.Value)
Output
1. https://localhost:5001/Test/Handler2/5?myUrl=https%253A%252F%252Fmydomain.com%252FSomePath%252F20
2. https://localhost:5001/Test/Handler2/5?myUrl=https%3A%2F%2Fmydomain.com%2FSomePath%2F20
IIS (雙星號**):
Input
1. Url.Action(nameof(Handler2), "Test", new { id = 5, myUrl="https%3A%2F%2Fmydomain.com%2FSomePath%2F20"}, Request.Scheme, Request.Host.Value)
2. Url.Action(nameof(Handler2), "Test", new { id = 5, myUrl="https://mydomain.com/SomePath/20"}, Request.Scheme, Request.Host.Value)
Output
1. https://mydomain.com/Test/Handler2/5?myUrl=https%253A%252F%252Fmydomain.com%252FSomePath%252F20
2. https://mydomain.com/Test/Handler2/5?myUrl=https%3A%2F%2Fmydomain.com%2FSomePath%2F20
可以發現生成的網址在兩個不同的平台上並無差異,值得注意的是使用 Url.Action()
會自動幫你的參數 UrlEncode 一次,所以就不用再自己做 UrlEncode 處理,否則某些場合會造成 Double UrlEncode 的問題。
2. 比較參數含有網址字串的要求
Kestrel (單星號*):
Input
1. https://localhost:5001/Test/Handler/5/https%3A%2F%2Fmydomain.com%2FSomePath%2F20
2. https://localhost:5001/Test/Handler/5/https://mydomain.com/SomePath/20
Output
1. https://localhost:5001/Test/Handler/5/https%3A%2F%2Fmydomain.com%2FSomePath%2F20
2. https://localhost:5001/Test/Handler/5/https://mydomain.com/SomePath/20
IIS (單星號*):
Input
1. https://mydomain.com/Test/Handler/5/https%3A%2F%2Fmydomain.com%2FSomePath%2F20
2. https://mydomain.com/Test/Handler/5/https://mydomain.com/SomePath/20
Output
1. https://mydomain.com/Test/Handler/5/https://mydomain.com/SomePath/20
2. https://mydomain.com/Test/Handler/5/https://mydomain.com/SomePath/20
Kestrel (雙星號**):
Input
1. https://localhost:5001/Test/Handler2/5/https%3A%2F%2Fmydomain.com%2FSomePath%2F20
2. https://localhost:5001/Test/Handler2/5/https://mydomain.com/SomePath/20
Output
1. https://localhost:5001/Test/Handler2/5/https%3A%2F%2Fmydomain.com%2FSomePath%2F20
2. https://localhost:5001/Test/Handler2/5/https://mydomain.com/SomePath/20
IIS (雙星號**):
Input
1. https://mydomain.com/Test/Handler2/5/https%3A%2F%2Fmydomain.com%2FSomePath%2F20
2. https://mydomain.com/Test/Handler2/5/https://mydomain.com/SomePath/20
Output
1. https://mydomain.com/Test/Handler2/5/https://mydomain.com/SomePath/20
2. https://mydomain.com/Test/Handler2/5/https://mydomain.com/SomePath/20
可以發現 IIS 在接收要求的參數時,確實會先將網址參數做 UrlDecode,而 Kestrel 則保留原始參數的編碼直接 Pass 到 Action 裡,但是不知道什麼原因,不管使用單星號或雙星號的方式所得到的測試結果並無差異,不像微軟文章所說的產生連結時雙星號會保留路徑(/)分隔符號字元,所以目前的認知是都先採用雙星號(**)為主。
總結:
1. 使用 Url.Action() 生成的網址在兩個不同的平台上沒有差異,然後 Url.Action() 會自動幫你的參數部分先 UrlEncode 一次,所以可以省去自己做 UrlEncode 處理。
2. IIS 在接收要求的參數時,會先將網址參數做 UrlDecode,而 Kestrel 則保留原始參數的編碼直接 Pass 到 Action 裡。
3. 自定路由 Template 的參數前綴符號不管使用單一星號(*)或雙星號(**)所得到的測試結果皆相同。
補充:
在 IIS 上如果要使用含有 UrlEncode 過的網址參數呼叫 Action 時,接到的參數會少一個 /
,例如傳 https://linmasaki09.blogspot.com/https%3A%2F%2Fwww.abc.com.tw,則接到的參數值會變為 https://linmasaki09.blogspot.com/https:/www.abc.com.tw,目前的解法是使用 IIS URL Rewrite 模組(沒有需先安裝)在 web.config
裡加上以下設定即可解決
<!--xml version="1.0" encoding="utf-8"?-->
<configuration>
<location path="." inheritinchildapplications="false">
<system.webserver>
.......... Remove other settings for brevity ..........
<security>
<requestfiltering allowdoubleescaping="true"> // 1. 在 requestFiltering 裡加上 allowDoubleEscaping="true"
<!-- 4MB -->
<requestlimits maxallowedcontentlength="4194304">
</requestlimits></requestfiltering>
</security>
<rewrite> // 2. 新增下面的 Rewrite Rule
<rules>
<rule name="Disable IIS decoding the URL as a parameter." stopprocessing="true">
<match url="/(.+http.+)">
<action type="Rewrite" url="{UNENCODED_URL}">
</action></match></rule>
</rules>
</rewrite>
.......... Remove other settings for brevity ..........
</system.webserver>
</location>
</configuration>