2019年8月8日 星期四

比較 .Net Core 2 路由參數 catch-all 單星號(*)及雙星號 (**) 在 IIS 及 Kestrel 上的表現

最近在測試 .Net Core 2 的 API 路徑如含有 UrlEncode 過的特殊字元當參數時,在本地端使用 Kestrel 測試正常,但是丟到 IIS 上時卻頻頻發生 404 找不到網頁的問題,折騰一番後總算找到了問題所在,所以乾脆在這邊記錄一下心得及路由參數 catch-all 單星號(*)及雙星號 (**) 的差異當做筆記。


問題發生點在於我有個 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 找不到該路徑的錯誤頁面如下…

Image

經過一段時間在網路上亂試亂寫的結果後,才知道原來 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>




參考資料

[MSDN] Routing in ASP.NET Core

訪客統計

103196