表单身份验证:禁用重定向到登录页面

75
我有一个使用ASP.NET Forms身份验证的应用程序。大部分情况下都运行良好,但我正在尝试通过.ashx文件添加对简单API的支持。我希望ashx文件具有可选的身份验证(即,如果您没有提供身份验证标头,它将匿名工作)。但是,根据您的操作,我想在某些条件下要求进行身份验证。
我认为只需在未提供所需身份验证时用状态代码401进行响应就可以了。但是,似乎Forms身份验证模块正在拦截它,并代替响应重定向到登录页面。我的意思是,如果我的ProcessRequest方法看起来像这样:
public void ProcessRequest(HttpContext context)
{
    Response.StatusCode = 401;
    Response.StatusDescription = "Authentication required";
}

然后我没有像预期的那样在客户端收到401错误代码,而是 实际上 收到了302重定向到登录页面。

对于普通的HTTP流量,我可以理解这样做是有用的,但对于我的API页面,我希望401未经修改地通过,以便客户端调用程序可以对其进行编程响应。

有什么办法可以做到这一点吗?

13个回答

82

ASP.NET 4.5新增了Boolean HttpResponse.SuppressFormsAuthenticationRedirect属性。

public void ProcessRequest(HttpContext context)
{
    Response.StatusCode = 401;
    Response.StatusDescription = "Authentication required";
    Response.SuppressFormsAuthenticationRedirect = true;
}

这是针对使用.NET 4.5的任何人的答案。太糟糕了,我没有在列表中读到这一点,我不得不在反编译的源代码中自己找到它! - Kevin Coulombe
3
您可以将此代码放入 Global.asax 的 Application_EndRequest 事件中,以在整个应用程序中应用此设置。示例 - David Sherret
6
我在 Application_BeginRequest 方法中使用了类似的代码片段,并且获得了很好的效果。这个请求是否是 AJAX?如果是,立即设置标志。 - Tyler Forsythe
1
谢谢,我现在将其标记为已接受的答案,因为这绝对是现在应该采取的方法。 - Dean Harding
发现这在从 FormsAuth 迁移到 ASP.NET Identity 时非常有用,因为我需要同时使用两者。 - Simon_Weaver

37
经过一番调查,看起来 FormsAuthenticationModule 添加了一个处理程序来处理 HttpApplicationContext.EndRequest 事件。在它的处理程序中,它检查了401状态码,并基本上执行了一个 Response.Redirect(loginUrl)。据我所知,如果想使用 FormsAuthenticationModule,没有办法覆盖此行为。
我最终解决它的方式是在 web.config 中禁用 FormsAuthenticationModule,如下所示:
<authentication mode="None" />

然后我自己实现了Application_AuthenticateEvent

void Application_AuthenticateRequest(object sender, EventArgs e)
{
    if (Context.User == null)
    {
        var oldTicket = ExtractTicketFromCookie(Context, FormsAuthentication.FormsCookieName);
        if (oldTicket != null && !oldTicket.Expired)
        {
            var ticket = oldTicket;
            if (FormsAuthentication.SlidingExpiration)
            {
                ticket = FormsAuthentication.RenewTicketIfOld(oldTicket);
                if (ticket == null)
                    return;
            }

            Context.User = new GenericPrincipal(new FormsIdentity(ticket), new string[0]);
            if (ticket != oldTicket)
            {
                // update the cookie since we've refreshed the ticket
                string cookieValue = FormsAuthentication.Encrypt(ticket);
                var cookie = Context.Request.Cookies[FormsAuthentication.FormsCookieName] ??
                             new HttpCookie(FormsAuthentication.FormsCookieName, cookieValue) { Path = ticket.CookiePath };

                if (ticket.IsPersistent)
                    cookie.Expires = ticket.Expiration;
                cookie.Value = cookieValue;
                cookie.Secure = FormsAuthentication.RequireSSL;
                cookie.HttpOnly = true;
                if (FormsAuthentication.CookieDomain != null)
                    cookie.Domain = FormsAuthentication.CookieDomain;
                Context.Response.Cookies.Remove(cookie.Name);
                Context.Response.Cookies.Add(cookie);
            }
        }
    }
}

private static FormsAuthenticationTicket ExtractTicketFromCookie(HttpContext context, string name)
{
    FormsAuthenticationTicket ticket = null;
    string encryptedTicket = null;

    var cookie = context.Request.Cookies[name];
    if (cookie != null)
    {
        encryptedTicket = cookie.Value;
    }

    if (!string.IsNullOrEmpty(encryptedTicket))
    {
        try
        {
            ticket = FormsAuthentication.Decrypt(encryptedTicket);
        }
        catch
        {
            context.Request.Cookies.Remove(name);
        }

        if (ticket != null && !ticket.Expired)
        {
            return ticket;
        }

        // if the ticket is expired then remove it
        context.Request.Cookies.Remove(name);
        return null;
    }
}

实际上比这更复杂,但我主要通过查看反编译器中FormsAuthenticationModule的实现来获得代码。我的实现与内置的FormsAuthenticationModule不同,因为如果您使用401做出响应,它不会执行任何操作 - 根本不会重定向到登录页面。我猜想,如果以后有这样的需求,我可以在上下文中添加一个项目来禁用自动重定向或类似的东西。


2
注意:上面由zacharydl提供的答案是在ASP.NET 4.5+中应该采用的方法。 - Dean Harding

11

我不确定这对每个人都适用,但在IIS7中,您可以在设置状态代码和描述后调用Response.End()。这样,那个该死的FormsAuthenticationModule就不会重定向了。

public void ProcessRequest(HttpContext context) {
    Response.StatusCode = 401;
    Response.StatusDescription = "Authentication required";
    Response.End();
}

3
约翰,干得好!难道我们不觉得绕过这个的难度太大了吗?HttpContext有SkipAuthorization属性,如果它也有SkipLoginRedirect就太好了。或者如果表单身份验证可以被目录范围和覆盖掉的话也不错。 - user87453
当我尝试这个功能时,我使用的是IIS 6,我认为在IIS 7和IIS 6上它的工作方式有些不同(例如,在IIS 6中,Response.End()只会抛出一个ThreadAbortException,而表单身份验证模块会捕获该异常 - 这意味着它仍然会为我执行逻辑重定向)。 - Dean Harding
2
这个可以工作,但是控制权仍然会转到FormsAuthenticationModule,它的EndRequest事件处理程序将尝试执行重定向,由于响应已经被End(),这会触发一个异常,该异常被记录在系统事件日志中,同时也会调用Application_Error() - sharptooth

8
为了扩展zacharydl的回答,我稍微修改了一下,使用这个方法解决了我的问题。在每个请求的开头,如果它是AJAX,则立即抑制行为。
protected void Application_BeginRequest()
{
    HttpRequestBase request = new HttpRequestWrapper(Context.Request);
    if (request.IsAjaxRequest())
    {
        Context.Response.SuppressFormsAuthenticationRedirect = true;
    }
}

5

我不知道你是如何使用Response.End()的。我尝试过,但是没有成功,然后查看了MSDN中有关Response.End()的说明:“停止页面的执行,并引发EndRequest事件”。

至于我使用的技巧是:

_response.StatusCode = 401;
_context.Items["401Override"] = true;
_response.End();

接下来,在Global.cs中添加一个EndRequest处理程序(在Authentication HTTPModule之后调用):

protected void Application_EndRequest(object sender, EventArgs e)
{
    if (HttpContext.Current.Items["401Override"] != null)
    {
        HttpContext.Current.Response.Clear();
        HttpContext.Current.Response.StatusCode = 401;
    }
}

在页面中,我使用了 StatusCode=418 并将覆盖文本添加到 StatusDescription 中。然后,在 EndRequest 事件中捕获它们,就像您重置响应一样。如果我在页面中使用状态码401,它仍然会被重定向。 - Steve Lautenschlager

4
我知道已经有一个带勾的答案,但是在尝试解决类似问题时,我发现可以使用这个http://blog.inedo.com/2010/10/12/http-418-im-a-teapot-finally-a-%e2%80%9clegitimate%e2%80%9d-use/)作为替代方法。

基本上你需要在你的代码中返回你自己的HTTP状态码(例如:418)。在我的情况下,是WCF数据服务。

throw new DataServiceException(418, "401 Unauthorized");

然后使用HTTP模块在EndRequest事件处理它,以将代码重写回401。

HttpApplication app = (HttpApplication)sender;
if (app.Context.Response.StatusCode == 418)
{
    app.Context.Response.StatusCode = 401;
}

浏览器/客户端将会收到正确的内容和状态码,这对我来说非常有效 :)

如果您想了解更多关于HTTP状态码418的信息,请参阅此问题和答案


太好了!它的工作方式就像我想要的那样 :) 而不是添加专用的http模块,我使用了“global.asax”事件EndRequest。可以钩住这个事件并覆盖“Init”方法。 - Pavlo Neiman
感谢提到使用自己的状态码:由于我只想为通过 AJAX 调用测试身份验证的单个页面返回 401,因此我能够将该页面上的“失败”Response.StatusCode更改为 418,以避免所有其他解决方法!IIS7会抱怨,但没有人需要访问那个页面。 - Kodithic

4
您发现的关于表单身份验证拦截401并重定向的内容是正确的,但我们也可以反向进行拦截。
基本上,您需要一个HTTP模块来拦截302重定向到登录页面并将其反向为401。
执行此操作的步骤在 此处 中解释。
给出的链接是关于WCF服务的,但在所有表单身份验证方案中都是相同的。
正如上面的链接所解释的那样,您还需要清除HTTP标头,但请记住,如果原始响应(即在拦截之前)包含任何cookie,则将cookie标头放回响应中。

嗯,是的,这是一个有趣的想法!当然,我已经有了适合自己的解决方案,但我相信其他人可能会发现这是更好的解决方案。谢谢! - Dean Harding
看起来像是一种黑客行为,可能会产生类似的奇怪副作用! - Alan Christensen

2

1
如果你使用的是.NET Framework版本大于等于v4.0但小于v4.5,那么有一个有趣的技巧。它使用反射来设置不可访问的SuppressFormsAuthenticationRedirect属性的值。
// Set property to "true" using reflection
Response
  .GetType()
  .GetProperty("SuppressFormsAuthenticationRedirect")
  .SetValue(Response, true, null);

0
为了将用户重定向到未授权页面而不是登录页面,Hack 的方法是在 Global 中实现 Application_EndRequest,并检查响应状态码 302,该状态码是从当前调用的操作进行临时重定向。
protected void Application_EndRequest(object sender, EventArgs e)
    {
        if(HttpContext.Current.Response.StatusCode == 302 && User.Identity.IsAuthenticated)
        {
            HttpContext.Current.Response.Clear();
            HttpContext.Current.Response.Redirect("/UnauthorizedPageUrl");
        }
    }

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