【问题标题】:AJAX and FormsAuthentication, how prevent FormsAuthentication overrides HTTP 401?AJAX 和 FormsAuthentication,如何防止 FormsAuthentication 覆盖 HTTP 401?
【发布时间】:2011-11-23 20:08:59
【问题描述】:

在一个配置了 FormsAuthentication 的应用程序中,当用户在没有 auth cookie 或使用过期 cookie 的情况下访问受保护的页面时,ASP.NET 会发出 HTTP 401 Unauthorized,然后 FormsAuthentication 模块会在请求结束之前拦截此响应,并且将其更改为找到 HTTP 302,设置 HTTP 标头“位置:/path/loginurl”以将用户代理重定向到登录页面,然后浏览器转到该页面并检索不受保护的登录页面,获得 HTTP 200 OK。

这确实是一个非常好的主意,当时还没有考虑 AJAX。

现在我的应用程序中有一个返回 JSON 数据的 url,它需要对用户进行身份验证。一切正常,问题是如果 auth cookie 过期,当我的客户端代码调用服务器时,它将通过登录页面的 html 获得 HTTP 200 OK,而不是 HTTP 401 Unauthorized(因为前面解释过)。然后我的客户端尝试将登录页面 html 解析为 json,但失败了。

那么问题是:如何处理来自客户端的过期身份验证?应对这种情况的最优雅的解决方案是什么?我需要知道调用何时成功,我想使用 HTTP 语义来实现。

是否可以以安全的跨浏览器方式从客户端读取自定义 HTTP 标头? 如果请求是 AJAX 请求,有没有办法告诉 FormsAuthenticationModule 不执行重定向? 有没有办法像覆盖 HTTP 请求方法一样使用 HTTP 标头覆盖 HTTP 状态?

我需要表单认证,我想避免重写那个模块或编写我自己的表单认证模块。

问候。

【问题讨论】:

标签: asp.net ajax http jquery forms-authentication


【解决方案1】:

我在实施accepted answer 时遇到问题。主要是,我的错误日志充满了Server cannot set status after HTTP headers have been sent 错误。

我尝试实施accepted answer 来质疑Server cannot set status after HTTP headers have been sent IIS7.5,再次没有成功。

谷歌搜索了一下,我偶然发现了SuppressFormsAuthenticationRedirect property

如果您的 .Net 版本 >= 4.5,则可以将以下代码添加到自定义 AuthorizeAttribute 类的 HandleUnauthorizedRequest 方法中。

public sealed class CustomAuthorizeAttribute : AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        if (filterContext.HttpContext.Request.IsAjaxRequest())
        {
            filterContext.HttpContext.Response.SuppressFormsAuthenticationRedirect = true;
            filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
            base.HandleUnauthorizedRequest(filterContext);
            return;
        }

        base.HandleUnauthorizedRequest(filterContext);
        return;
    }
}

重要的部分是if 块。如果您在 .Net 4.5 上并且已经有自定义授权,这是最简单的事情。

【讨论】:

    【解决方案2】:

    我遇到了同样的问题,不得不在 MVC 中使用自定义属性。您可以轻松地将其调整为在 Web 表单中工作,如果您的所有页面都从某个基本页面继承(MVC 中的全局属性允许相同的事情 - 覆盖所有控制器/操作的 OnAuthorization 方法),则可以覆盖基本页面中页面的授权申请)

    这是属性的样子:

    public class AjaxAuthorizationAttribute : FilterAttribute, IAuthorizationFilter
        {
            public void OnAuthorization(AuthorizationContext filterContext)
            {
                if (filterContext.HttpContext.Request.IsAjaxRequest()
                    && !filterContext.HttpContext.User.Identity.IsAuthenticated
                    && (filterContext.ActionDescriptor.GetCustomAttributes(typeof(AuthorizeAttribute), true).Count() > 0
                    || filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes(typeof(AuthorizeAttribute), true).Count() > 0))
                {
                    filterContext.HttpContext.SkipAuthorization = true;
                    filterContext.HttpContext.Response.Clear();
                    filterContext.HttpContext.Response.StatusCode = (int)System.Net.HttpStatusCode.Unauthorized;
                    filterContext.Result = new HttpUnauthorizedResult("Unauthorized");
                    filterContext.Result.ExecuteResult(filterContext.Controller.ControllerContext);
                    filterContext.HttpContext.Response.End();
                }
            }
        }
    

    请注意,您需要调用 HttpContext.Response.End();否则您的请求将被重定向到登录(因此我失去了一些头发)。

    在客户端,我使用了 jQuery ajaxError 方法:

    var lastAjaxCall = { settings: null, jqXHR: null };
    var loginUrl = "yourloginurl";
    
    //...
    //...
    
    $(document).ready(function(){
        $(document).ajaxError(function (event, jqxhr, settings) {
                if (jqxhr.status == 401) {
                    if (loginUrl) {
                        $("body").prepend("<div class='loginoverlay'><div class='full'></div><div class='iframe'><iframe id='login' src='" + loginUrl + "'></iframe></div></div>");
                        $("div.loginoverlay").show();
                        lastAjaxCall.jqXHR = jqxhr;
                        lastAjaxCall.settings = settings;
                    }
                }
        }
    
    }
    

    这显示了在当前页面的 iframe 中登录(看起来用户被重定向但你可以让它不同),当登录成功时,这个弹出窗口被关闭,并重新发送原始 ajax 请求:

    if (lastAjaxCall.settings) {
            $.ajax(lastAjaxCall.settings);
            lastAjaxCall.settings = null;
        }
    

    这允许您的用户在会话到期时登录,而不会丢失他们在最后显示的表单中输入的任何工作或数据。

    【讨论】:

    • 好。实际上我正在使用MVC。我有自己的身份验证过滤器,所以我只需添加几行这样的“如果未通过身份验证并且它是 AJAX 调用:跳过身份验证,返回 401 并结束连接”。而且效果很好!谢谢。
    • 不错。正是我需要的。仅供参考,在 mvc 中使用全局操作过滤器将它们添加到 global.asax 中的RegisterGlobalFilters。见Global Action Filters
    • 此解决方案不安全,因为它不能正确处理缓存。 weblogs.asp.net/jgalloway/archive/2012/05/04/… 这里有一些解释,我建议进一步阅读 AuthorizeAttribute 的源代码 - 那里有很多处理缓存的代码,这是有充分理由的。
    • 我不相信浏览器会缓存 401 错误 :) 其余代码与授权无关,Galloway 的文章处理的是自定义身份验证/授权过程。这里没有自定义,此代码的作用是检查请求是否为 ajax,如果是则返回空的 401 响应而不是登录页面,因此您可以在 iframe 中显示登录页面,或者您希望防止用户丢失工作上下文和任何他所在页面的内容。授权还是使用mvc的AuthorizeAttribute进行的,如果没有AuthorizeAttribute,这段代码任何一段都不会执行。
    【解决方案3】:

    我从其他帖子中大量窃取了这个答案,但一个想法可能是将HttpModuleintercept the redirect 实现到登录页面(该链接上的说明)。

    您还可以修改该示例 HttpModule 以仅在通过 AJAX 发出请求时才拦截重定向,前提是当请求不是通过 AJAX 发出时默认行为是正确的:

    Detect ajax call, ASP.net

    所以大致如下:

    class AuthRedirectHandler : IHttpModule
    {
        #region IHttpModule Members
    
        public void Dispose()
        {
    
        }
    
        public void Init(HttpApplication context)
        {
            context.EndRequest+= new EventHandler(context_EndRequest);
        }
    
    
        void context_EndRequest(object sender, EventArgs e)
        {
            HttpApplication app = (HttpApplication)sender;
            if (app.Response.StatusCode == 302 
                && app.Request.Headers["X-Requested-With"] == "XMLHttpRequest"
                && context.Response.RedirectLocation.ToUpper().Contains("LOGIN.ASPX"))
            {
                app.Response.ClearHeaders();
                app.Response.ClearContent();
                app.Response.StatusCode = 401;
            }
        }
    
        #endregion
    }
    

    如果您的应用中有其他合法的 302 重定向,您还可以确保重定向到您的实际登录页面。

    然后你只需添加到你的 web.config 中:

      <httpModules>
        <add name="AuthRedirectHandler" type="SomeNameSpace.AuthRedirectHandler, SomeNameSpace" />
      </httpModules>
    

    无论如何。再次,实际的原始想法进入了这个答案,我只是从 SO 和网络的其他部分中提取各种位。

    【讨论】:

    • 好吧,正如我所说,我试图避免重写 http 模块,主要是因为我不想维护另一段代码。此外,该解决方案有点棘手,我想将 AJAX 调用重定向到另一个端点吗?我无法阻止所有 http 302,然后我会遇到与现在使用 http 401 类似的情况:P 谢谢。
    • 1.您不是在“重写 HTTP 模块”,而是在添加一个非常简短的(不处理任何实际身份验证)来处理未经授权的 AJAX 请求和 2。正如我所说,该示例不包括每个过滤器可以想象,(在这种情况下甚至是可取的),但是登录页面的 302 重定向要么是 a)登录页面,要么是 b)在 web.config 中设置的值。这些值中的任何一个都应该易于计算并包含在条件中。我认为您不会找到涉及不到十几行代码的解决方案。
    • 无论如何,这个话题已经针对 ASP.NET 表单进行了详细的讨论:*.com/questions/199099/… 如果您碰巧使用 MVC 并且关心实际情况,还有大量更聪明的 MVC 服务器端解决方案寻找他们。
    • 我已经看过所有这些解决方案,其中一些是旧的(2008 年的那个),你为什么认为我特别说我不想写一个模块来处理这个问题?也许在最新的 asp.net 版本中出现了一些新的东西。在上一段之前有三个问题,这些问题就是这篇文章的内容。如果可能的话.. 好,如果不是快乐的日子也是如此。我知道这里有 1000 种解决方法,就像大多数编程问题一样。
    • 好吧,我回答了你的问题:“如果请求是 AJAX 请求,有没有办法告诉 FormsAuthenticationModule 不执行重定向?”很抱歉,您不喜欢使用简单的 HTTP 模块来做到这一点。此外,SO 上没有人会犹豫在任何旧问题出现时为其添加新解决方案。相信我,如果有灵丹妙药的话,它会被附加到某个地方的一个老问题上。