ASP.NET MVC requireHttps solo en producción

121

Quiero usar RequireHttpsAttribute para evitar que se envíen solicitudes HTTP no seguras a un método de acción.

C#

[RequireHttps] //apply to all actions in controller
public class SomeController 
{
    [RequireHttps] //apply to this action only
    public ActionResult SomeAction()
    {
        ...
    }
}

VB

<RequireHttps()> _
Public Class SomeController

    <RequireHttps()> _
    Public Function SomeAction() As ActionResult
        ...
    End Function

End Class

Desafortunadamente, ASP.NET Development Server no es compatible con HTTPS.

¿Cómo puedo hacer que mi aplicación ASP.NET MVC use RequireHttps cuando se publique en el entorno de producción, pero no cuando se ejecute en mi estación de trabajo de desarrollo en el servidor de desarrollo ASP.NET?

Zack Peterson
fuente
3
Pruebe con su IIS local y con IIS Express. Vea mi blog SSL blogs.msdn.com/b/rickandy/archive/2011/04/22/… y blogs.msdn.com/b/rickandy/archive/2012/03/23/…
RickAndMSFT

Respuestas:

129

Esto no ayudará si ejecuta versiones de lanzamiento en su estación de trabajo de desarrollo, pero la compilación condicional podría hacer el trabajo ...

#if !DEBUG
[RequireHttps] //apply to all actions in controller
#endif
public class SomeController 
{
    //... or ...
#if !DEBUG
    [RequireHttps] //apply to this action only
#endif
    public ActionResult SomeAction()
    {
    }

}

Actualizar

En Visual Basic, los atributos son técnicamente parte de la misma línea que la definición a la que se aplican. No puede poner declaraciones de compilación condicional dentro de una línea, por lo que se ve obligado a escribir la declaración de función dos veces, una con el atributo y otra sin ella. Sin embargo, funciona si no te importa la fealdad.

#If Not Debug Then
    <RequireHttps()> _
    Function SomeAction() As ActionResult
#Else
    Function SomeAction() As ActionResult
#End If
        ...
    End Function

Actualización 2

Varias personas han mencionado derivar RequireHttpsAttributesin proporcionar un ejemplo, así que aquí hay uno para usted. Creo que este enfoque sería mucho más limpio que el enfoque de compilación condicional, y sería mi preferencia en su posición.

DESCARGO DE RESPONSABILIDAD: No he probado este código, ni siquiera un poco, y mi VB está bastante oxidado. Todo lo que sé es que se compila. Lo escribí en base a las sugerencias de spot, queen3 y Lance Fisher. Si no funciona, al menos debería transmitir la idea general y darle un punto de partida.

Public Class RemoteRequireHttpsAttribute
    Inherits System.Web.Mvc.RequireHttpsAttribute

    Public Overrides Sub OnAuthorization(ByVal filterContext As  _
                                         System.Web.Mvc.AuthorizationContext)
        If IsNothing(filterContext) Then
            Throw New ArgumentNullException("filterContext")
        End If

        If Not IsNothing(filterContext.HttpContext) AndAlso _
            filterContext.HttpContext.Request.IsLocal Then
            Return
        End If

        MyBase.OnAuthorization(filterContext)
    End Sub

End Class

Básicamente, el nuevo atributo simplemente se cierra en lugar de ejecutar el código de autorización SSL predeterminado, si la solicitud actual es local (es decir, está accediendo al sitio a través de localhost). Puedes usarlo así:

<RemoteRequireHttps()> _
Public Class SomeController

    <RemoteRequireHttps()> _
    Public Function SomeAction() As ActionResult
        ...
    End Function

End Class

Mucho más limpio! Siempre que mi código no probado realmente funcione.

Joel Mueller
fuente
Gracias por, um, editar mi publicación para mí, Zack. Su pregunta estaba en C #, así que publiqué una respuesta de C #. No sabía que VB era relevante. ¿Alguien sabe si hay una manera de usar la compilación condicional para controlar los atributos en VB, o simplemente no es posible?
Joel Mueller
Sí, funciona para C #, y también funciona para VB, pero debe hacer una duplicación bastante fea de la definición de función / clase. Vea mi respuesta actualizada arriba.
Joel Mueller
Lo siento. Los ejemplos de código VB son cada vez más difíciles de encontrar. No pensé que importaría. He actualizado la pregunta original. ¿La compilación condicional alrededor de los atributos funciona con seguridad en C #? No he probado Esa parece una solución perfecta y elegante.
Zack Peterson
Su código RemoteRequireHttpsAttribute funciona perfectamente. Eso es mucho más elegante para VB que la compilación condicional. Gracias de nuevo Joel.
Zack Peterson el
2
Gracias, esto era exactamente lo que necesitaba. ¡Salud!
davecoulter
65

Si alguien necesita la versión C #:

using System;
using System.Web.Mvc;

namespace My.Utils
{
    public class MyRequireHttpsAttribute : RequireHttpsAttribute
    {
        public override void OnAuthorization(AuthorizationContext filterContext)
        {
            if (filterContext == null)
            {
                throw new ArgumentNullException("filterContext");
            }

            if (filterContext.HttpContext != null && filterContext.HttpContext.Request.IsLocal)
            {
                return;
            }

            base.OnAuthorization(filterContext);
        }
    }
}
mikesl
fuente
bien al leer esto y esto como medida de seguridad debemos añadir filters.Add(new MyRequireHttpsAttribute ());en FilterConfig?
shaijut
En base a esta respuesta, creé una solución para MVC 6 usando el filtro en Startup.cs o el estilo de atributo en el Controlador.
Nick Niebling
26

Derivar de RequireHttps es un buen enfoque.

Para evitar el problema por completo, también puede usar IIS en su máquina local con un certificado autofirmado. IIS es más rápido que el servidor web incorporado, y tiene la ventaja de que su entorno de desarrollo se parece más a la producción.

Scott Hanselman tiene un gran recurso sobre algunas formas de implementar HTTPS local con VS2010 e IIS Express.

Lance Fisher
fuente
ya - hasta que intentes reenviar puertos con un dispositivo Mifi wifi Verizon y descubras que el puerto 443 no está disponible para reenviar !!! # * & # * & $
Simon_Weaver
Lo que no me gusta de usar IIS en su máquina local con un certificado autofirmado es que tengo que pasar por un paso adicional de implementación para probar los cambios. Creo que si está probando algo relacionado con la seguridad de lo que tiene sentido, pero digamos que si solo está comprobando algún otro cambio menor, es difícil tener que implementar solo para evitar la incapacidad de Cassini para soportar HTTPS.
davecoulter
1
@davecoulter: use IIS express en las versiones cliente de Windows, no se necesita cassini y funcionará exactamente igual que IIS, incluida la capacidad de SSL.
Erik Funkenbusch
@Mystere Man: sí, lo descubrí desde ese comentario. Gracias por el consejo :)
davecoulter
Se debe agregar más información o enlaces sobre cómo hacer tales cosas.
stephenbayer
12

Aprovechando el sistema de filtro MVC y Global.asax.cs, supongo que podría hacer esto ...

    protected void Application_Start()
    {
      RegisterGlobalFilters(GlobalFilters.Filters);
    }

    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
      filters.Add(new HandleErrorAttribute());
      if(Config.IsProduction) //Some flag that you can tell if you are in your production environment.
      {
        filters.Add(new RequireHttpsAttribute());
      }
    }
gt124
fuente
Prefiero esta respuesta, ya que implica una verificación por vida útil de la aplicación en lugar de implementar un nuevo filtro que se ejecutará \ llamado con cada solicitud.
Abdulhameed
10

Como fue el servidor de desarrollo ASP.Net el que causó su problema en primer lugar, vale la pena señalar que Microsoft ahora tiene IIS Express , que se incluye con Visual Studio (desde VS2010 SP1). Esta es una versión reducida de IIS que es tan fácil de usar como el Servidor de desarrollo, pero admite el conjunto completo de características de IIS 7.5, incluido SSL.

Scott Hanselman tiene una publicación detallada sobre el trabajo con SSL en IIS Express .

Samuel Jack
fuente
9

¿Qué tal heredar el atributo RequireHttps en un atributo personalizado? Luego, dentro de su atributo personalizado, verifique la propiedad IsLocal de la solicitud actual para ver si la solicitud proviene de la máquina local. Si es así, no aplique la funcionalidad base. De lo contrario, llame a la operación base.

Mancha
fuente
4

Esto funcionó para mí, MVC 6 (ASP.NET Core 1.0) . El código verifica si la depuración está en desarrollo, y si no, no se requiere SSL. Todas las ediciones están en Startup.cs .

Añadir:

private IHostingEnvironment CurrentEnvironment { get; set; }

Añadir:

public Startup(IHostingEnvironment env)
{
    CurrentEnvironment = env;
}

Editar:

public void ConfigureServices(IServiceCollection services)
{
    // additional services...

    services.AddMvc(options =>
    {
        if (!CurrentEnvironment.IsDevelopment())
        {
            options.Filters.Add(typeof(RequireHttpsAttribute));
        }
    });
}
Eric Beijner
fuente
3

Si puede derivar y anular, hágalo. Si no puede: MVC viene con fuentes, simplemente tome las fuentes y cree su propio atributo [ForceHttps] que verifique IsLocal.

queen3
fuente
3

Para MVC 3 agregué mi propio FilterProvider (basado en el código que se encuentra aquí: Filtros globales y condicionales que, entre otras cosas (mostrar información de depuración para usuarios locales, etc.) decorarán todas las acciones con RequireHttpsAttributecuándo HttpContext.Request.IsLocal == false.

juhan_h
fuente
O simplemente puede agregarlo condicionalmente a la colección de filtro global cuando la solicitud es local. Tenga en cuenta que querrá verificar esto en un bloque try / catch si la aplicación está configurada para iniciarse de inmediato, ya que la solicitud puede no estar disponible.
tvanfosson
3

Después de investigar en voz alta, pude resolver este problema con IIS Express y anular el método OnAuthorization de la clase Controller (Ref. # 1). También he seguido la ruta recomendada por Hanselman (Ref. 2). Sin embargo, no estaba completamente satisfecho con estas dos soluciones debido a dos razones: 1. La OnAuthorization de Ref # 1 solo funciona en el nivel de acción, no en el nivel de clase de controlador 2. La Ref # 2 requiere mucha configuración (Win7 SDK para makecert ), comandos de netsh y, para poder usar el puerto 80 y el puerto 443, necesito iniciar VS2010 como administrador, lo que desapruebo.

Entonces, se me ocurrió esta solución que se centra en la simplicidad con las siguientes condiciones:

  1. Quiero poder utilizar el RequireHttps attbbute en la clase Controlador o en el nivel de acción

  2. Quiero que MVC use HTTPS cuando el atributo RequireHttps esté presente, y use HTTP si está ausente

  3. No quiero tener que ejecutar Visual Studio como administrador

  4. Quiero poder usar cualquier puerto HTTP y HTTPS asignado por IIS Express (Ver Nota # 1)

  5. Puedo reutilizar el certificado SSL autofirmado de IIS Express, y no me importa si veo el mensaje SSL no válido

  6. Quiero que el desarrollo, la prueba y la producción tengan exactamente la misma base de código y el mismo binario y sean tan independientes de la configuración adicional (por ejemplo, usando netsh, complemento de cert mmc, etc.) como sea posible

Ahora, con los antecedentes y la explicación fuera del camino, espero que este código ayude a alguien y ahorre algo de tiempo. Básicamente, cree una clase BaseController que herede de Controller y obtenga sus clases de controlador de esta clase base. Como has leído hasta aquí, supongo que sabes cómo hacer esto. Entonces, ¡feliz codificación!

Nota # 1: Esto se logra mediante el uso de una función útil 'getConfig' (ver código)

Ref # 1: http://puredotnetcoder.blogspot.com/2011/09/requirehttps-attribute-in-mvc3.html

Ref # 2: http://www.hanselman.com/blog/WorkingWithSSLAtDevelopmentTimeIsEasierWithIISExpress.aspx

========== Código en BaseController ===================

     #region Override to reroute to non-SSL port if controller action does not have RequireHttps attribute to save on CPU 
    // By L. Keng, 2012/08/27
    // Note that this code works with RequireHttps at the controller class or action level.
    // Credit: Various stackoverflow.com posts and http://puredotnetcoder.blogspot.com/2011/09/requirehttps-attribute-in-mvc3.html
    protected override void OnAuthorization(AuthorizationContext filterContext)
    {
        // if the controller class or the action has RequireHttps attribute
        var requireHttps = (filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes(typeof(RequireHttpsAttribute), true).Count() > 0 
                            || filterContext.ActionDescriptor.GetCustomAttributes(typeof(RequireHttpsAttribute), true).Count() > 0);
        if (Request.IsSecureConnection)
        {
            // If request has a secure connection but we don't need SSL, and we are not on a child action   
            if (!requireHttps && !filterContext.IsChildAction)
            {
                var uriBuilder = new UriBuilder(Request.Url)
                {
                    Scheme = "http",
                    Port = int.Parse(getConfig("HttpPort", "80")) // grab from config; default to port 80
                };
                filterContext.Result = this.Redirect(uriBuilder.Uri.AbsoluteUri);
            }
        }
        else
        {
            // If request does not have a secure connection but we need SSL, and we are not on a child action   
            if (requireHttps && !filterContext.IsChildAction)
            {
                var uriBuilder = new UriBuilder(Request.Url)
                {
                    Scheme = "https",
                    Port = int.Parse(getConfig("HttpsPort", "443")) // grab from config; default to port 443
                };
                filterContext.Result = this.Redirect(uriBuilder.Uri.AbsoluteUri);
            }
        }
        base.OnAuthorization(filterContext);
    }
    #endregion

    // a useful helper function to get appSettings value; allow caller to specify a default value if one cannot be found
    internal static string getConfig(string name, string defaultValue = null)
    {
        var val = System.Configuration.ConfigurationManager.AppSettings[name];
        return (val == null ? defaultValue : val);
    }

============== código final ================

En Web.Release.Config, agregue lo siguiente para borrar HttpPort y HttpsPort (para usar los valores predeterminados 80 y 443).

<appSettings>
<add key="HttpPort" value="" xdt:Transform="SetAttributes" xdt:Locator="Match(key)"/>
<add key="HttpsPort" value="" xdt:Transform="SetAttributes" xdt:Locator="Match(key)"/>
</appSettings>
Leng Keng
fuente
3

Una solución que puede utilizar tanto en la producción como en la estación de trabajo de desarrollo. Se basa en su opción de la configuración de la aplicación en web.config

<appSettings>
     <!--Use SSL port 44300 in IIS Express on development workstation-->
     <add key="UseSSL" value="44300" />
</appSettings>

Si no desea utilizar SSL, elimine la clave. Si utiliza el puerto SSL estándar 443, elimine el valor o especifique 443.

Luego use la implementación personalizada de RequireHttpsAttribute que se ocupa de su condición. En realidad, se deriva de RequireHttps y utiliza la misma implementación del método base, excepto para agregar condiciones.

public class RequireHttpsConditional : RequireHttpsAttribute
{
    protected override void HandleNonHttpsRequest(AuthorizationContext filterContext)
    {
        var useSslConfig = ConfigurationManager.AppSettings["UseSSL"];
        if (useSslConfig != null)
        {
            if (!string.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
            {
                throw new InvalidOperationException("The requested resource can only be accessed via SSL.");
            }

            var request = filterContext.HttpContext.Request;
            string url = null;
            int sslPort;

            if (Int32.TryParse(useSslConfig, out sslPort) && sslPort > 0)
            {
                url = "https://" + request.Url.Host + request.RawUrl;

                if (sslPort != 443)
                {
                    var builder = new UriBuilder(url) {Port = sslPort};
                    url = builder.Uri.ToString();
                }
            }

            if (sslPort != request.Url.Port)
            {
                filterContext.Result = new RedirectResult(url);
            }
        }
    }
}

No se olvide de decorar LogOn método en el AccountController

[RequireHttpsConditional]
[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)

y algo así en su Vista de inicio de sesión para publicar el formulario a través de https.

<% using (Html.BeginFormSecure("LogOn", "Account", new { ReturnUrl = Request.QueryString["ReturnUrl"] }, Request.IsSecureConnection, Request.Url)) { %>
Mella
fuente
Recibo este error: XMLHttpRequest no puede cargar m.XXX.com/Auth/SignIn . No hay encabezado 'Access-Control-Allow-Origin' presente en el recurso solicitado. Por lo tanto, el origen ' m.XXX.com ' no tiene acceso permitido.
Ranjith Kumar Nagiri
2

Como mencionó Joel, puede modificar la compilación utilizando la #if !DEBUGdirectiva.

Me acabo de enterar de que puede alterar el valor del símbolo DEBUG en el elemento de compilación del archivo web.config. Espero que ayude.

Jose
fuente
1

MVC 6 (ASP.NET Core 1.0):

La solución adecuada sería usar env.IsProduction () o env.IsDevelopment (). Lea más sobre la razón detrás en esta respuesta sobre cómo requerir https solo en producción .

Respuesta resumida a continuación (vea el enlace de arriba para leer más sobre las decisiones de diseño) para 2 estilos diferentes:

  1. Startup.cs - filtro de registro
  2. BaseController - estilo de atributo

Startup.cs (filtro de registro):

public void ConfigureServices(IServiceCollection services)
{
    // TODO: Register other services

    services.AddMvc(options =>
    {
        options.Filters.Add(typeof(RequireHttpsInProductionAttribute));
    });
}

BaseController.cs (estilo de atributo):

[RequireHttpsInProductionAttribute]
public class BaseController : Controller
{
    // Maybe you have other shared controller logic..
}

public class HomeController : BaseController
{
    // Add endpoints (GET / POST) for Home controller
}

RequireHttpsInProductionAttribute : los dos anteriores están utilizando atributos personalizados heredados de RequireHttpsAttribute :

public class RequireHttpsInProductionAttribute : RequireHttpsAttribute
{
    private bool IsProduction { get; }

    public RequireHttpsInProductionAttribute(IHostingEnvironment environment)
    {
        if (environment == null)
            throw new ArgumentNullException(nameof(environment));
        this.IsProduction = environment.IsProduction(); 
    }
    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        if (this.IsProduction)
            base.OnAuthorization(filterContext);
    }
    protected override void HandleNonHttpsRequest(AuthorizationContext filterContext)
    {
        if(this.IsProduction)
            base.HandleNonHttpsRequest(filterContext);
    }
}
Nick Niebling
fuente
1

Esta fue la forma más limpia para mí. En mi App_Start\FilterConfig.csarchivo Sin embargo, ya no puedo ejecutar versiones de lanzamiento.

... 
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
        if (!Web.HttpContext.Current.IsDebuggingEnabled) {
            filters.Add(new RequireHttpsAttribute());   
        }
        ...
}

Alternativamente, puede configurarlo para que solo requiera https cuando su página de error personalizada esté activada.

... 
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
        if (Web.HttpContext.Current.IsCustomErrorEnabled) {
            filters.Add(new RequireHttpsAttribute());   
        }
        ...
}
Carter Medlin
fuente
Esta es una solución fácil que funciona muy bien en MVC 5 :)
MWD