ASP.NET MVC RequireHttps 只在生产环境中使用

121

我想要使用RequireHttpsAttribute来防止未加密的HTTP请求发送到一个操作方法。

C#

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

VB

的翻译是:

VB

<RequireHttps()> _
Public Class SomeController

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

End Class

很遗憾,ASP.NET Development Server不支持HTTPS。

当我的ASP.NET MVC应用程序在生产环境中发布时,如何使用RequireHttps,但在ASP.NET Development Server上运行时不使用RequireHttps?


3
请先在本地的IIS和IIS Express中进行测试。请查看我的SSL博客http://blogs.msdn.com/b/rickandy/archive/2011/04/22/better-faster-easier-ssl-testing-for-asp-net-mvc-amp-webforms.aspx和http://blogs.msdn.com/b/rickandy/archive/2012/03/23/securing-your-asp-net-mvc-4-app-and-the-new-allowanonymous-attribute.aspx。 - RickAndMSFT
16个回答

130

如果您在开发工作站上运行发布版本,则这不会有任何帮助,但是条件编译可以完成此工作...

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

}

更新

在Visual Basic中,属性技术上属于它们适用的定义的同一行。您无法在一行内放置条件编译语句,因此必须将函数声明写两次 - 一次带属性,一次不带属性。虽然这样做很丑陋,但它确实可行。

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

更新2

有几个人提到了从RequireHttpsAttribute派生而来,但没有给出示例,所以在这里为您提供一个。我认为这种方法比条件编译方法更加清晰,如果处于您的位置,这将是我的首选。

免责声明:我没有测试过这段代码,甚至连一点也没有。此外,我的VB语言已经非常生疏了。我根据spot、queen3和Lance Fisher的建议编写了这段代码。如果它不能正常工作,它至少应该传达出一般的思路,并给您一个起点。

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

新的属性只是在当前请求是本地(即通过localhost访问站点)时退出而不运行默认的SSL授权代码。您可以像这样使用它:

<RemoteRequireHttps()> _
Public Class SomeController

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

End Class

非常干净!只要我的未经测试的代码真正起作用。


谢谢你帮我编辑帖子,Zack。你的问题是关于C#的,所以我回复了一个C#的答案。我不知道VB是否相关。有人知道在VB中是否有办法使用条件编译来控制属性吗?还是这根本不可能? - Joel Mueller
是的,它适用于C#,也适用于VB,但您必须进行一些相当丑陋的函数/类定义复制。请参见我上面更新的答案。 - Joel Mueller
抱歉,VB代码示例越来越难找了。我没想到这会有影响。我已经更新了原始问题。在C#中,围绕属性进行条件编译是否一定有效?我还没有测试过。那似乎是一个完美、优雅的解决方案。 - Zack Peterson
你的RemoteRequireHttpsAttribute代码完美地运行了。这比条件编译对于VB来说更加优雅。再次感谢Joel。 - Zack Peterson
filterContext 怎么可能为空?这只是你的防御性编程标准吗,还是有其他原因? - Simon_Weaver
这是因为我不太了解代码可能被调用的上下文,所以我想保险起见。如果您愿意,可以省略空值检查... - Joel Mueller

65

如果有人需要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);
        }
    }
}

阅读 这篇文章这篇文章 后,作为一项安全措施,我们是否应该在FilterConfig中添加filters.Add(new MyRequireHttpsAttribute()); - Shaiju T
根据这个答案,我创建了一个MVC 6的解决方案,可以在Startup.cs中使用过滤器或在控制器上使用属性样式。 - Nick Niebling

26

直到你尝试使用Mifi wifi Verizon设备进行端口转发时,才发现无法转发443端口!#&#&$ - Simon_Weaver
我不喜欢在本地机器上使用自签名证书的IIS的原因是,我必须经过额外的部署步骤来测试更改。如果您正在测试与安全相关的内容,则这是有意义的,但是如果您只是检查其他一些较小的更改,则必须部署才能绕过Cassini无法支持HTTPS的限制,这是很痛苦的。 - davecoulter
1
@davecoulter - 在Windows的客户端版本上使用IIS Express,无需Cassini,它的功能与IIS完全相同,包括具有SSL能力。 - Erik Funkenbusch
@神秘人 - 是的,自从那条评论后我已经发现了。谢谢您的提示 :) - davecoulter
需要添加更多关于如何进行这些操作的信息或链接。 - stephenbayer

12

利用MVC过滤器系统和Global.asax.cs,我认为你可以这样做...

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

我更喜欢这个答案,因为它每个应用程序生命周期只涉及一个检查,而不是实施一个新的过滤器,该过滤器将在每个单独的请求中被执行或调用。 - Abdulhameed

10
作为引起问题的ASP.Net开发服务器,值得注意的是,微软现在有IIS Express,它随Visual Studio(自VS2010 SP1以来)一起发布。这是一个简化版的IIS,使用起来像开发服务器一样容易,但支持包括SSL在内的完整功能集合IIS 7.5。
Scott Hanselman在使用IIS Express工作中处理SSL方面有详细的帖子。

9

那么将RequireHttps属性继承到自定义属性中如何?然后,在自定义属性内部,检查当前请求的IsLocal属性,以确定请求是否来自本地机器。 如果是,则不应用基本功能。否则,调用基本操作。


4
这对我很有帮助,MVC 6 (ASP.NET Core 1.0)。 代码检查是否在开发环境中进行调试,如果不是,则不需要ssl。 所有编辑都在Startup.cs中进行。
添加:
private IHostingEnvironment CurrentEnvironment { get; set; }

添加:

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

编辑:

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

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

3
在进行调研后,我通过对Controller类的OnAuthorization方法进行覆盖(Ref#1)以及采用Hanselman推荐的路线(Ref#2)解决了与IIS Express相关的问题。但是,由于以下两个原因,这两种解决方案并不能完全令我满意: 1. Ref#1的OnAuthorization只适用于操作级别,而非控制器类级别 2. Ref#2需要大量的设置(Win7 SDK for makecert)、netsh命令,并且为了使用80和443端口,我需要以管理员身份启动VS2010,这一点让我感到不满意。
所以,我提出了这个解决方案,着重于简单性,并遵循以下条件:
  1. 我希望能够在控制器类或操作级别使用 RequireHttps 属性

  2. 当存在 RequireHttps 属性时,我希望 MVC 使用 HTTPS,不存在时使用 HTTP

  3. 我不想以管理员身份运行 Visual Studio

  4. 我希望能够使用由 IIS Express 分配的任何 HTTP 和 HTTPS 端口(请参见注释#1)

  5. 我可以重用 IIS Express 的自签名 SSL 证书,并且不介意看到无效的 SSL 提示

  6. 我希望开发、测试和生产具有完全相同的代码库和二进制文件,并尽可能独立于其他设置(例如使用 netsh、mmc cert snap-in 等)

现在,有了背景和解释,我希望这段代码能够帮助某些人并节省时间。基本上,创建一个从 Controller 继承的 BaseController 类,并从此基类派生你的控制器类。既然你已经读到这里,我假设你知道如何完成这些。所以,愉快地编码吧!

注释#1:这是通过使用一个有用的函数 'getConfig'(参见代码)实现的。

Ref#1: http://puredotnetcoder.blogspot.com/2011/09/requirehttps-attribute-in-mvc3.html的参考资料。
Ref#2: http://www.hanselman.com/blog/WorkingWithSSLAtDevelopmentTimeIsEasierWithIISExpress.aspx的参考资料。
========== 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);
    }

在Web.Release.Config中添加以下内容以清除HttpPort和HttpsPort(使用默认的80和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

您可以在生产环境和开发工作站上使用的一个解决方案。它基于 web.config 应用程序设置中的选项。

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

如果不想使用 SSL,请移除密钥。如果使用标准的 SSL 端口 443,请移除值或指定为 443。
接着,使用自定义实现的 RequireHttpsAttribute,该类会处理您的条件。它实际上是从 RequireHttps 派生而来,并使用相同的基础方法实现,只需添加一些条件即可。
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);
            }
        }
    }
}

不要忘记在AccountController的LogOn方法中进行装饰。
[RequireHttpsConditional]
[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)

如果您需要通过https提交表单,可以在您的LogOn视图中添加以下内容:

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

我遇到了这个错误: XMLHttpRequest 无法加载 https://m.XXX.com/Auth/SignIn。请求的资源上没有 'Access-Control-Allow-Origin' 标头。因此,源 'http://m.XXX.com' 不允许访问。 - Ranjith Kumar Nagiri

3

MVC 3中,我添加了自己的FilterProvider(基于此处找到的代码:全局和条件Filters),其中包括(为本地用户显示调试信息等)在HttpContext.Request.IsLocal == false时,会给所有操作添加RequireHttpsAttribute修饰符。


或者,当请求是本地的时候,您可以将其有条件地添加到全局过滤器集合中。请注意,如果应用程序设置为立即启动,则需要在try/catch块中检查此操作,因为请求可能不可用。 - tvanfosson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接