IE6-8无法从HTTPS网站下载文件

9
我有一个MVC .Net应用程序,其中包含返回报告文件的操作,通常是.xslx文件:
byte[] data = GetReport();
return File(data, 
    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 
    "filename.xlsx");

在测试和所有浏览器中都能很好地工作,但将其放在SSL网站上时,IE6、7和8会失败(所有适当的浏览器仍然可以正常工作),并显示以下无用错误消息:

Unable to download filename from server. Unable to open this Internet site. The requested site is either unavailable or cannot be found. Please try again later.

这在一个旧的应用程序(非MVC)中有效,而此操作则替代了该应用程序。

我们不能要求用户在本地进行任何更改 - 大约60%的用户仍在使用IE6!

我该如何使用MVC修复这个问题?

更新

进一步挖掘发现,这是IE6-8中的基本故障。根据Eric Law's IE internals blog,在SSL连接期间,IE将no-cache指令视为绝对规则。因此,它不是不缓存副本,而是认为no-cache意味着即使具有Content-Disposition:attachment和显式提示下载位置,也不应该能够将副本保存到磁盘上。

显然,这是错误的,但尽管它在IE9中得到了修复,我们仍然被困在所有IE6-8用户中。

使用MVC的操作筛选器属性会产生以下标头:

Cache-Control:no-cache, no-store, must-revalidate
Pragma:no-cache

使用Fiddler来动态更改这些内容,我们可以验证需要返回的标头。
Cache-Control:no-store, no-cache, must-revalidate

注意Cache-Control的顺序必须在no-cache之前有no-store,并且Pragma指令必须完全删除。
这是一个问题——我们广泛使用MVC的操作属性,我真的不想从头开始重写它们。即使我们可以,如果您尝试删除Pragma指令,IIS也会抛出异常。
如何使Microsoft的MVC和IIS返回Microsoft的IE6-8可以在HTTPS下处理的无缓存指令?我不希望允许响应的私有缓存(根据此similar question),也不要忽略MVC内置方法的覆盖(根据我的答案,这只是我当前最好的hack)。

@tpeczek 看更新 - 如果可能的话,我不想覆盖MVC的操作过滤器,尽管我会查看那个答案。 - Keith
@tpeczek 另外,那个问题的答案似乎是使用 Cache-Control: private,这意味着如果用户在同一会话中两次下载相同的文件,则他们将从本地浏览器缓存中获取副本而不是最新数据。 这不是解决方法。 我需要告诉浏览器不要缓存(但允许下载),并以 IE 能够理解的格式告知。 - Keith
让我们就这个问题明确一下 - 我想知道如何使MVC输出IE6-8可以理解的缓存指令,而不是将缓存设置为私有可能会产生类似的效果。我有类似的症状,但Calin的问题对我没有用处。 - Keith
2个回答

8

我想出了一个解决方法,但它肯定不是正统的 - 这是一个新的缓存属性,用来替换内置的[OutputCache]

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public sealed class IENoCacheAttribute : ActionFilterAttribute
{
    public override void OnResultExecuting(ResultExecutingContext filterContext)
    {
        if (filterContext.HttpContext.Request.IsSecureConnection &&
            string.Equals(filterContext.HttpContext.Request.Browser.Browser, "IE", StringComparison.OrdinalIgnoreCase) &&
            filterContext.HttpContext.Request.Browser.MajorVersion < 9)
        {
            filterContext.HttpContext.Response.ClearHeaders();
            filterContext.HttpContext.Response.AddHeader("cache-control", "no-store, no-cache, must-revalidate");
        }
        else
        {
            filterContext.HttpContext.Response.Cache.SetNoStore();
            filterContext.HttpContext.Response.Cache.SetCacheability(HttpCacheability.NoCache);
            filterContext.HttpContext.Response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches);
        }

        base.OnResultExecuting(filterContext);
    }
}

但这只是一个权宜之计 - 我真正想要的是扩展现有的[OutputCache]Response.Cache结构,使其具有适合旧版IE的所需输出。


2

在这方面,我有类似的处理方式,我创建了一个BaseController基类。

[OutputCache(Duration=0)]
public class BaseController : Controller
{
    //snip snip: some utility stuff and shared endpoints among all my controllers
}

这导致了IE8中上述提到的问题。然而,应用如上所示的并没有起作用。问题在于指令会删除所有标题,包括可能的Content-Disposition标题等,导致文件下载无法正确进行。
因此,我的方法是以这样一种方式覆盖默认的OutputCacheAttribute.cs,即在IE的情况下不应用任何类型的缓存标题,特别是有问题的no-cache标题。
public class EnhancedOutputCacheAttribute : OutputCacheAttribute
{

    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {

        if (!IsFileResultAndOldIE(filterContext))
            base.OnActionExecuted(filterContext);
        else
        {
            //try the best to avoid any kind of caching
            filterContext.HttpContext.Response.Cache.SetCacheability(HttpCacheability.Private);
            filterContext.HttpContext.Response.Cache.SetMaxAge(new TimeSpan(0));
            filterContext.HttpContext.Response.Cache.SetExpires(DateTime.Now.AddMinutes(-5D));
        }
    }

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        if (!IsFileResultAndOldIE(filterContext))
            base.OnActionExecuting(filterContext);
    }

    public override void OnResultExecuted(ResultExecutedContext filterContext)
    {
        if (!IsFileResultAndOldIE(filterContext))
            base.OnResultExecuted(filterContext);
    }

    public override void OnResultExecuting(ResultExecutingContext filterContext)
    {
        if (!IsFileResultAndOldIE(filterContext))
            base.OnResultExecuting(filterContext);
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="filterContext"></param>
    /// <returns><c>true</c> for FileResults and if the browser is < IE9</returns>
    private bool IsFileResultAndOldIE(dynamic filterContext)
    {
        return filterContext.Result is FileResult &&
               filterContext.HttpContext.Request.IsSecureConnection &&
               string.Equals(filterContext.HttpContext.Request.Browser.Browser, "IE", StringComparison.OrdinalIgnoreCase) &&
               filterContext.HttpContext.Request.Browser.MajorVersion < 9;
    }

}

这里有对应的代码片段:https://gist.github.com/4633225

非常有用(+1)。不幸的是,由于IE速度太慢,一次只能发出两个请求,我需要它比其他浏览器更多地缓存。 - Keith
我已经修改了这里的解决方案,以便还检查FileContentResult:bool result = (filterContext.Result is FileResult || filterContext.Result is FileContentResult) - stevieg
@stevieg:你不必显式地检查FileContentResult,因为它是从FileResult派生的。 - Markus Wolters

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