65.9K
CodeProject 正在变化。 阅读更多。
Home

ASP.NET URL 重写解决方案

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.24/5 (12投票s)

2007年6月22日

CPOL

8分钟阅读

viewsIcon

114060

downloadIcon

819

功能强大的 ASP.NET URL 重写和处理重写参数的解决方案。

引言

Screenshot - fig1.jpg

该解决方案为 ASP.NET 开发人员提供了一种可扩展且易于管理的方式来重写其应用程序中使用的 URL。该解决方案分为三个部分:

  1. URL 配置管理器
  2. 允许配置重写规则。此功能已开发用于允许重写以及排除某些文件或文件夹和重定向。

  3. 导航管理器
  4. 这使开发人员能够管理网站的虚拟结构。提供的示例使用 XML 文件,但也可以轻松地更改为使用其他类型的数据源。我认为此模块使此 URL 重写示例在大多数其他可用示例中脱颖而出。它允许将文件夹定义为参数,开发人员可以请求这些参数。

  5. 回发 (Postbacks)
  6. 我在大多数可用的重写解决方案中发现的一个问题是,它们会回发到重写后的页面,导致 URL 不一致。通过覆盖基类 HTMLForm,我们可以避免这种情况。

背景

URL 重写是一种方法,它允许以用户友好的方式显示页面的 URL,同时为开发人员提供所需的信息。目前有许多解决方案可用,从内置的 ASP.NET 重写到 IIS 的 ISAPI 过滤器。

为什么选择此解决方案?

在 ASP.NET 中实现重写有很多方法,但我发现很少有示例能够满足我在应用程序中所需的全部功能,这促使我开发了这篇文章。此解决方案具有以下特点:

  1. 无需对 IIS 或服务器进行任何配置更改
  2. 使用正则表达式轻松定义规则
  3. 支持基于文件、文件夹和正则表达式的排除
  4. 重定向功能
  5. 轻松访问提供的参数

这其中最棒的功能之一是无需考虑“真实”页面;每个页面都会被重写,导航功能提供对 URL 和查询字符串所有方面的访问。

1. URL 配置管理器

此模块的任务是管理与 URL 重写相关的规则。在本例中,它基于外部 XML 文件,但如果需要,也可以轻松地将其集成到 web.config 文件中。

一个示例配置文件如下:

<?xml version="1.0" encoding="utf-8" ?>
<urlconfiguration enabled="true" />
    <excludepath url="~/testfolder/(.*)" />
    <excludefile url="~/testpage.aspx" />
    <redirect url="~/newfolder/(.*)" newpath="~/testpage.aspx" />
    <rewrite url="~/(.*).aspx" newpath="~/default.aspx" />
</urlconfiguration />

此示例显示了可用的规则类型。下面将更详细地介绍这些规则。

<excludepath url="~/testfolder/(.*)" />

此规则定义了对 testfolder 中任何文件的请求不应被重写。这也将包括任何子文件夹中的文件。

<excludefile url="~/testpage.aspx" />

此规则规定,对位于应用程序根目录中的 testpage.aspx 文件的任何请求都将不会被重写。

<redirect url="~/newfolder/(.*)" newpath="~/testpage.aspx" />

重定向规则会将匹配指定标准的任何文件重写为 newPath 属性中定义的 URL。

<redirect url="~/newfolder/(.*)" newpath="~/testpage.aspx" />

重定向规则会绕过重写,并立即 Response.Redirect 到指定的 URL,该 URL 可以是站点内部或外部的页面。

<rewrite url="~/(.*).aspx" newpath="~/default.aspx" />

最后一条规则会将匹配标准的任何文件重写为由特定页面处理。例如,如果需要,可以将 /books/ 文件夹中的任何内容轻松更改为重写到 showbook.aspx

现在我们有了配置文件,还需要创建几个类:一个用于读取数据,另一个类实现 IHTTPModule,它将执行重定向例程。

首先,我们将创建一个 XMLConfigurationManager 类,该类从指定的配置文件中读取数据,以便提供给重定向类。

Public Class XMLConfigurationManager

  Private _configurationPath As String

  Public Property ConfigurationPath() As String
    Get
      Return _configurationPath
    End Get
    Set(ByVal value As String)
      _configurationPath = value
    End Set
  End Property

  Public Sub New(ByVal configurationDocumentPath As String)
    ConfigurationPath = configurationDocumentPath
  End Sub

  Friend Function GetConfiguration() As XmlNode
    'Ensure that the configuration path exists
    If Not IO.File.Exists(ConfigurationPath) Then
      Throw New Exception("Could not obtain configuration information")
    End If

    'Load the configuration settings
    Dim settings As New XmlDocument
    settings.Load(ConfigurationPath)
    Return settings.ChildNodes(1)
  End Function

End Class

此类有一个构造函数,它接受配置文件路径(可以存储在 web.config 文件中),并提供一个函数来返回根配置节点。

需要的第二个类是执行重写的 HTTP 模块;在这种情况下,它是一个名为 UrlRewritingModule 的类。

Public Class UrlRewritingModule
  Implements System.Web.IHttpModule
  
  'TODO: Insert class code here

End Class

这会将该类创建为一个 HTTP 模块。然后,我们可以将其添加到 web.config;我将在文章后面详细介绍。

Public Sub Dispose() Implements System.Web.IHttpModule.Dispose
End Sub

Public Sub Init(ByVal context As System.Web.HttpApplication) 
    Implements System.Web.IHttpModule.Init
    
    If context Is Nothing Then
      Throw New Exception("No context available")
    End If
    AddHandler context.AuthorizeRequest, AddressOf Me.rewrite
End Sub

上面显示的 Init 方法只是确保应用程序存在,并创建一个将在请求被授权后触发的事件处理程序。

Private Sub rewrite(ByVal sender As Object, ByVal e As EventArgs)
  Dim app As HttpApplication = CType(sender, HttpApplication)
  If app Is Nothing Then
    Throw New Exception("No application available")
  End If

  Dim urlConfiguration As New XMLConfigurationManager _ 
    (HttpContext.Current.Request.PhysicalApplicationPath + _ 
      System.Configuration.ConfigurationManager.AppSettings("UrlDataRelativePath"))
  Dim requestedPage As String = "~" + app.Request.RawUrl
  Dim querystring As String = String.Empty
  If requestedPage.Contains("?") Then
    querystring = requestedPage.Substring(requestedPage.IndexOf("?") + 1)
    requestedPage = requestedPage.Substring(0, requestedPage.IndexOf("?"))
  End If

  Dim newUrl As String = getRewrittenUrl(requestedPage, 
    urlConfiguration.GetConfiguration)

  If Not String.IsNullOrEmpty(querystring) Then
    newUrl += "?" + querystring
  End If

  HttpContext.Current.RewritePath(newUrl, False)
End Sub

此方法首先使用从 web.config 文件获取的文件路径,从 XMLConfigurationManager 类读取配置。然后,在将请求的 URL 传递给 getRewrittenUrl 函数的同时,将其从查询字符串中分离出来。完成后,将查询字符串重新附加到 URL,并使用 ASP.NET 的 httpcontext.current.rewritepath 函数重写页面。

URL 配置的最后一部分是根据提供的规则返回重写 URL 的函数。

Private Function getRewrittenUrl(ByVal requestedPage As String, _
   ByVal configurationNode As XmlNode) As String

  'If the rewriting is disabled then return the source url
  If CType(configurationNode.Attributes("enabled").Value, Boolean) = False Then
    Return requestedPage
  End If

  'Iterate though the configuration document looking for a match
  For Each childNode As XmlNode In configurationNode.ChildNodes
    Dim regEx As New Regex(childNode.Attributes("url").Value, _
      RegexOptions.IgnoreCase)
    If regEx.Match(requestedPage).Success Then
      Select Case childNode.Name
        Case "excludePath", "excludeFile"
          Return requestedPage
        Case "rewrite"
          Return childNode.Attributes("newPath").Value
        Case "redirect"
          HttpContext.Current.Response.Redirect _ 
           (childNode.Attributes("newPath").Value)
      End Select
    End If
  Next
  Return requestedPage
End Function

上面的代码遍历规则,并根据节点的名称执行所需的功能,该功能在排除的情况下返回原始 URL,将请求重定向到指定页面,或将 URL 重写到指定的目录。

2. 导航管理器

导航管理器是位于 URL 重写之上的功能,它为开发人员提供有关当前页面的信息。此模块同样使用外部 XML 文件,但我认为在许多情况下,最好使用数据库。因此,我实现了一个基类和一个基于 XML 的管理器;可以使用类似的方法轻松开发 SQL/OLEDB 或其他提供程序。

理论

导航的设计方式是允许网站内存在固定和“动态”的文件夹结构。任何动态文件夹都可以作为键值对暴露给应用程序。例如,URL /products/shoes/nike/shoes.aspx

  • /products/ - 用于分隔站点功能区域的固定文件夹。
  • /shoes/ - 可能是类别。这是一个动态值,开发人员需要能够向用户显示信息。
  • /nike/ - 制造商。同样是动态文件夹。
  • /shoes.aspx - 页面名称。

导航管理器允许在代码中请求当前类别、制造商或正在查看的页面;例如:

Dim currentManufacturer as string = provider.GetNavigationValue("manufacturer")

Dim pageName as string = provider.GetNavigationValue("CurrentPage")

配置文件

此文件创建网站导航的虚拟结构。本示例已从我当前的 SQL 解决方案转换为 XML,以便于撰写本文。

<?xml version="1.0" encoding="utf-8" ?>
<Navigation>
    <Folder Name="Products" Value="products" IsDynamic="False">
        <Folder Name="Category" Value="productCategory" IsDynamic ="True" />
    </Folder>
    <Folder Name="About" Value="about" IsDynamic="False" />
    <Folder Name="Articles" Value="articles" IsDynamic="False">
        <Folder Name="Subject" Value="articleSubject" IsDynamic="true">
            <Folder Name="Year" Value="year" IsDynamic="true" />
        </Folder>
    </Folder>
</Navigation>

正如您所见,这是一个简单的文件夹结构,其中定义了文件夹是动态还是静态。一个限制是每个文件夹只能包含一个动态文件夹;否则,应用程序将无法确定使用哪个键。

Name 属性指定了一个更友好的文件夹名称,可能用于面包屑或在站点上显示。Value 属性是在尝试获取值时暴露给开发人员的键名,而 IsDynamic 指定文件夹是否为动态(显而易见)。

NavigationProviderBase

此基类包含管理导航的基本逻辑。

Public MustInherit Class NavigationProviderBase

  Protected MustOverride Sub processNavigation()

  Private _navigationParts As New Dictionary(Of String, String)
  Friend Property NavigationParts() As Dictionary(Of String, String)
    Get
      Return _navigationParts
    End Get
    Set(ByVal value As Dictionary(Of String, String))
      _navigationParts = value
    End Set
  End Property

  Private _querystringParts As New Dictionary(Of String, Object)
  Friend Property QuerystringParts() As Dictionary(Of String, Object)
    Get
      Return _querystringParts
    End Get
    Set(ByVal value As Dictionary(Of String, Object))
      _querystringParts = value
    End Set
  End Property

  Private _currentUrl As String
  Public Property CurrentUrl() As String
    Get
      Return _currentUrl
    End Get
    Set(ByVal value As String)
      _currentUrl = value
    End Set
  End Property

  Public Sub New()
    Dim querystring As String = String.Empty
    CurrentUrl = HttpContext.Current.Request.RawUrl

    'Remove the domain and querystring information
    If HttpContext.Current.Request.RawUrl.Contains("?") Then
      CurrentUrl = HttpContext.Current.Request.RawUrl.Substring _ 
     (0, CurrentUrl.IndexOf("?"))
    End If

    'Add the querystring values to the dictionary
     processQuerystring()
  End Sub

  Private Sub processQuerystring()
    'Convert the querstring items to an array
    For Each key As String In HttpContext.Current.Request.QueryString.Keys
      QuerystringParts.Add(key, HttpContext.Current.Request.QueryString(key))
    Next
  End Sub

End Class

基类包含创建查询字符串值的信息,这些值可以与导航值一起提供给页面(导航值在另一个类中进行解释,以便使用不同的数据源)。查询字符串在此类中的实现是一个附加功能,但对应用程序而言并非必不可少。

XMLNavigationProvider

此类继承自 NavigationProviderBase(如上),其任务是从 XML 文件中读取数据到页面可用的键值对列表中。

Public Class XMLNavigationProvider
  Inherits NavigationProviderBase

  Private _configurationPath As String
  Public ReadOnly Property ConfigurationPath() As String
    Get
      Return _configurationPath
    End Get
  End Property

  Public Sub New(ByVal configurationPath As String)
    MyBase.New()
    _configurationPath = configurationPath
    processNavigation()
  End Sub

  Protected Overrides Sub processNavigation()
    'Create an array to contain the raw information regarding the Url
    Dim items() As String = CurrentUrl.Split(New Char() {"/"c},  _ 
    StringSplitOptions.RemoveEmptyEntries)

    'Load the XML navigation structure
    'Ensure that the configuration path exists
    If Not IO.File.Exists(ConfigurationPath) Then
       Throw New Exception("Could not obtain configuration information")
    End If

    'Load the configuration settings
    Dim settings As New XmlDocument
    settings.Load(ConfigurationPath)

    ProcessNode(items, settings.ChildNodes(1), 0)
  End Sub

  Protected Sub ProcessNode(ByVal items() As String, _
  ByVal parentNode As XmlNode, ByVal currentLevel As Integer)
     'Iterate though the items, build up the navigation keys
     If currentLevel >= items.Length Then Return

     If items(currentLevel).ToLower.Contains(".aspx") Then
       NavigationParts.Add("currentPage", _
       items(currentLevel).Replace(".aspx", ""))
     End If

     Dim currentFolder As XmlNode = Nothing
       For Each item As XmlNode In parentNode.ChildNodes
         If CType(item.Attributes("IsDynamic").Value, Boolean) _ 
         And Not items(currentLevel).ToLower.Contains(".aspx") Then
           currentFolder = item
          Else
            If CType(item.Attributes("Value").Value, String).ToLower _ 
              = items(currentLevel).ToLower Then
              currentFolder = item
            End If
          End If
        Next

        If currentFolder IsNot Nothing Then
          NavigationParts.Add(CType(currentFolder.Attributes("Value").Value _ 
          , String).ToLower, items(currentLevel))
          ProcessNode(items, currentFolder, currentLevel + 1)
        End If
   End Sub

End Class

代码主要围绕一个主函数,该函数遍历导航部分(本质上是文件夹)并将这些值映射到配置文件中包含的键。然后将它们添加到 NavigationParts 集合中。

Web.config 更改

需要对 web.config 文件进行一些更改才能启用 URL 重写。我们需要定义导航和 URL 配置文件以及添加 HTTP 模块的路径。

<appsettings />
  <add value="/config/URLConfiguration.xml" key="UrlDataRelativePath" />
  <add value="/config/NavigationConfiguration.xml" key="NavigationDataRelativePath" />
</appsettings />

并在 system.web 节点内部,我们需要:

<httpmodules />
  <add name="UrlRewritingModule" type="Rewriting.UrlRewritingModule" />
</httpmodules />

处理回发

此时,您将拥有一个可以正确重写 URL 的解决方案,但在回发时,应用程序将返回到重写的页面,这不是期望的功能。为了解决这个问题,我们可以创建一个自定义 HTMLForm,以确保回发发生在重写的页面上。

Public Class FormRewriterControlAdapter
   Inherits System.Web.UI.Adapters.ControlAdapter

   Protected Overrides Sub Render(ByVal writer As _
   System.Web.UI.HtmlTextWriter)
       MyBase.Render(New RewriteFormHtmlTextWriter(writer))
    End Sub

End Class


Public Class RewriteFormHtmlTextWriter
   Inherits HtmlTextWriter

   Sub New(ByVal writer As HtmlTextWriter)
       MyBase.New(writer)
       Me.InnerWriter = writer.InnerWriter
   End Sub

   Sub New(ByVal writer As System.IO.TextWriter)
       MyBase.New(writer)
       MyBase.InnerWriter = writer
   End Sub

   Public Overrides Sub WriteAttribute(ByVal name As String, _ 
      ByVal value As String, ByVal fEncode As Boolean)

       If (name = "action") Then
          Dim Context As HttpContext
          Context = HttpContext.Current
          If Context.Items("ActionAlreadyWritten") Is Nothing Then
            value = Context.Request.RawUrl
            Context.Items("ActionAlreadyWritten") = True
          End If
       End If
       MyBase.WriteAttribute(name, value, fEncode)

   End Sub

End Class

然后,您可以通过将一个 Form.browser 文件添加到 App_Browsers 文件夹中,并包含以下文本来实现:

<browsers>
  <browser refID="Default">
    <controlAdapters>
      <adapter controlType="System.Web.UI.HtmlControls.HtmlForm"
               adapterType="URLRewriting.Rewriting.FormRewriterControlAdapter" />
    </controlAdapters>
  </browser>

</browsers>

来源

请下载上面链接的完整解决方案,以获取源代码和一个简单的测试应用程序。

历史

  • 2007 年 7 月 2 日:在Andrei Rinea发表评论后,我已更新代码,使其不再使用继承的表单来处理回发,因为这会破坏设计时视图,而是使用自定义的 ControlAdapter。我在 Scott Gu 的博客上找到了这段代码,并将其集成到此解决方案中。

此解决方案的设计力求通用,实际上可能无法满足您应用程序的所有需求。我很乐意提出任何可能有帮助的更改。

© . All rights reserved.