今天老周和大伙伴们聊聊有关 Url Rewrite 的事情,翻译过来就是 URL 重写。
这里不得不提一下,URL重定向与重写的不同。
1、URL重定向是客户端(通常是浏览器)向服务器请求地址A,然后服务器要求重定向到B,返回状态码 301 或 302 给客户端,并且夹带一个 Location 的标头,其值表示要重定向的目标 URL,即B;随后客户端再用B向服务器发起请求,若成功,服务器返回内容并夹带状态码 200。
2、URL重写只在服务器上转换URL,当客户端请求地址A进入服务器后,服务器自行处理并转向B。最后返回B地址的内容,夹带状态码 200。此过程只在服务器上发生,不需要与客户端进行多次通信。因此浏览器地址栏中的URL也不会发生变化。
-------------------------------------------------- 超级分界线 ------------------------------------------------------
实现 URL 重写不需要向服务容器注册功能类,但可以在 Service 集合中配置 Options。你需要通过 RewriteOptions 对象来指定重定向的规则。定义规则的方法是实现 IRule 接口。此接口只有一个 ApplyRule 方法。在实现该方法时,根据需要修改 HttpContext.Request.Path 来设置新的 URL。
------------------------------------------------------------------------------------------------------------------------
下面咱们直接上示例,这里我写了一个简单的URL重写规则。
public sealed class MyRule : IRule { public void ApplyRule(RewriteContext context) { var request = context.HttpContext.Request; var oldPath = request.Path; // 正则表达式来匹配 var match = Regex.Match(oldPath, "/ft(\\d+)"); if(match == null || match.Success == false) { context.Result = RuleResult.ContinueRules; return; } // 找出匹配的分组 var matchedval = match!.Groups[1].Value; // 新的URL PathString newPath = "/fight-action/" + matchedval; request.Path = newPath; //修改为新URL context.Result = RuleResult.SkipRemainingRules; } }
这个规则是这样的:客户端请求 https://localhost/ft3,通过正则表达式,找出URL中的数值 3 ,然后改为新的 URL:https://localhost/fight-action/3。即
https://host/ft{数值} ----> https://host/fight-action/{数值}
为什么要重写 URL 呢,假设网站内部实现某功能的 URL 很长,很难看,很难记忆,用户看到它就想抽它。为了让用户觉得好记好看,在公开的前台 UI 或者 Web API 中使用一个更短更方便记忆的 URL。这又是为何呢?老周做个假设:假设你有个新闻系统,按照最初的开发设计,写一篇新闻的 URL 是 http://killer/news_manager/addnew。嗯,这个路径逻辑清晰、层次分明、表义明了(对开发人员来说,这样好维护,模块化);可对用户来说,他哪管你模块化还是分尸化,他就觉得这太长,不好记,也不好输入。行,咱们给来个 URL 重写,对外公开的 URL 变成 http://killer/addnews,而服务器内部还是转回原来的地址来处理,但客户端是毫无察觉的。
还有一种情况是网站修改了,后台的结构变了,API 的结构变了,可你懒得把所有前台 UI 改动。于是,你也可以写个 URL 重写规则,让旧 URL 自动转到新的 URL 上,同样客户端毫无察觉的(明修____,暗渡____)。
回到 ASP.NET Core 主体代码,这里为了节省体力和脑力,老周就不做 HTML 页了,直接 MapGet 代表每个页面。
// 模拟一些路径 app.MapGet("/", () => "燕双鹰战斗仪"); app.MapGet("/fight-action/{mode}", (int mode) => mode switch { 0 => "全自动扫射装载中……", 1 => "装逼两分钟,开挂三小时", 2 => "无限子弹碾压", 3 => "我赌你的枪里没有子弹", 4 => "脑浆警告", 5 => "像你这样的人应该怎么改变,不会改变的,只有X", _ => "外挂已到期" });
路径 /fight-action 后面一段是路由参数 mode,然后这个数值会随同参数 mode 传入lambda 表达式,内部根据 mode 的值返回不同的字符串。
使用 URL 重写我们不需要向服务容器添加依赖注入对象,直接在 HTTP 管线上以中间件方式 Use 一下即可。
app.UseRewriter();
不过,这个无参数调用是未添加任何自定义重写规则的,咱们有两种方法添加规则。
第一种方法:保持 UseRewriter 方法无参数调用,使用服务容器来 Config 一下 RewriteOptions 选项类。这个方法实际是在服务容器中生成了 IOptions<RewriteOptions> 对象,中间件类 RewriteMiddleware 的构造函数会注入这个选项类实例。
// 以下为 .NET 源代码 public RewriteMiddleware( RequestDelegate next, IWebHostEnvironment hostingEnvironment, ILoggerFactory loggerFactory, IOptions<RewriteOptions> options) { // …… _next = next; _options = options.Value; _fileProvider = _options.StaticFileProvider ?? hostingEnvironment.WebRootFileProvider; _logger = loggerFactory.CreateLogger<RewriteMiddleware>(); }
现在,咱们把刚刚写的 MyRule 规则配置一下。
var builder = WebApplication.CreateBuilder(args); // 配置 rewrite options builder.Services.Configure<RewriteOptions>(rwo => { rwo.Add(new MyRule()); }); var app = builder.Build();
第二种方法:在HTTP 管理线中调用 UseRewriter 方法前,直接 new 一个 RewriteOptions 实例,然后添加刚刚写的规则。最后调用 UseRewriter 方法的带参数版本,把 options 传给它即可。
RewriteOptions rwopt = new(); rwopt.Add(new MyRule()); app.UseRewriter(rwopt);
两种方法任选其一就可以了,不需要重复配置。
现在咱们运行一下示例。
默认打开主页是这样的。
然后把 URL 改为 /ft3。
URL 已经跳转,不过浏览器地址栏不会有变化,而且不会返回 301、302 给客户端,因为这个跳转过程是在服务器上完成的。
------------------------------------ 未知分界线 -------------------------------------------
其实,RewriteOptions 补充了一些扩展方法,使得咱们在简单重写URL(不需要特复杂的逻辑分析,用正则就能搞定的)时可以不去实现 IRule 接口。故,咱们这个示例可以改成这样更简洁的实现。
var builder = WebApplication.CreateBuilder(args); // 配置 rewrite options builder.Services.Configure<RewriteOptions>(rwo => { rwo.AddRewrite("ft(\\d+)", "fight-action/$1", true); }); var app = builder.Build();
第一个参数是正则表达式,括号中表示捕捉为一个分组,第二个参数是新的 URL,其中“$1”表示引用正则表达式中捕捉的分组编号,咱们这个正则表达式中只有一个分组,即捕捉数值的 \d+,所以用 $1 引用它;如果有其他分组,那就依此类推,$2、$3、$4。它的意思就是引用捕捉到这个分组的值。如,匹配 ft2,捕捉到数值 2,然后替换 $1,使新的URL为 fight-action/2。注意在匹配和替换的 URL 都不用“/”开头,反正类库在处理时也会删除“/”的,所以我们就没必须加“/”。
再测试一下。
运行后,转到 /ft5,结果如下。
好了,今天的节目就到此了,下次有空咱们再聊。