Должно ли [RequireHttps] в MVC выполнить 301 постоянную переадресацию? Почему это делает 302 (плохо для SEO?)

Я заметил на скрипаче, что [RequireHttps] передает код статуса 302 вместо 301. Я не уверен, как это имеет смысл...

Если вы говорите, что контроллер [RequireHttps], то вы никогда не хотите, чтобы люди посещали Http-версию этой страницы. Итак, почему нет постоянной переадресации..., говорящей в поисковых системах, "пожалуйста, обновите свои ссылки на https-версии этой страницы".

Если это имеет смысл, и я прав, есть ли способ изменить его на 301 перенаправление?

Ответ 1

Кажется, что выбор с 302 по 301 был немного произвольным для начала. Однако не обязательно следует, что каждый URL-адрес будет "иметь", чтобы использовать схему HTTPS. Там очень хорошо может быть страница, которая позволяет получить доступ как от HTTP, так и от HTTPS, даже если это может способствовать последнему. Реализация, где это может произойти, может иметь некоторый код, подключенный для определения того, следует ли использовать HTTPS на основе некоторых специальных критериев.

В качестве сценария взгляните на Gmail. В пределах настроек один способ позволяет разрешать или запрещать протокол HTTPS на больших участках приложения. Какой код следует вернуть? 301 не будет точным, поскольку он не "постоянный"... только изменение по просьбе пользователя. К сожалению, 302 не совсем точна либо потому, что ошибка 302 подразумевает, что есть намерение изменить ссылку в какой-то момент в будущем (ссылка http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html).

Конечно, Gmail - пример, потому что части сайта, которые разрешают этот параметр, обычно не индексируются поисковой системой, но возможность все еще существует.

И чтобы ответить на ваш последний вопрос, если вам нужен другой код состояния в ASP.NET MVC (который, как я полагаю, вы используете из небольшого примера синтаксиса), можно изменить с помощью простого пользовательского атрибута:

public class MyRequireHttpsAttribute : RequireHttpsAttribute
{
    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        base.OnAuthorization(filterContext);

        if (!filterContext.HttpContext.Request.IsSecureConnection)
            filterContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.MovedPermanently;
    }
}

Теперь все действия, реализующие атрибут, должны возвращать код статуса 301 при доступе по протоколу HTTP.

Ответ 2

Ответ Dulan близок, но он не работает, по крайней мере, с нашим решением MVC 4+. Но после некоторых проб и ошибок мы действительно работали с 301 вместо 302. Вот новый класс:

public class CustomRequireHttpsAttribute : RequireHttpsAttribute
{
    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        #if !DEBUG
        base.OnAuthorization(filterContext);

        if (!filterContext.HttpContext.Request.IsSecureConnection)
        {
            string url = "https://" + filterContext.HttpContext.Request.Url.Host + filterContext.HttpContext.Request.RawUrl;
            filterContext.Result = new RedirectResult(url, true);
        }
        #endif
    }
}

Причина, по которой ответ Dulan не работает, по-видимому, заключается в том, что свойство Permanent filterContext.Result является readonly и может быть установлено только при вызове RedirectResult(), и проблема в том, что RedirectResult() вызывается в методе base.OnAuthorization(). Поэтому просто вызовите базовый метод, затем переопределите filterContext.Result ниже со вторым параметром true, чтобы сделать результат Permanent. После этого мы начали видеть 301 код в Fiddler2.

Ответ 3

Решение Dulan поставило меня на правильный путь, но образец кода не остановил перенаправление 302 из основной реализации RequireHttpsAttribute. Итак, я просмотрел код RequireHttpsAttribute и взломал его. Вот что я придумал:

using System.Net;
using System.Web.Mvc;
using System;
using System.Diagnostics.CodeAnalysis;

[SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "Unsealed because type contains virtual extensibility points.")]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public class RequireHttps301Attribute : FilterAttribute, IAuthorizationFilter 
{

    public virtual void OnAuthorization(AuthorizationContext filterContext) 
    {
        if (filterContext == null) {
            throw new ArgumentNullException("filterContext");
        }

        if (!filterContext.HttpContext.Request.IsSecureConnection) {
            HandleNonHttpsRequest(filterContext);
        }
    }

    protected virtual void HandleNonHttpsRequest(AuthorizationContext filterContext) 
    {
        // only redirect for GET requests, otherwise the browser might not propagate the verb and request
        // body correctly.

        if (!String.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)) {
            throw new InvalidOperationException("Only redirect for GET requests, otherwise the browser might not propagate the verb and request body correctly.");
        }

        // redirect to HTTPS version of page
        string url = "https://" + filterContext.HttpContext.Request.Url.Host + filterContext.HttpContext.Request.RawUrl;
        //what mvc did to redirect as a 302 
        //filterContext.Result = new RedirectResult(url);

        //what I did to redirect as a 301
        filterContext.HttpContext.Response.StatusCode =  (int)HttpStatusCode.MovedPermanently;
        filterContext.HttpContext.Response.RedirectLocation = url;
    }
}

Ответ 4

Просто помните, что RequireHttpsAttribute также генерирует исключение InvalidOperationException, если запрос является чем угодно, кроме запроса GET. Это лучше обслуживается возвратом 405 Method Not Allowed, что является гораздо более подходящей ошибкой.

В моей реализации ниже я также даю пользователю атрибута возможность того, хотите ли они перенаправить навсегда (301) или временно (302). Как сказал @Dulan, вы должны выполнить 301 постоянную переадресацию, если только страница может быть доступна только HTTPS и временная переадресация 302, если к странице можно получить доступ через HTTP или HTTPS.

/// <summary>
/// Represents an attribute that forces an unsecured HTTP request to be re-sent over HTTPS.
/// <see cref="System.Web.Mvc.RequireHttpsAttribute"/> performs a 302 Temporary redirect from a HTTP URL to a HTTPS URL.
/// This filter gives you the option to perform a 301 Permanent redirect or a 302 temporary redirect.
/// You should perform a 301 permanent redirect if the page can only ever be accessed by HTTPS and a 302 temporary redirect if
/// the page can be accessed over HTTP or HTTPS.
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
public class RedirectToHttpsAttribute : FilterAttribute, IAuthorizationFilter
{
    private readonly bool permanent;

    /// <summary>
    /// Initializes a new instance of the <see cref="RedirectToHttpsAttribute"/> class.
    /// </summary>
    /// <param name="permanent">if set to <c>true</c> the redirection should be permanent; otherwise, <c>false</c>.</param>
    public RedirectToHttpsAttribute(bool permanent)
    {
        this.permanent = permanent;
    }

    /// <summary>
    /// Gets a value that indicates whether the redirection should be permanent.
    /// </summary>
    /// <value>
    /// <c>true</c> if the redirection should be permanent; otherwise, <c>false</c>.
    /// </value>
    public bool Permanent
    {
        get { return this.permanent; }
    }

    /// <summary>
    /// Determines whether a request is secured (HTTPS) and, if it is not, calls the <see cref="HandleNonHttpsRequest"/> method.
    /// </summary>
    /// <param name="filterContext">An object that encapsulates information that is required in order to use the <see cref="System.Web.Mvc.RequireHttpsAttribute"/> attribute.</param>
    /// <exception cref="System.ArgumentNullException">The filterContext parameter is null.</exception>
    public virtual void OnAuthorization(AuthorizationContext filterContext)
    {
        if (filterContext == null)
        {
            throw new ArgumentNullException("filterContext");
        }

        if (!filterContext.HttpContext.Request.IsSecureConnection)
        {
            this.HandleNonHttpsRequest(filterContext);
        }
    }

    /// <summary>
    /// Handles unsecured HTTP requests that are sent to the action method.
    /// </summary>
    /// <param name="filterContext">An object that encapsulates information that is required in order to use the <see cref="System.Web.Mvc.RequireHttpsAttribute"/> attribute.</param>
    /// <exception cref="System.InvalidOperationException">The HTTP request contains an invalid transfer method override. All GET requests are considered invalid.</exception>
    protected virtual void HandleNonHttpsRequest(AuthorizationContext filterContext)
    {
        // Only redirect for GET requests, otherwise the browser might not propagate the verb and request body correctly.
        if (!string.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
        {
            // The RequireHttpsAttribute throws an InvalidOperationException. Some bots and spiders make HEAD requests (to reduce bandwidth) 
            // and we don’t want them to see a 500-Internal Server Error. A 405 Method Not Allowed would be more appropriate.
            throw new HttpException((int)HttpStatusCode.MethodNotAllowed, "Method Not Allowed");
        }

        string url = "https://" + filterContext.HttpContext.Request.Url.Host + filterContext.HttpContext.Request.RawUrl;
        filterContext.Result = new RedirectResult(url, this.permanent);
    }
}