什么是路由?
假设您有一个名为 RecipeDisplay.aspx 的 ASP.NET Web 窗体,此窗体位于一个名为 Web Forms 的文件夹中。使用此 Web 窗体查看方案时,传统方法是构建一个指向该窗体实际位置的 URL,并将某些数据通过编码方式嵌入到查询字符串中,以告知 Web 窗体要显示的方案。此类 URL 的结尾可能如下所示:/WebForms/RecipeDisplay.aspx?id=5,其中数字 5 代表在一个充满方案的数据库表中的主键值。
路由从本质上来说就是把一个 URL 端点分解成多个参数,然后使用这些参数将 HTTP 请求处理引导至特定的组件。让我们以 URL /recipe/5 为例。借助正确的路由配置,您仍可以使用 Web 窗体 RecipeDisplay.aspx 来响应此 URL。
此时 URL 不再代表实际的路径。单词 recipe 变为代表一个参数,路由引擎可以用它找到处理 recipe 请求的组件。数字 5 代表第二个参数,在处理期间用它显示某个特定的 recipe。此时不宜通过编码将数据库关键字嵌入 URL 中,更好的做法是使用形如 /recipe/tacos 的 URL。此 URL 不但包括足够多的参数来显示特定的 recipe,还非常便于人们阅读理解,它可以为最终用户揭示其意图,并包括一些重要的关键字供搜索引擎使用。
URL 重写的简史
在 ASP.NET 中,使用以 /recipe/tacos 结尾的 URL 时,从传统上而言需要有人来处理 URL 重写架构。有关 URL 重写的详细信息,请参阅 Scott Mitchell 撰写的权威文章“ 在 ASP.NET 中执行 URL 重写”。该文描述了在 ASP.NET 中使用 HTTP 模块和 HttpContext 类的静态 RewritePath 方法重写 URL 的常见途径。Scott 的文章还详细说明了友好且可改动 URL 的优点。
在过去曾使用过 RewritePath API 的用户可能非常清楚重写方法中的某些怪异现象和缺点。RewritePath 面临的主要问题是在处理请求的过程中该方法如何更改所使用的虚拟路径。使用 URL 重写时,需要设定每个 Web 窗体的回发目标(通常通过在请求过程中再次重写 URL 来实现),以避免回发转到内部已重写的 URL。
此外,大多数开发人员在实现 URL 重写时都采用单向转换模式,因为无法通过任何简单的机制使 URL 重写逻辑双向工作。例如,要赋予 URL 重写逻辑一个面向公众的 URL 并使该逻辑返回 Web 窗体的内部 URL 非常容易。但是,要为 Web 窗体的内部 URL 赋予重写逻辑并使其返回进入窗体所需的公共 URL 却很困难。生成到隐藏在被重写的 URL 中的其他 Web 窗体的超链接时,后者会非常有用。本专栏的其余部分介绍了 URL 路由引擎如何解决这些问题。
路由和路由处理程序
URL 路由引擎中包含三种基本角色:路由、路由处理程序和路由模块。路由将 URL 与路由处理程序关联在一起。Route 类来自 System.Web.Routing 命名空间,它的实例在运行时会代表一个路由并描述该路由的参数和约束。路由处理程序继承自 System.Web.Routing.IRouteHandler 接口。此接口要求路由处理程序实现 GetHttpHandler 方法,而此方法将返回一个实现 IHttpHandler 接口的对象。在最开始的时候,IHttpHandler 接口即已是 ASP.NET 的一部分,而 Web 窗体 (System.Web.UI.Page) 则属于 IhttpHandler。在使用 Web 窗体路由时,路由处理程序需要定位、实例化并返回正确的 Web 窗体。最终,路由模块嵌入到了 ASP.NET 处理管道中。该模块会拦截传入的请求、检查 URL 并判断是否存在任何已定义的匹配路由。该模块将检索匹配路由的相关路由处理程序,并向路由处理程序申请处理该请求的 IhttpHandler。
我提到过的三个主要类型如 图 1 所示。在下一节中,我将展示这三种角色的工作方式。
配置 ASP.NET 的路由
要配置 ASP.NET 网站或 Web 应用程序的路由功能,首先需要添加一个对 System.Web.Routing 程序集的引用。.NET Framework 3.5 的 SP1 安装程序会将此程序集安装到全局程序集缓存中,您可以在标准的“Add Reference”(添加引用)对话框内找到该程序集。
您还需要在 ASP.NET 管道中配置路由模块。路由模块是标准的 HTTP 模块。对于 IIS 6.0 和更早版本以及 Visual Studio Web 开发服务器,可以使用 web.config 的 来安装模块,具体如下所示:
要在 IIS 7.0 中运行带路由功能的网站,需要用到 web.config 中的两个条目。第一个条目是 URL 路由模块配置,位于 的 中。此外还需要 的 中的一个条目来处理对 UrlRouting.axd 的请求。这两个条目如 图 2 所示。另请参阅侧栏“IIS 7.0 配置条目”。
将 URL 路由模块配置到管道中后,它会将其自身绑定到 PostResolveRequestCache 和 PostMapRequestHandler 事件上。 图 3 显示了管道事件的一个子集。URL 重写实现通常在 BeginRequest 事件过程中执行其工作,该事件是在一个请求中最早被触发的事件。使用 URL 路由时,路由处理程序会在 PostResolveRequestCache 阶段(此阶段发生在身份验证、授权以及缓存查询等处理阶段之后)进行路由匹配和选择。我会在本专栏后面的内容中再次讨论此事件定时的含义。
配置路由
路由和路由处理程序的联系非常紧密,但我首先着眼于路由配置代码。路由引擎的 RouteTable 类会通过其静态 Routes 属性公开 RouteCollection。首先您必须在此集合内配置所有自定义路由,然后应用程序才能开始执行第一个请求,这意味着您需要使用 global.asax 文件和 Application_Start 事件。
图 4 显示了要让 "/recipe/brownies" 进入 RecipeDisplay.aspx Web 窗体必须使用的路由注册代码。在 RouteCollection 类中,Add 方法的参数包括一个友好的路由名称,后跟路由本身。Route 构造函数的第一个参数是 URL 模式。该模式由多个 URL 片段组成,这些片段将出现在指向此应用程序的 URL 的末尾(且在进入该应用程序的根所需的任何片段之后)。而对于根位于 localhost/food/ 中的应用程序, 图 4 中的路由模式将匹配 localhost/food/recipe/brownies。
图 4 /recipe/brownies 的路由注册代码
protected void Application_Start(object sender, EventArgs e)
{
RegisterRoutes();
}
private static void RegisterRoutes()
{
RouteTable.Routes.Add(
"Recipe",
new Route("recipe/{name}",
new RecipeRouteHandler(
"~/WebForms/RecipeDisplay.aspx")));
}
大括号中的内容表示参数,路由引擎会自动提取该处的值并将其放入名称/值字典中,在请求持续期间,该字典将始终存在。在前一个 localhost/food/recipe/brownies 示例中,路由引擎将提取值 "brownies" 并使用 "name" 作为关键字将其存储到字典中。在我介绍路由处理程序的代码时,您会了解到如何来使用字典。
您可以根据需要向 RouteTable 中添加任意数量的路由,但路由的排列顺序非常重要。路由引擎会根据集合中路由出现的顺序来使用这些路由测试所有传入 URL,引擎会选择第一个与模式匹配的路由。为此,您应首先添加最具特殊性的路由。如果以 URL 模式 "{category}/{subcategory}" 在方案路由之前添加了一个通用路由,则路由引擎永远也不会发现该方案路由。另外还要注意——路由引擎在执行模式匹配时是不区分大小写的。
Route 构造函数的重载版本允许您创建默认参数值并应用约束。当传入 URL 中的参数不包含任何值时,Defaults 允许您指定路由引擎的默认值以将其放入名称/值参数字典中。例如,当路由引擎发现一个不包含名称值(如 localhost/food/recipe)的方案 URL 时,可使用 "brownies" 作为默认的方案名称。
Constraints 允许您指定正则表达式,以对参数进行验证并精确调整与传入 URL 匹配的路由模式。如果使用主键值在 URL 中标识方案(类似于 localhost/food/recipe/5),则可以使用正则表达式来确保 URL 中的主键值是整数。您还可以使用实现 IRouteConstraint 接口的对象来应用约束。
Route 构造函数的第二个参数是我的路由处理程序的一个新实例,我将在 图 5 中对其进行介绍。
图 5 RecipeRouteHandler
public class RecipeRouteHandler : IRouteHandler
{
public RecipeRouteHandler(string virtualPath)
{
_virtualPath = virtualPath;
}
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
var display = BuildManager.CreateInstanceFromVirtualPath(
_virtualPath, typeof(Page)) as IRecipeDisplay;
display.RecipeName = requestContext.RouteData.Values["name"] as string;
return display;
}
string _virtualPath;
}
方案路由处理程序
以下代码段显示了方案请求路由处理程序的基本实现。由于路由处理程序最终必须创建一个 IHttpHandler 的实例(在本例中为 RecipeDisplay.aspx),因此构造函数需要一个虚拟路径,它将指向路由处理程序将要创建的 Web 窗体。GetHttpHandler 方法将此虚拟路径传递给 ASP.NET BuildManager,以便检索实例化的 Web 窗体:
interface IRecipeDisplay : IHttpHandler
{
string RecipeName { get; set; }
}
请注意,路由处理程序如何能够同时从路由引擎的参数字典中提取数据,这是 RequestContext 类的 RouteData 属性。路由引擎会设置 RequestContext 并在其调用此方法时传递一个实例。有许多选项可用来将路由数据传入 Web 窗体。例如,您可以将路由数据传递到 HttpContext 项目集合中。在本例中,您为要实现的 Web 窗体定义了一个接口 (IRecipeDisplay)。路由处理程序可以设置 Web 窗体的强类型化属性,以传递 Web 窗体需要的任何信息,此方法同时适用于 ASP.NET 网站和 ASP.NET 应用程序编译模式。
路由和安全性
在使用 ASP.NET 路由时,您仍可以继续使用您喜欢的所有 ASP.NET 功能——母版页、输出缓存、主题、用户控件以及更多其他功能。但是也有一个不容忽视的例外情况。路由模块的神奇功能源自管道中的事件,这些事件发生在身份验证和授权处理阶段之后,这意味着 ASP.NET 将授权您的用户使用公共的可视 URL,而非到 ASP.NET Web 窗体(路由处理程序选择用来处理请求的)的虚拟路径。您需要对使用路由的应用程序的授权策略特别加以注意。
假设您只想允许那些通过身份验证的用户来查看方案,其中一种方法是修改根 web.config 以使用授权设置,如下所示:
尽管此方法可阻止匿名用户查看 /recipe/tacos,但它也存在着两个根本的缺点。第一,此设置无法阻止用户直接请求 /WebForms/RecipeDisplay.aspx(尽管您可以添加另一个授权规则来阻止所有用户直接请求 Web 窗体文件夹的资源)。第二,无需更改授权规则即可很容易地更改 global.asax.cs 中的路由配置,致使您的秘密方案暴露给匿名用户。
另一种授权方法是根据 RecipeDisplay.aspx Web 窗体的实际位置对其进行保护,它将包含
设置的 web.config 文件直接放入受保护的文件夹。但是,由于 ASP.NET 是根据公共 URL 来授权用户的,因此您需要对路由处理程序使用的虚拟路径进行手动授权检查。
您需要将下列代码添加到路由处理程序的 GetHttpHandler 方法的开头。此代码使用 UrlAuthorizationModule 类的静态 CheckUrlAccessForPrincipal 方法(与在 ASP.NET 管道中执行授权检查时使用的模块相同):
if (!UrlAuthorizationModule.CheckUrlAccessForPrincipal(
_virtualPath, requestContext.HttpContext.User,
requestContext.HttpContext.Request.HttpMethod))
{
requestContext.HttpContext.Response.StatusCode =
(int)HttpStatusCode.Unauthorized;
requestContext.HttpContext.Response.End();
}
要通过 RequestContext 访问 HttpContext 成员,需要添加对 System.Web.Abstractions 程序集的引用。
有了安全路由处理程序,您现在可以将注意力转到需要在数据库中为每个方案生成超链接的页面。事实证明路由逻辑也可以帮助您构建此页面。
URL 生成
为生成到任何给定方案的超链接,我将再次转到在应用程序启动过程中配置的路由集合。如此处所示,RouteCollection 类有一个 GetVirtualPath 方法可用于此场合:
VirtualPathData pathData =
RouteTable.Routes.GetVirtualPath(
null,
"Recipe",
new RouteValueDictionary { { "Name", recipeName } });
return pathData.VirtualPath;
您需要传入所需的路由名称 ("Recipe") 和所需参数的字典及其关联值。此方法将使用您先前创建的 URL 模式 (/recipe/{name}) 来构建适合的 URL。
下列代码将使用此方法来生成匿名的类型化对象的集合。这些对象具有 Name 和 Url 属性,您可以将其与数据绑定一同使用以生成可用方案的列表或表格。
var recipes =
new RecipeRepository()
.GetAllRecipeNames()
.OrderBy(recipeName => recipeName)
.Select(recipeName =>
new
{
Name = recipeName,
Url = GetVirtualPathForRecipe(recipeName)
});
通过路由配置生成 URL 意味着您可以更改配置而无需担心在应用程序中创建的链接中断。当然,您仍可以中断用户中意的链接和书签,但在设计应用程序的 URL 结构时有能力进行更改是一项重要的优势。
路由总结
URL 路由引擎可完成有关 URL 模式匹配和 URL 生成的所有繁琐工作。您需要做的只是配置路由并实现路由处理程序。通过使用路由,您可以真正脱离文件扩展名和文件系统的物理布局,并且您无需处理因使用 URL 重写而出现的怪异现象。您可以将注意力集中在为最终用户和搜索引擎优化 URL 设计上。此外,在即将面世的 ASP.NET 4.0 中,Microsoft 正致力于使 Web 窗体的 URL 路由更易于使用且可配置性更好。