web-dev-qa-db-fra.com

ASP.NET MVC RequireHttps en production uniquement

Je souhaite utiliser RequireHttpsAttribute pour empêcher l'envoi de requêtes HTTP non sécurisées à une méthode d'action.

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

Malheureusement, ASP.NET Development Server ne prend pas en charge HTTPS.

Comment puis-je faire en sorte que mon application MVC ASP.NET utilise RequireHttps lorsqu'elle est publiée dans l'environnement de production, mais pas lorsqu'elle est exécutée sur mon poste de travail de développement sur le serveur de développement ASP.NET?

121
Zack Peterson

Cela n'aidera pas si vous exécutez des versions Release sur votre poste de travail de développement, mais la compilation conditionnelle pourrait faire le travail ...

#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()
    {
    }

}

Mise à jour

En Visual Basic, les attributs font techniquement partie de la même ligne que la définition à laquelle ils s'appliquent. Vous ne pouvez pas mettre des instructions de compilation conditionnelle dans une ligne, vous êtes donc obligé d'écrire la déclaration de fonction deux fois - une fois avec l'attribut et une fois sans. Cela fonctionne, cependant, si cela ne vous dérange pas.

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

Mise à jour 2

Plusieurs personnes ont mentionné dériver de RequireHttpsAttribute sans fournir d'exemple, alors en voici un pour vous. Je pense que cette approche serait beaucoup plus propre que l'approche de compilation conditionnelle, et ce serait ma préférence dans votre position.

AVERTISSEMENT: Je n'ai pas testé ce code, même un peu, et mon VB est assez rouillé. Tout ce que je sais, c'est qu'il compile. Je l'ai écrit sur la base des suggestions de spot, queen3 et Lance Fisher. Si cela ne fonctionne pas, cela devrait au moins transmettre l'idée générale et vous donner un point de départ.

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

Fondamentalement, le nouvel attribut se ferme au lieu d'exécuter le code d'autorisation SSL par défaut, si la demande actuelle est locale (c'est-à-dire que vous accédez au site via localhost). Vous pouvez l'utiliser comme ceci:

<RemoteRequireHttps()> _
Public Class SomeController

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

End Class

Beaucoup plus propre! À condition que mon code non testé fonctionne réellement.

129
Joel Mueller

Si quelqu'un a besoin de la version 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);
        }
    }
}
65
mikesl

Dériver de RequireHttps est une bonne approche.

Pour contourner complètement le problème, vous pouvez également utiliser IIS sur votre machine locale avec un certificat auto-signé. IIS est plus rapide que le serveur Web intégré) et vous avez l'avantage que votre environnement de développement ressemble davantage à la production.

Scott Hanselman a une grande ressource sur quelques façons d'implémenter HTTPS local avec VS2010 et IIS Express.

26
Lance Fisher

En utilisant le système de filtre MVC et Global.asax.cs, je suppose que vous pourriez le faire ...

    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());
      }
    }
11
gt124

Comme c'est le serveur de développement ASP.Net qui a causé votre problème en premier lieu, il convient de noter que Microsoft a maintenant IIS Express , qui est livré avec Visual Studio (depuis VS2010 SP1). Il s'agit d'une version réduite de IIS qui est aussi facile à utiliser que le serveur de développement, mais prend en charge l'ensemble complet des fonctionnalités de IIS 7.5, y compris SSL).

Scott Hanselman a un article détaillé sur travailler avec SSL dans IIS Express .

10
Samuel Jack

Que diriez-vous d'hériter de l'attribut RequireHttps dans un attribut personnalisé. Ensuite, à l'intérieur de votre attribut personnalisé, vérifiez la propriété IsLocal de la demande actuelle pour voir si la demande provient de la machine locale. Si c'est le cas, n'appliquez pas la fonctionnalité de base. Sinon, appelez l'opération de base.

9
spot

Cela a fonctionné pour moi, MVC 6 (ASP.NET Core 1.0). Le code vérifie si le débogage est en cours de développement, et sinon, ssl n'est pas requis. Toutes les modifications sont dans Startup.cs.

Ajouter:

private IHostingEnvironment CurrentEnvironment { get; set; }

Ajouter:

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

Modifier:

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

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

Une solution que vous pouvez utiliser aussi bien en production qu'en station de développement. Il est basé sur votre option dans les paramètres de l'application dans web.config

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

Si vous ne souhaitez pas utiliser SSL, retirez la clé. Si vous utilisez le port SSL standard 443, supprimez la valeur ou spécifiez 443.

Utilisez ensuite l'implémentation personnalisée de RequireHttpsAttribute qui prend soin de votre état. Il est dérivé en fait de RequireHttps et utilise la même implémentation de la méthode de base, sauf pour ajouter des conditions.

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

N'oubliez pas de décorer la méthode LogOn dans AccountController

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

et quelque chose comme ça dans votre LogOn Voir afin de poster un formulaire sur https.

<% using (Html.BeginFormSecure("LogOn", "Account", new { ReturnUrl = Request.QueryString["ReturnUrl"] }, Request.IsSecureConnection, Request.Url)) { %>
3
Nick

Pour MVC 3, j'ai ajouté mon propre FilterProvider (basé sur le code trouvé ici: Filtres globaux et conditionnels qui, entre autres (afficher les informations de débogage pour les utilisateurs locaux, etc.) décorera toutes les actions avec RequireHttpsAttribute quand HttpContext.Request.IsLocal == false.

3
juhan_h

Si vous pouvez dériver et remplacer - faites-le. Si vous ne pouvez pas - MVC est fourni avec les sources, prenez simplement les sources et créez votre propre attribut [ForceHttps] qui vérifie IsLocal.

3
queen3

Après avoir fait des recherches sur Aroud, j'ai pu résoudre ce problème avec IIS Express et un remplacement de la méthode OnAuthorization de la classe Controller (Ref # 1). J'ai également opté pour l'itinéraire recommandé par Hanselman (Ref # 2). Cependant, je n'étais pas complètement satisfait de ces deux solutions pour deux raisons: 1. OnAuthorization de la référence # 1 ne fonctionne qu'au niveau de l'action, pas au niveau de la classe du contrôleur 2. La référence # 2 nécessite beaucoup de configuration ( Win7 SDK pour makecert), les commandes netsh et, pour utiliser le port 80 et le port 443, je dois lancer VS2010 en tant qu'administrateur, ce que je désapprouve.

J'ai donc trouvé cette solution qui met l'accent sur la simplicité dans les conditions suivantes:

  1. Je veux pouvoir utiliser l'attribut RequireHttps au niveau de la classe ou de l'action Controller

  2. Je veux que MVC utilise HTTPS lorsque l'attribut RequireHttps est présent et utilise HTTP s'il est absent

  3. Je ne veux pas avoir à exécuter Visual Studio en tant qu'administrateur

  4. Je veux pouvoir utiliser tous les ports HTTP et HTTPS attribués par IIS Express (voir la note # 1)

  5. Je peux réutiliser le certificat SSL auto-signé de IIS Express, et je m'en fiche si je vois l'invite SSL non valide

  6. Je veux que le développement, le test et la production aient exactement la même base de code et le même binaire et aussi indépendants de la configuration supplémentaire (par exemple, en utilisant netsh, le composant logiciel enfichable mmc cert, etc.) que possible

Maintenant, avec l'arrière-plan et l'explication à l'écart, j'espère que ce code aidera quelqu'un et fera gagner du temps. Fondamentalement, créez une classe BaseController qui hérite de Controller et dérivez vos classes de contrôleur de cette classe de base. Puisque vous avez lu jusqu'ici, je suppose que vous savez comment faire cela. Alors, bon codage!

Note # 1: Ceci est réalisé par l'utilisation d'une fonction utile 'getConfig' (voir code)

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

Réf # 2: http://www.hanselman.com/blog/WorkingWithSSLAtDevelopmentTimeIsEasierWithIISExpress.aspx

========== Code dans 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);
    }

============== code de fin ================

Dans Web.Release.Config, ajoutez ce qui suit pour effacer HttpPort et HttpsPort (pour utiliser les valeurs par défaut 80 et 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>
3
Leng Keng

Comme Joel l'a mentionné, vous pouvez modifier la compilation en utilisant le #if !DEBUG directive.

Je viens de découvrir que vous pouvez modifier la valeur du symbole DEBUG dans l'élément de compilation du fichier web.config. J'espère que ça t'as aidé.

2
Jose

MVC 6 (ASP.NET Core 1.0):

La bonne solution serait d'utiliser env.IsProduction () ou env.IsDevelopment (). En savoir plus sur la raison derrière dans cette réponse sur comment exiger https uniquement en production .

Réponse condensée ci-dessous (voir le lien ci-dessus pour en savoir plus sur les décisions de conception) pour 2 styles différents:

  1. Startup.cs - filtre de registre
  2. BaseController - style d'attribut

Startup.cs (filtre de registre):

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

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

BaseController.cs (style d'attribut):

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

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

RequireHttpsInProductionAttribute : les deux ci-dessus utilisent un attribut personnalisé hérité 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);
    }
}
1
Nick Niebling

C'était la façon la plus propre pour moi. Dans mon App_Start\FilterConfig.cs fichier. Cependant, je ne peux plus exécuter les versions de versions.

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

Alternativement, vous pouvez le configurer pour ne nécessiter que https lorsque votre page d'erreur personnalisée est activée.

... 
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
        if (Web.HttpContext.Current.IsCustomErrorEnabled) {
            filters.Add(new RequireHttpsAttribute());   
        }
        ...
}
1
Carter Medlin