Shouldn't [RequireHttps] in MVC do a 301 perma

2019-02-08 15:54发布

I noticed on fiddler that [RequireHttps] does status code 302 redirects instead of 301. I'm not sure how this makes sense...

If you are saying that a controller [RequireHttps], then you never-ever want people to go visit the Http version of that page. So why isn't a permanent redirect... telling the search engines "please update your links permanantly to the https version of this page".

If this makes sense, and i'm right, is there a way to change it to 301 redirect?

4条回答
Root(大扎)
2楼-- · 2019-02-08 16:09

Dulan's solution put me on the right path, but the code sample didn't stop the 302 redirect from the core RequireHttpsAttribute implementation. So, I looked up the code of RequireHttpsAttribute and hacked it. Here's what I came up with:

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;
    }
}
查看更多
放我归山
3楼-- · 2019-02-08 16:18

Dulan's answer is close, but it does not work, at least with our MVC 4+ solution. But after some trial and error, we did get ours working with 301's instead of 302's. Here is the new class:

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
    }
}

The reason Dulan's answer didn't work seems to be because the Permanent property of the filterContext.Result is readonly and can only be set when the RedirectResult() is called, and the problem is that RedirectResult() is called in the base.OnAuthorization() method. So just call the base method, then override the filterContext.Result below with the second parameter of true to make the result Permanent. After doing this, we began to see 301 codes in Fiddler2.

查看更多
劳资没心,怎么记你
4楼-- · 2019-02-08 16:19

It seems as if the choice to go with 302 over 301 was a little arbitrary to begin with. However, it does not necessarily follow that every URL is going to "have" to utilize the HTTPS scheme. There very well could be a page that allows access from both HTTP or HTTPS even if it may encourage the latter. An implementation where this may occur could have some code wired up to determine whether or not to use HTTPS based on some special criteria.

As a case scenario, take a look at Gmail. Within the settings, one is capable of allowing or disallowing the HTTPS protocol throughout large portions of the application. Which code should be returned then? 301 wouldn't be accurate, as it isn't "permanent"...only a change at the behest of the user. Sadly, 302 isn't quite accurate either because a 302 error implies that there is intent to change the link back at some point in the future (related reference http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html).

Granted, Gmail is a rough example because the portions of the site that allow that option are not typically indexed by a search engine, but the possibility still exists.

And to answer your final question, if you want a different status code in ASP.NET MVC (which I assume you're using from the small syntax example), it is possible to change with a simple, custom attribute:

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;
    }
}

Now all actions that implement the attribute should return a 301 status code when accessed via the HTTP protocol.

查看更多
走好不送
5楼-- · 2019-02-08 16:26

Just a quick note that the RequireHttpsAttribute also throws an InvalidOperationException if the request is anything but a GET request. This is better served by returning a 405 Method Not Allowed, which is a much more appropriate error.

In my implementation below, I also give the user of the attribute the option of whether or not they want to redirect permanently (301) or temporarily (302). As said by @Dulan, 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>
/// 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);
    }
}
查看更多
登录 后发表回答