ASP.NET MVC RequireHttps tylko w środowisku produkcyjnym

121

Chcę użyć RequireHttpsAttribute, aby zapobiec wysyłaniu niezabezpieczonych żądań HTTP do metody akcji.

DO#

[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

Niestety serwer programistyczny ASP.NET nie obsługuje protokołu HTTPS.

Jak sprawić, by moja aplikacja ASP.NET MVC korzystała z RequireHttps po opublikowaniu w środowisku produkcyjnym, ale nie w przypadku uruchomienia na mojej deweloperskiej stacji roboczej na serwerze programistycznym ASP.NET?

Zack Peterson
źródło
3
Przetestuj za pomocą lokalnych usług IIS i usług IIS Express. Zobacz mój blog SSL blogs.msdn.com/b/rickandy/archive/2011/04/22/ ... i blogs.msdn.com/b/rickandy/archive/2012/03/23/ ...
RickAndMSFT

Odpowiedzi:

129

To nie pomoże, jeśli uruchomisz kompilacje wydania na swojej stacji roboczej, ale kompilacja warunkowa może załatwić sprawę ...

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

}

Aktualizacja

W języku Visual Basic atrybuty są technicznie częścią tego samego wiersza, co definicja, do której się odnoszą. Nie możesz umieścić instrukcji kompilacji warunkowej w linii, więc musisz dwukrotnie napisać deklarację funkcji - raz z atrybutem, a raz bez. Ale działa, jeśli nie przeszkadza ci brzydota.

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

Zaktualizuj 2

Kilka osób wspomniało o pochodzeniu z programu RequireHttpsAttributebez podania przykładu, więc oto jeden dla Ciebie. Myślę, że to podejście byłoby znacznie czystsze niż podejście kompilacji warunkowej i na twoim stanowisku wolałbym to.

ZRZECZENIE SIĘ: Nie testowałem tego kodu, nawet trochę, a mój VB jest dość zardzewiały. Wiem tylko, że się kompiluje. Napisałem go w oparciu o sugestie spot, queen3 i Lance Fisher. Jeśli to nie zadziała, powinno przynajmniej przekazać ogólną ideę i dać punkt wyjścia.

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

Zasadniczo nowy atrybut po prostu kończy działanie, zamiast uruchamiać domyślny kod autoryzacji SSL, jeśli bieżące żądanie jest lokalne (to znaczy uzyskujesz dostęp do witryny za pośrednictwem hosta lokalnego). Możesz go używać w ten sposób:

<RemoteRequireHttps()> _
Public Class SomeController

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

End Class

Dużo czystsze! Pod warunkiem, że mój niesprawdzony kod faktycznie działa.

Joel Mueller
źródło
Dzięki za edycję mojego posta, Zack. Twoje pytanie było w C #, więc opublikowałem odpowiedź C #. Nie wiedziałem, że VB ma znaczenie. Czy ktoś wie, czy istnieje sposób na użycie kompilacji warunkowej do kontrolowania atrybutów w VB, czy jest to po prostu niemożliwe?
Joel Mueller
Tak, działa dla C # i działa również dla VB, ale musisz zrobić raczej brzydkie powielenie definicji funkcji / klasy. Zobacz moją zaktualizowaną odpowiedź powyżej.
Joel Mueller
Przepraszam. Próbki kodu VB są coraz trudniejsze do zdobycia. Nie sądziłem, że to będzie miało znaczenie. Zaktualizowałem oryginalne pytanie. Czy kompilacja warunkowa wokół atrybutów na pewno działa w języku C #? Nie testowałem. Wydaje się, że to idealne, eleganckie rozwiązanie.
Zack Peterson
Twój kod RemoteRequireHttpsAttribute działa doskonale. To znacznie bardziej eleganckie w przypadku VB niż kompilacja warunkowa. Jeszcze raz dziękuję Joel.
Zack Peterson
2
Dzięki - to było dokładnie to, czego potrzebowałem. Twoje zdrowie!
davecoulter
65

Jeśli ktoś potrzebuje wersji 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
źródło
OK po przeczytaniu tego i to ze względów bezpieczeństwa należy dodać filters.Add(new MyRequireHttpsAttribute ());w FilterConfig?
shaijut
Na podstawie tej odpowiedzi stworzyłem rozwiązanie dla MVC 6, używając filtru w Startup.cs lub stylu atrybutu na kontrolerze.
Nick Niebling,
26

Wyprowadzanie z RequireHttps to dobre podejście.

Aby całkowicie obejść ten problem, możesz użyć usług IIS na komputerze lokalnym z certyfikatem z podpisem własnym. Usługi IIS są szybsze niż wbudowany serwer internetowy, a masz tę zaletę, że środowisko programistyczne bardziej przypomina produkcję.

Scott Hanselman ma świetne źródło informacji na temat kilku sposobów implementacji lokalnego protokołu HTTPS w VS2010 i IIS Express.

Lance Fisher
źródło
tak - dopóki nie spróbujesz przekierować portów za pomocą urządzenia Mifi wifi Verizon i nie stwierdzisz, że port 443 nie jest dostępny do przekierowania !!! # * & # * & $
Simon_Weaver
To, co mi się nie podoba w używaniu usług IIS na komputerze lokalnym z certyfikatem z podpisem własnym, polega na tym, że muszę przejść dodatkowy etap wdrażania, aby przetestować zmiany. Myślę, że jeśli testujesz coś związanego z bezpieczeństwem, niż ma to sens, ale powiedz, że jeśli sprawdzasz tylko jakąś inną drobną zmianę, to jest ból, który trzeba wdrożyć tylko po to, aby ominąć niezdolność Cassini do obsługi HTTPS.
davecoulter
1
@davecoulter - używaj IIS express w klienckich wersjach systemu Windows, nie jest potrzebne cassini i będzie działać dokładnie tak samo, jak IIS, w tym z obsługą protokołu SSL.
Erik Funkenbusch
@Mystere Man - tak, dowiedziałem się o tym od czasu tego komentarza. Dzięki za cynk :)
davecoulter
należy dodać więcej informacji lub link o tym, jak się do tego zabrać.
stephenbayer
12

Wykorzystując system filtrów MVC i Global.asax.cs, zakładam, że możesz to zrobić ...

    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
źródło
Wolę tę odpowiedź, ponieważ wiąże się ona z jednym sprawdzeniem na okres życia aplikacji, a nie implementacją nowego filtru, który będzie wykonywany \ wywoływany przy każdym żądaniu.
Abdulhameed
10

Ponieważ to właśnie serwer deweloperski ASP.Net spowodował problem w pierwszej kolejności, warto zauważyć, że Microsoft ma teraz IIS Express , który jest dostarczany z Visual Studio (od VS2010 SP1). Jest to okrojona wersja usług IIS, która jest równie łatwa w użyciu jak serwer deweloperski, ale obsługuje pełny zestaw funkcji usług IIS 7.5, w tym SSL.

Scott Hanselman opublikował szczegółowy post dotyczący pracy z protokołem SSL w IIS Express .

Samuel Jack
źródło
9

Co powiesz na dziedziczenie atrybutu RequireHttps w atrybucie niestandardowym. Następnie w atrybucie niestandardowym sprawdź właściwość IsLocal bieżącego żądania, aby sprawdzić, czy żądanie pochodzi z komputera lokalnego. Jeśli tak, nie stosuj podstawowej funkcji. W przeciwnym razie wywołaj operację bazy.

miejsce
źródło
4

To zadziałało dla mnie, MVC 6 (ASP.NET Core 1.0) . Kod sprawdza, czy debugowanie jest w trakcie opracowywania, a jeśli nie, protokół SSL nie jest wymagany. Wszystkie zmiany znajdują się w Startup.cs .

Dodaj:

private IHostingEnvironment CurrentEnvironment { get; set; }

Dodaj:

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

Edytować:

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

    services.AddMvc(options =>
    {
        if (!CurrentEnvironment.IsDevelopment())
        {
            options.Filters.Add(typeof(RequireHttpsAttribute));
        }
    });
}
Eric Beijner
źródło
3

Jeśli możesz wyprowadzić i nadpisać - zrób to. Jeśli nie możesz - MVC zawiera źródła, po prostu weź źródła i utwórz własny atrybut [ForceHttps], który sprawdza IsLocal.

królowa3
źródło
3

Do MVC 3 dodałem własnego FilterProvider (na podstawie kodu znalezionego tutaj: filtry globalne i warunkowe, które między innymi (wyświetlanie informacji o debugowaniu dla lokalnych użytkowników itp.) Będą dekorować wszystkie akcje RequireHttpsAttributekiedy HttpContext.Request.IsLocal == false.

juhan_h
źródło
Lub możesz po prostu warunkowo dodać go do globalnej kolekcji filtrów, gdy żądanie jest lokalne. Pamiętaj, że będziesz chciał to sprawdzić w bloku try / catch, jeśli aplikacja jest ustawiona na natychmiastowe uruchomienie, ponieważ żądanie może być niedostępne.
tvanfosson
3

Po wielu badaniach udało mi się rozwiązać ten problem za pomocą IIS Express i nadpisania metody OnAuthorization klasy kontrolera (Ref # 1). Poszedłem też trasą polecaną przez Hanselmana (Ref # 2). Jednak nie byłem w pełni usatysfakcjonowany tymi dwoma rozwiązaniami z dwóch powodów: 1. Autoryzacja OnAuthorization Ref # 1 działa tylko na poziomie akcji, a nie na poziomie klasy kontrolera 2. Ref # 2 wymaga dużo konfiguracji (Win7 SDK dla makecert ), polecenia netsh i, aby użyć portu 80 i portu 443, muszę uruchomić VS2010 jako administrator, na co marszczę brwi.

Tak więc wymyśliłem to rozwiązanie, które stawia na prostotę przy następujących warunkach:

  1. Chcę móc używać attbbute RequireHttps na poziomie klasy kontrolera lub akcji

  2. Chcę, aby MVC używał protokołu HTTPS, gdy atrybut RequireHttps jest obecny, i protokołu HTTP, jeśli go nie ma

  3. Nie chcę uruchamiać programu Visual Studio jako administrator

  4. Chcę mieć możliwość korzystania z dowolnych portów HTTP i HTTPS przypisanych przez IIS Express (patrz uwaga nr 1)

  5. Mogę ponownie użyć samopodpisanego certyfikatu SSL programu IIS Express i nie obchodzi mnie, czy widzę nieprawidłowy monit SSL

  6. Chcę, aby tworzenie, testowanie i produkcja miały dokładnie taką samą bazę kodu i ten sam plik binarny oraz były niezależne od dodatkowej konfiguracji (np. Przy użyciu netsh, przystawki mmc cert itp.)

Teraz, po usunięciu tła i wyjaśnień, mam nadzieję, że ten kod pomoże komuś i zaoszczędzi trochę czasu. Zasadniczo utwórz klasę BaseController, która dziedziczy po kontrolerze, i wyprowadź klasy kontrolera z tej klasy bazowej. Ponieważ przeczytałeś tak daleko, zakładam, że wiesz, jak to zrobić. Życzymy miłego kodowania!

Uwaga nr 1: Osiąga się to za pomocą użytecznej funkcji „getConfig” (patrz kod)

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

Nr ref. 2: http://www.hanselman.com/blog/WorkingWithSSLAtDevelopmentTimeIsEasierWithIISExpress.aspx

========== Kod w 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);
    }

============== kod końcowy ================

W Web.Release.Config dodaj następujący kod, aby wyczyścić HttpPort i HttpsPort (aby użyć domyślnych 80 i 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
źródło
3

Jedno rozwiązanie, którego możesz używać zarówno na produkcji, jak i na stanowiskach programistycznych. Opiera się na opcji z ustawień aplikacji w web.config

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

Jeśli nie chcesz używać SSL, usuń klucz. Jeśli używasz standardowego portu SSL 443, usuń wartość lub podaj 443.

Następnie użyj niestandardowej implementacji RequireHttpsAttribute, która dba o Twój stan. W rzeczywistości pochodzi z RequireHttps i używa tej samej implementacji metody podstawowej, z wyjątkiem dodawania warunków.

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

Nie zapomnij udekorować metody LogOn w AccountController

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

i coś takiego w widoku LogOn, aby wysłać formularz przez https.

<% using (Html.BeginFormSecure("LogOn", "Account", new { ReturnUrl = Request.QueryString["ReturnUrl"] }, Request.IsSecureConnection, Request.Url)) { %>
Nacięcie
źródło
Otrzymuję ten błąd: XMLHttpRequest nie może załadować m.XXX.com/Auth/SignIn . Żądany zasób nie zawiera nagłówka „Access-Control-Allow-Origin”. Dlatego źródłom.XXX.com ” nie ma dostępu.
Ranjith Kumar Nagiri
2

Jak wspomniał Joel, możesz zmienić kompilację za pomocą #if !DEBUGdyrektywy.

Właśnie się dowiedziałem, że możesz zmienić wartość symbolu DEBUG w elemencie kompilacji pliku web.config. Mam nadzieję, że to pomoże.

Jose
źródło
1

MVC 6 (ASP.NET Core 1.0):

Właściwym rozwiązaniem byłoby użycie env.IsProduction () lub env.IsDevelopment (). Przeczytaj więcej o powodach w tej odpowiedzi, jak wymagać protokołu HTTPS tylko w środowisku produkcyjnym .

Poniżej skrócona odpowiedź (patrz link powyżej, aby przeczytać więcej o decyzjach projektowych) dla 2 różnych stylów:

  1. Startup.cs - zarejestruj filtr
  2. BaseController - styl atrybutu

Startup.cs (filtr rejestru):

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

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

BaseController.cs (styl atrybutu):

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

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

RequireHttpsInProductionAttribute : Oba powyższe używają atrybutu niestandardowego dziedziczącego po 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
źródło
1

To był dla mnie najczystszy sposób. W moim App_Start\FilterConfig.cspliku. Nie można już jednak uruchamiać kompilacji wydań.

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

Alternatywnie możesz ustawić, aby wymagał protokołu HTTPS tylko wtedy, gdy włączona jest niestandardowa strona błędu.

... 
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
        if (Web.HttpContext.Current.IsCustomErrorEnabled) {
            filters.Add(new RequireHttpsAttribute());   
        }
        ...
}
Carter Medlin
źródło
To proste rozwiązanie, które świetnie sprawdza się w MVC 5 :)
MWD