Dot-Net

如何正確規範化 ASP.NET MVC 應用程序中的 URL?

  • January 7, 2016

我試圖找到一種很好的通用方法來規範化 ASP.NET MVC 2 應用程序中的 url。到目前為止,這是我想出的:

// Using an authorization filter because it is executed earlier than other filters
public class CanonicalizeAttribute : AuthorizeAttribute
{
   public bool ForceLowerCase { get;set; }

   public CanonicalizeAttribute()
       : base()
   {
       ForceLowerCase = true;
   }

   public override void OnAuthorization(AuthorizationContext filterContext)
   {
       RouteValueDictionary values = ExtractRouteValues(filterContext);
       string canonicalUrl = new UrlHelper(filterContext.RequestContext).RouteUrl(values);
       if (ForceLowerCase)
           canonicalUrl = canonicalUrl.ToLower();

       if (filterContext.HttpContext.Request.Url.PathAndQuery != canonicalUrl)
           filterContext.Result = new PermanentRedirectResult(canonicalUrl);
   }

   private static RouteValueDictionary ExtractRouteValues(AuthorizationContext filterContext)
   {
       var values = filterContext.RouteData.Values.Union(filterContext.RouteData.DataTokens).ToDictionary(x => x.Key, x => x.Value);
       var queryString = filterContext.HttpContext.Request.QueryString;
       foreach (string key in queryString.Keys)
       {
           if (!values.ContainsKey(key))
               values.Add(key, queryString[key]);
       }
       return new RouteValueDictionary(values);
   }
}

// Redirect result that uses permanent (301) redirect
public class PermanentRedirectResult : RedirectResult
{
   public PermanentRedirectResult(string url) : base(url) { }

   public override void ExecuteResult(ControllerContext context)
   {
       context.HttpContext.Response.RedirectPermanent(this.Url);
   }
}

現在我可以像這樣標記我的控制器:

[Canonicalize]
public class HomeController : Controller { /* ... */ }

這一切似乎都運作良好,但我有以下擔憂:

  1. CanonicalizeAttribute當很難想到我不想要這種行為的情況時,我仍然必須將其添加到我想要規範化的每個控制器(或操作方法)中。似乎應該有一種方法可以在整個站點範圍內獲得這種行為,而不是一次一個控制器。
  2. 我在過濾器中實施“強制小寫”規則的事實似乎是錯誤的。當然,以某種方式將其添加到路由 url 邏輯中會更好,但我想不出在我的路由配置中執行此操作的方法。我想為@"[a-z]*"控制器和操作參數(以及任何其他字元串路由參數)添加約束,但我認為這會導致路由不匹配。此外,因為小寫規則沒有在路由級別應用,所以有可能在我的頁面中生成包含大寫字母的連結,這看起來很糟糕。

有什麼明顯的我在這裡俯瞰嗎?

對於預設 ASP.NET MVC 路由的輕鬆性質、忽略字母大小寫、尾部斜杠等,我感到同樣“癢”。和你一樣,我想要一個通用的解決方案,最好是作為路由邏輯的一部分我的應用程序。

在網上到處搜尋後,沒有找到有用的庫,我決定自己推出一個。結果就是Canonicalize,這是一個補充 ASP.NET 路由引擎的開源類庫。

您可以通過 NuGet 安裝該庫:Install-Package Canonicalize

在您的路線註冊中:routes.Canonicalize().Lowercase();

除了小寫,包中還包含其他幾個 URL 規範化策略。強制www打開或關閉域前綴,強制使用特定的主機名,尾部斜杠等。添加自定義 URL 規範化策略也很容易,我非常願意接受更新檔,為“官方” Canonicalize發行版添加更多策略。

我希望你或其他人會覺得這很有幫助,即使這個問題已經存在一年了:)

MVC 5 和 6 可以選擇為您的路由生成小寫 URL。我的路線配置如下所示:

public static class RouteConfig
{
   public static void RegisterRoutes(RouteCollection routes)
   {
       // Imprive SEO by stopping duplicate URL's due to case or trailing slashes.
       routes.AppendTrailingSlash = true;
       routes.LowercaseUrls = true;

       routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

       routes.MapRoute(
           name: "Default",
           url: "{controller}/{action}/{id}",
           defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional });
   }
}

使用此程式碼,您應該不再需要規範化 URL,因為這是為您完成的。如果您使用 HTTP 和 HTTPS URL 並為此需要一個規範的 URL,則可能會出現一個問題。在這種情況下,很容易使用上述方法並將 HTTP 替換為 HTTPS,反之亦然。

另一個問題是連結到您網站的外部網站可能會省略尾部斜杠或添加大寫字元,為此您應該執行 301 永久重定向到帶有尾部斜杠的正確 URL。完整用法和原始碼,參考我的文和RedirectToCanonicalUrlAttribute過濾器:

/// <summary>
/// To improve Search Engine Optimization SEO, there should only be a single URL for each resource. Case 
/// differences and/or URL's with/without trailing slashes are treated as different URL's by search engines. This 
/// filter redirects all non-canonical URL's based on the settings specified to their canonical equivalent. 
/// Note: Non-canonical URL's are not generated by this site template, it is usually external sites which are 
/// linking to your site but have changed the URL case or added/removed trailing slashes.
/// (See Google's comments at http://googlewebmastercentral.blogspot.co.uk/2010/04/to-slash-or-not-to-slash.html
/// and Bing's at http://blogs.bing.com/webmaster/2012/01/26/moving-content-think-301-not-relcanonical).
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
public class RedirectToCanonicalUrlAttribute : FilterAttribute, IAuthorizationFilter
{
   private readonly bool appendTrailingSlash;
   private readonly bool lowercaseUrls;

   #region Constructors

   /// <summary>
   /// Initializes a new instance of the <see cref="RedirectToCanonicalUrlAttribute" /> class.
   /// </summary>
   /// <param name="appendTrailingSlash">If set to <c>true</c> append trailing slashes, otherwise strip trailing 
   /// slashes.</param>
   /// <param name="lowercaseUrls">If set to <c>true</c> lower-case all URL's.</param>
   public RedirectToCanonicalUrlAttribute(
       bool appendTrailingSlash, 
       bool lowercaseUrls)
   {
       this.appendTrailingSlash = appendTrailingSlash;
       this.lowercaseUrls = lowercaseUrls;
   } 

   #endregion

   #region Public Methods

   /// <summary>
   /// Determines whether the HTTP request contains a non-canonical URL using <see cref="TryGetCanonicalUrl"/>, 
   /// if it doesn't calls the <see cref="HandleNonCanonicalRequest"/> method.
   /// </summary>
   /// <param name="filterContext">An object that encapsulates information that is required in order to use the 
   /// <see cref="RedirectToCanonicalUrlAttribute"/> attribute.</param>
   /// <exception cref="ArgumentNullException">The <paramref name="filterContext"/> parameter is <c>null</c>.</exception>
   public virtual void OnAuthorization(AuthorizationContext filterContext)
   {
       if (filterContext == null)
       {
           throw new ArgumentNullException("filterContext");
       }

       if (string.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.Ordinal))
       {
           string canonicalUrl;
           if (!this.TryGetCanonicalUrl(filterContext, out canonicalUrl))
           {
               this.HandleNonCanonicalRequest(filterContext, canonicalUrl);
           }
       }
   }

   #endregion

   #region Protected Methods

   /// <summary>
   /// Determines whether the specified URl is canonical and if it is not, outputs the canonical URL.
   /// </summary>
   /// <param name="filterContext">An object that encapsulates information that is required in order to use the 
   /// <see cref="RedirectToCanonicalUrlAttribute" /> attribute.</param>
   /// <param name="canonicalUrl">The canonical URL.</param>
   /// <returns><c>true</c> if the URL is canonical, otherwise <c>false</c>.</returns>
   protected virtual bool TryGetCanonicalUrl(AuthorizationContext filterContext, out string canonicalUrl)
   {
       bool isCanonical = true;

       canonicalUrl = filterContext.HttpContext.Request.Url.ToString();
       int queryIndex = canonicalUrl.IndexOf(QueryCharacter);

       if (queryIndex == -1)
       {
           bool hasTrailingSlash = canonicalUrl[canonicalUrl.Length - 1] == SlashCharacter;

           if (this.appendTrailingSlash)
           {
               // Append a trailing slash to the end of the URL.
               if (!hasTrailingSlash)
               {
                   canonicalUrl += SlashCharacter;
                   isCanonical = false;
               }
           }
           else
           {
               // Trim a trailing slash from the end of the URL.
               if (hasTrailingSlash)
               {
                   canonicalUrl = canonicalUrl.TrimEnd(SlashCharacter);
                   isCanonical = false;
               }
           }
       }
       else
       {
           bool hasTrailingSlash = canonicalUrl[queryIndex - 1] == SlashCharacter;

           if (this.appendTrailingSlash)
           {
               // Append a trailing slash to the end of the URL but before the query string.
               if (!hasTrailingSlash)
               {
                   canonicalUrl = canonicalUrl.Insert(queryIndex, SlashCharacter.ToString());
                   isCanonical = false;
               }
           }
           else
           {
               // Trim a trailing slash to the end of the URL but before the query string.
               if (hasTrailingSlash)
               {
                   canonicalUrl = canonicalUrl.Remove(queryIndex - 1, 1);
                   isCanonical = false;
               }
           }
       }

       if (this.lowercaseUrls)
       {
           foreach (char character in canonicalUrl)
           {
               if (char.IsUpper(character))
               {
                   canonicalUrl = canonicalUrl.ToLower();
                   isCanonical = false;
                   break;
               }
           }
       }

       return isCanonical;
   }

   /// <summary>
   /// Handles HTTP requests for URL's that are not canonical. Performs a 301 Permanent Redirect to the canonical URL.
   /// </summary>
   /// <param name="filterContext">An object that encapsulates information that is required in order to use the 
   /// <see cref="RedirectToCanonicalUrlAttribute" /> attribute.</param>
   /// <param name="canonicalUrl">The canonical URL.</param>
   protected virtual void HandleNonCanonicalRequest(AuthorizationContext filterContext, string canonicalUrl)
   {
       filterContext.Result = new RedirectResult(canonicalUrl, true);
   }

   #endregion
}

確保所有請求都被 301 重定向到正確的規範 URL 的使用範例:

filters.Add(new RedirectToCanonicalUrlAttribute(
   RouteTable.Routes.AppendTrailingSlash, 
   RouteTable.Routes.LowercaseUrls));

引用自:https://stackoverflow.com/questions/3797182