在HTTP的語意中,重定向一般指的是伺服器端通過返回一個狀態碼為3XX的響應促使使用者端像另一個地址再次發起請求,本章將此稱為「使用者端重定向「。既然有使用者端重定向,自然就有伺服器端重定向,本章所謂的伺服器端重定向指的是在伺服器端通過改變請求路徑將請求導向另一個終結點。ASP.NET下的重定向是通過RewriteMiddleware中介軟體實現的。(本文提供的範例演示已經同步到《ASP.NET Core 6框架揭祕-範例演示版》)
[S2501]使用者端重定向 (原始碼)
[S2502]伺服器端重定向 (原始碼)
[S2503]採用IIS重寫規則實現重定向(原始碼)
[S2504]採用Apache重寫規則實現重定向(原始碼)
[S2505]基於HTTPS終結點的重定向(原始碼)
我們可以為RewriteMiddleware中介軟體定義使用者端重定向規則使之返回一個Location報頭指向重定向地址的3XX響應。使用者端(比如瀏覽器)在接收到這樣的響應後會根據狀態碼約定的語意向重定向地址重新發起請求,我們將這種由使用者端對新的地址重新請求的方式稱為「使用者端重定向」。
下面演示的這個例子會將請求路徑以「foo/**」為字首的請求重定向到新的路徑「/bar/**」。如程式碼片段所示,我們通過呼叫UseRewriter擴充套件方法註冊了RewriteMiddleware中介軟體,該方法會將對應的RewriteOptions設定選項作為引數。我們直接呼叫建構函式建立的這個RewriteOptions物件,並呼叫其AddRedirect擴充套件方法新增了一個重定向規則,該方法定義了兩個引數,前者(「^/foo/(.*)」)代表參與重定向的原始路徑模式(正規表示式),後者(「baz/$1」)表示重定向目標地址模板,預留位置「$1」表示在進行正則匹配時產生的首段捕獲內容(字首「foo/」後面的部分)。請求的URL會作為響應的內容。
using Microsoft.AspNetCore.Rewrite; var app = WebApplication.Create(); var options = new RewriteOptions().AddRedirect("^foo/(.*)", "bar/$1"); app.UseRewriter(options); app.MapGet("/{**foobar}", (HttpRequest request) =>$"{request.Scheme}://{request.Host}{request.PathBase}{request.Path}"); app.Run();
演示程式註冊了一個採用「/{**foobar}」路由模板的終結點,請求URL直接作為該終結點的響應內容。演示程式啟動之後,所有路徑以「/foo」為字首的請求都會自動重定向到以「/bar」為字首的地址。如果請求路徑被設定為「/foo/abc/123」,最終將會被重定向到圖1所示的「/bar/abc/123」路徑下。
圖1 使用者端重定向
整個過程涉及HTTP報文交換更能體現使用者端重定向的本質。如下所示的是整個過程涉及的兩次報文交換,我們可以看出伺服器端第一次返回的是狀態碼為302的響應,根據對映規則生成的重定向地址體現在Location報頭上。
GET http://localhost:5000/foo/abc/123 HTTP/1.1 Host: localhost:5000 HTTP/1.1 302 Found Content-Length: 0 Date: Wed, 22 Sep 2021 13:34:17 GMT Server: Kestrel Location: /bar/abc/123
GET http://localhost:5000/bar/abc/123 HTTP/1.1 Host: localhost:5000 HTTP/1.1 200 OK Date: Wed, 22 Sep 2021 13:34:17 GMT Server: Kestrel Content-Length: 33 http://localhost:5000/bar/abc/123
伺服器端重定向會在伺服器端通過重寫請求路徑的方式將請求重定向到新的終結點。對於前面演示的程式來說,我們只需要對它做簡單的修改就能切換到伺服器端重定向。如下面的程式碼片段所示,在RewriteOptions物件被建立後,我們呼叫它的另一個AddRewrite擴充套件方法註冊了一條伺服器端重定向(URL重寫)規則,原始請求路徑的正規表示式和重定向路徑均保持不變。
using Microsoft.AspNetCore.Rewrite; var app = WebApplication.Create(); var options = new RewriteOptions(). .AddRewrite(regex: "^foo/(.*)", replacement: "bar/$1", skipRemainingRules: true); app.UseRewriter(options); app.MapGet("/{**foobar}", (HttpRequest request) => $"{request.Scheme}://{request.Host}{request.PathBase}{request.Path}"); app.Run();
圖2 伺服器端重定向
重定向是絕大部分Web伺服器(比如IIS、Apache和Nginx等)都會提供的功能,但是不同的伺服器型別針對重定向規則具有不同的定義方式。IIS中的重定向被稱為「URL重寫」,具體的URL重寫規則採用XML格式進行定義,RewriteMiddleware中介軟體對它提供了原生的支援。我們將URL重寫規則以如下的方式定義在建立的rewrite.xml檔案中,並將該檔案儲存在演示專案的根目錄下。
<rewrite> <rules> <rule name="foo"> <match url="^foo/(.*)" /> <action type="Redirect" url="baz/{R:1}" /> </rule> <rule name="bar"> <match url="^bar/(.*)" /> <action type="Rewrite" url="baz/{R:1}" /> </rule> </rules> </rewrite>
如上所示的XML檔案定義了兩條指向目標地址「baz/{R:1}」的規則,這裡的預留位置「{R:1}」和前面定義的「$1」一樣,都表示針對初始請求路徑進行正則匹配時得到的第一段捕獲內容。兩條規則用來匹配原始路徑的正規表示式分別定義為「^foo/(.*)」和「^bar/(.*)」。它們採用的Action型別也不相同,前者為「Redirect」,表示使用者端重定向;後者為「Rewrite」,表示伺服器端重定向。
為了將採用XML檔案定義的IIS重定向規則應用到演示程式中,我們對演示程式如下的修改。如程式碼片段所示,在RewriteOptions物件被建立出來後,我們呼叫了它的AddIISUrlRewrite擴充套件方法新增了IIS URL重寫規則,該方法的兩個引數分別表示用來讀取規則檔案的IFileProvider物件和規則檔案針對該物件的路徑。由於規則檔案儲存與專案根目錄下,這也是ASP.NET應用「內容根目錄」所在的位置,所以我們可以使用內容根目錄對應的IFileProvider物件。
using Microsoft.AspNetCore.Rewrite; var app = WebApplication.Create(); var options = new RewriteOptions().AddIISUrlRewrite(fileProvider: app.Environment.ContentRootFileProvider, filePath: "rewrite.xml"); app.UseRewriter(options); app.MapGet("/{**foobar}", (HttpRequest request) =>$"{request.Scheme}://{request.Host}{request.PathBase}{request.Path}"); app.Run();
改動的程式啟動之後,我們針對新增的兩條重定向規則傳送了對應的請求,它們採用的請求路徑分別為「/foo/abc/123」和「/bar/abc/123」。從圖3所示的輸出可以看出,這兩個請求均被重定向到相同的目標路徑「/baz/abc/123」。
圖3 IIS重定向規則
由於傳送的兩個請求分別採用使用者端和伺服器端重定向方式導向新的地址,所以瀏覽器針對前者顯示的是重定向後的地址,對於後者則顯示原始的地址。整個過程涉及到的如下三次報文互動更能說明兩種重定向方式的差異,從報文內容我們可以進一步看出第一次採用的是響應狀態碼為301的永久重定向。
GET http://localhost:5000/foo/abc/123 HTTP/1.1 Host: localhost:5000 HTTP/1.1 301 Moved Permanently Content-Length: 0 Date: Wed, 22 Sep 2021 23:26:02 GMT Server: Kestrel Location: /baz/abc/123
GET http://localhost:5000/baz/abc/123 HTTP/1.1 Host: localhost:5000 HTTP/1.1 200 OK Date: Wed, 22 Sep 2021 23:26:02 GMT Server: Kestrel Content-Length: 33 http://localhost:5000/baz/abc/123
GET http://localhost:5000/bar/abc/123 HTTP/1.1 Host: localhost:5000 HTTP/1.1 200 OK Date: Wed, 22 Sep 2021 23:26:26 GMT Server: Kestrel Content-Length: 33 http://localhost:5000/baz/abc/123
上面我們演示了RewriteMiddleware中介軟體針對IIS重定向規則的支援,實際上該中介軟體還支援Apache的重定向模組mod_rewriter所採用的重定向規則定義形式,我們照例來做一個簡單的演示。我們在專案根目錄下新增了一個名為rewrite.config的組態檔,並在其中定義瞭如下兩條重定向規則。
RewriteRule ^/foo/(.*) /baz/$1 [R=307] RewriteRule ^/bar/(.*) - [F]
上面第一條規則利用R這個Flag將路徑與正規表示式「^/foo/(.*)」相匹配的請求以重定向到新的路徑「/baz/$1」,具體採用的是針對狀態碼307的臨時使用者端重定向。對於其路徑與正規表示式「^/bar/(.*)」相匹配的請求,我們將它視為未經授權授權的請求,所以對應的規則採用F(Forbidden)這個Flag。為了讓演示程式採用上述這個組態檔定義的Apache重定向規則,我們只需要按照如下的方式呼叫RewriteOptions 物件的AddApacheModRewrite擴充套件方法就可以了。
using Microsoft.AspNetCore.Rewrite; var app = WebApplication.Create(); var options = new RewriteOptions().AddApacheModRewrite(fileProvider: app.Environment.ContentRootFileProvider, filePath: "rewrite.config"); app.UseRewriter(options); app.MapGet("/{**foobar}", (HttpRequest request) =>$"{request.Scheme}://{request.Host}{request.PathBase}{request.Path}"); app.Run();
改動的程式啟動之後,我們針對新增的兩條重定向規則傳送了對應的請求,它們採用的請求路徑分別為「/foo/abc/123」和「/bar/abc/123」。從圖4所示的輸出可以看出,第一個請求均被重定向到相同的目標路徑「/baz/abc/123」,第二個請求返回一個狀態碼為403的響應。
圖4Apache mod_rewrite重定向規則
如下所示的是整個過程涉及到的三次報文交換。我們可以看出第一次請求得到的響應狀態碼正式我們在規則中顯式設定的307。第二個請求由於被視為許可權不足,伺服器端直接返回一個狀態為「403 Forbidden」的響應。
GET http://localhost:5000/foo/abc/123 HTTP/1.1 Host: localhost:5000 HTTP/1.1 307 Temporary Redirect Content-Length: 0 Date: Wed, 22 Sep 2021 23:56:26 GMT Server: Kestrel Location: /baz/abc/123
GET http://localhost:5000/baz/abc/123 HTTP/1.1 Host: localhost:5000 HTTP/1.1 200 OK Date: Wed, 22 Sep 2021 23:56:26 GMT Server: Kestrel Content-Length: 33
GET http://localhost:5000/bar/abc/123 HTTP/1.1 Host: localhost:5000 HTTP/1.1 403 Forbidden Content-Length: 0 Date: Wed, 22 Sep 2021 23:56:33 GMT Server: Kestrel
將針對HTTP請求重定向到對應HTTPS終結點是一種常見的重定向場景。如下所示的演示針對路徑「/foo」和「/bar」註冊了兩個終結點,它們均由註冊的兩個中介軟體構建的RequestDelegate委託作為處理器,其中一個就是呼叫UseRewriter擴充套件方法註冊的RewriteMiddleware中介軟體,另一箇中介軟體則是通過呼叫Run擴充套件方法註冊的,後者依然將最終請求的URL作為響應的內容。
using Microsoft.AspNetCore.Rewrite; var app = WebApplication.Create(); app.MapGet("/foo", CreateHandler(app, 302)); app.MapGet("/bar", CreateHandler(app, 307)); app.Run(); static RequestDelegate CreateHandler(IEndpointRouteBuilder endpoints, int statusCode) { var app = endpoints.CreateApplicationBuilder(); app .UseRewriter(new RewriteOptions().AddRedirectToHttps(statusCode, 5001)) .Run(httpContext => { var request = httpContext.Request; var address = $"{request.Scheme}://{request.Host}{request.PathBase}{request.Path}"; return httpContext.Response.WriteAsync(address); }); return app.Build(); }
兩個終結點的處理器通過本地方法CreateHandler建立出來的。該方法呼叫當前WebApplication物件的CreateApplicationBuilder方法建立了一個新的IApplicationBuilder物件,並呼叫後者的UseRewriter擴充套件方法註冊了RewriteMiddleware中介軟體。我們為該中介軟體提供的HTTPS重定向規則是通過呼叫RewriteOptions物件的AddRedirectToHttps擴充套件方法定義的,該方法時指定了重定向響應採用的狀態碼(302和307)和HTTPS終結點採用的埠號。改動的程式啟動之後,針對兩個終結點的HTTP請求(「http://localhost:5000/foo」和「http://localhost:5000/bar」)均以圖5所示的形式被重定向到了對應的HTTPS終結點。
圖5 HTTPS重定向
整個過程涉及到如下四次報文交換,我們可以看出我們通過呼叫AddRedirectToHttps擴充套件方法定義的規則採用的是使用者端重定向。重定向響應採用了我們設定的狀態碼,分別是「302 Found」和「307 Temporary Redirect」。
GET http://localhost:5000/foo HTTP/1.1 Host: localhost:5000 HTTP/1.1 302 Found Content-Length: 0 Date: Thu, 23 Sep 2021 12:10:51 GMT Server: Kestrel Location: https://localhost:5001/foo
GET https://localhost:5001/foo HTTP/1.1 Host: localhost:5001 HTTP/1.1 200 OK Date: Thu, 23 Sep 2021 12:10:51 GMT Server: Kestrel Content-Length: 26 https://localhost:5001/foo
GET http://localhost:5000/bar HTTP/1.1 Host: localhost:5000 HTTP/1.1 307 Temporary Redirect Content-Length: 0 Date: Thu, 23 Sep 2021 12:10:57 GMT Server: Kestrel Location: https://localhost:5001/bar
GET https://localhost:5001/bar HTTP/1.1 Host: localhost:5001 HTTP/1.1 200 OK Date: Thu, 23 Sep 2021 12:10:57 GMT Server: Kestrel Content-Length: 26 https://localhost:5001/bar