在HTTP的语义中,重定向一般指的是服务端通过返回一个状态码为3XX的响应促使客户端像另一个地址再次发起请求,本章将此称为“客户端重定向“。既然有客户端重定向,自然就有服务端重定向,本章所谓的服务端重定向指的是在服务端通过改变请求路径将请求导向另一个终结点。ASP.NET下的重定向是通过RewriteMiddleware中间件实现的。(本文提供的示例演示已经同步到《ASP.NET Core 6框架揭秘-实例演示版》)
(资料图片)
[S2501]客户端重定向[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.1Host: localhost:5000HTTP/1.1 302 FoundContent-Length: 0Date: Wed, 22 Sep 2021 13:34:17 GMTServer: KestrelLocation: /bar/abc/123
GET http://localhost:5000/bar/abc/123 HTTP/1.1Host: localhost:5000HTTP/1.1 200 OKDate: Wed, 22 Sep 2021 13:34:17 GMTServer: KestrelContent-Length: 33http://localhost:5000/bar/abc/123[S2502]服务端重定向
服务端重定向会在服务端通过重写请求路径的方式将请求重定向到新的终结点。对于前面演示的程序来说,我们只需要对它做简单的修改就能切换到服务端重定向。如下面的代码片段所示,在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();改动的程序启动后,如果利用浏览器采用相同的路径(“/foo/abc/123”)对站点发起请求,我们会得到如图2所示的相同的响应内容。由于这次采用的是服务端重定向,整个过程只会涉及一次报文交换,所以浏览器的请求地址不会改变。
图2 服务端重定向
[S2503]采用IIS重写规则实现重定向重定向是绝大部分Web服务器(比如IIS、Apache和Nginx等)都会提供的功能,但是不同的服务器类型针对重定向规则具有不同的定义方式。IIS中的重定向被称为“URL重写”,具体的URL重写规则采用XML格式进行定义,RewriteMiddleware中间件对它提供了原生的支持。我们将URL重写规则以如下的方式定义在创建的rewrite.xml文件中,并将该文件保存在演示项目的根目录下。
如上所示的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.1Host: localhost:5000HTTP/1.1 301 Moved PermanentlyContent-Length: 0Date: Wed, 22 Sep 2021 23:26:02 GMTServer: KestrelLocation: /baz/abc/123
GET http://localhost:5000/baz/abc/123 HTTP/1.1Host: localhost:5000HTTP/1.1 200 OKDate: Wed, 22 Sep 2021 23:26:02 GMTServer: KestrelContent-Length: 33http://localhost:5000/baz/abc/123
GET http://localhost:5000/bar/abc/123HTTP/1.1Host: localhost:5000HTTP/1.1 200 OKDate: Wed, 22 Sep 2021 23:26:26 GMTServer: KestrelContent-Length: 33http://localhost:5000/baz/abc/123[S2504]采用Apache重写规则实现重定向
上面我们演示了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/123HTTP/1.1Host: localhost:5000HTTP/1.1 307 Temporary RedirectContent-Length: 0Date: Wed, 22 Sep 2021 23:56:26 GMTServer: KestrelLocation: /baz/abc/123
GET http://localhost:5000/baz/abc/123HTTP/1.1Host: localhost:5000HTTP/1.1 200 OKDate: Wed, 22 Sep 2021 23:56:26 GMTServer: KestrelContent-Length: 33
GET http://localhost:5000/bar/abc/123HTTP/1.1Host: localhost:5000HTTP/1.1 403 ForbiddenContent-Length: 0Date: Wed, 22 Sep 2021 23:56:33 GMTServer: Kestrel[S2505]基于HTTPS终结点的重定向
将针对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/fooHTTP/1.1Host: localhost:5000HTTP/1.1 302 FoundContent-Length: 0Date: Thu, 23 Sep 2021 12:10:51 GMTServer: KestrelLocation: https://localhost:5001/foo
GET https://localhost:5001/foo HTTP/1.1Host: localhost:5001HTTP/1.1 200 OKDate: Thu, 23 Sep 2021 12:10:51 GMTServer: KestrelContent-Length: 26https://localhost:5001/foo
GET http://localhost:5000/barHTTP/1.1Host: localhost:5000HTTP/1.1 307 Temporary RedirectContent-Length: 0Date: Thu, 23 Sep 2021 12:10:57 GMTServer: KestrelLocation: https://localhost:5001/bar
GET https://localhost:5001/bar HTTP/1.1Host: localhost:5001HTTP/1.1 200 OKDate: Thu, 23 Sep 2021 12:10:57 GMTServer: KestrelContent-Length: 26https://localhost:5001/bar