【问题标题】:How to correctly canonicalize a URL in an ASP.NET MVC application?如何正确规范化 ASP.NET MVC 应用程序中的 URL?
【发布时间】:2010-09-26 09:19:26
【问题描述】:

我正在尝试找到一种很好的通用方法来规范化 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]*" 约束添加到控制器和操作参数(以及任何其他字符串路由参数),但我认为这会导致路由不匹配。此外,因为小写规则没有在路由级别应用,所以可能会在我的页面中生成包含大写字母的链接,这看起来很糟糕。

我在这里忽略了什么明显的东西吗?

【问题讨论】:

  • 有一种方法可以通过全局过滤器在 MVC3 中获得站点范围,但在小于 MVC3 的情况下,您需要创建一个基本控制器,将属性应用于该控制器并从中派生所有控制器。我得问一下这个用例是什么?
  • 搜索引擎优化。确保来自格式不正确的链接的蜘蛛被(永久)重定向到正确的链接——小写字母实际上只是一个旁白。 en.wikipedia.org/wiki/Canonicalization#Search_Engines_and_SEO

标签: .net asp.net-mvc-2 url-routing canonical-link


【解决方案1】:

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。完整用法和源代码请参考我的blog postRedirectToCanonicalUrlAttribute过滤器:

/// <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));

【讨论】:

  • 当您应该使用操作过滤器时,为什么还要使用授权过滤器?培根不是火腿是有原因的。
  • IAuthorizationFilter 是正确的选择。请参阅this 答案。过滤器在请求管道中较早执行。理想情况下,如果我们要重定向用户,我们应该尽早完成。
  • 这并没有说明什么时候应该使用授权过滤器而不是操作过滤器。我认为在这种情况下我们应该同意不同意。
  • 很公平。 RequireHttpsAttribute 也使用IAuthorizationFilter 来执行重定向。是性能还是命名,取决于开发人员的喜好。
  • 那是因为它与安全有关。您的规范 URL 重定向没有。
【解决方案2】:

对于默认 ASP.NET MVC 路由的宽松性质、忽略字母大小写、尾部斜杠等,我也感到同样“痒”。和你一样,我想要一个通用的解决方案,最好是作为我的应用程序中的路由逻辑。

在网上到处搜索后,没有找到有用的库,我决定自己推出一个。结果是Canonicalize,一个补充了 ASP.NET 路由引擎的开源类库。

您可以通过 NuGet 安装库:Install-Package Canonicalize

并在您的路线注册中:routes.Canonicalize().Lowercase();

除了小写字母之外,包中还包含其他几个 URL 规范化策略。强制www域前缀打开或关闭,强制特定主机名,尾部斜杠等。添加自定义URL规范化策略也很容易,我非常愿意接受补丁添​​加更多策略到“官方” 规范化分布。

我希望你或其他人会觉得这很有帮助,即使这个问题已经存在一年了:)

【讨论】:

  • 一定会去看看的。
  • 非常适合重定向到与 ssl 证书匹配的主机名 - 谢谢
  • 优秀的图书馆,正是我想要的。谢谢。
【解决方案3】:

以下是我如何在 MVC2 中创建规范 URL。我使用 IIS7 重写模块 v2 将我的所有 URL 设为小写,并去除尾部斜杠,因此不需要从我的代码中执行此操作。 (Full blog post)

将其添加到 head 部分的母版页中,如下所示:

<%=ViewData["CanonicalURL"] %>
<!--Your other head info here-->

创建过滤器属性 (CanonicalURL.cs):

public class CanonicalURL : ActionFilterAttribute
{
    public string Url { get; private set; }

    public CanonicalURL(string url)
    {
       Url = url;
    }

    public override void OnResultExecuting(ResultExecutingContext filterContext)
    {
        string fullyQualifiedUrl = "http://www.example.com" + this.Url;
        filterContext.Controller.ViewData["CanonicalUrl"] = @"<link rel='canonical' href='" + fullyQualifiedUrl + "' />";
        base.OnResultExecuting(filterContext);
    }
}

从你的行动中调用它:

[CanonicalURL("Contact-Us")]
public ActionResult Index()
 {
      ContactFormViewModel contact = new ContactFormViewModel(); 
      return View(contact);
}

有关搜索引擎相关帖子的其他一些有趣文章,请查看 Matt Cutts 博客

【讨论】:

  • 我猜这里有错误。看看 SO,如果你在 URL 中编辑 title 它仍然会重定向到这里。使用您的代码,我猜它会呈现错误的 URL。无论路径如何,SO 都会呈现相同的效果
  • URL 是从动作中注入的。它的工作不仅仅是为了表明这是该资源的主要 ​​url 进行重定向。
  • 我喜欢这个想法,比尝试从路线或类似的黑客攻击中自动确定它要整洁得多。 +1 AAA 会再次发表评论
  • 感谢您的评论,希望对您有所帮助! :)
猜你喜欢
  • 1970-01-01
  • 2011-02-24
  • 2011-05-09
  • 2020-03-04
  • 2017-01-28
  • 1970-01-01
  • 1970-01-01
  • 2018-01-02
  • 2012-05-22
相关资源
最近更新 更多