Аутентификация форм: отключить перенаправление на страницу входа в систему

У меня есть приложение, использующее ASP.NET Forms Authentication. По большей части, он отлично работает, но я пытаюсь добавить поддержку простого API через файл .ashx. Я хочу, чтобы файл ashx имел дополнительную аутентификацию (т.е. Если вы не предоставляете заголовок аутентификации, то он просто работает анонимно). Но, в зависимости от того, что вы делаете, я хочу требовать аутентификации при определенных условиях.

Я думал, что было бы просто ответить на код состояния 401, если требуемая аутентификация не была предоставлена, но похоже, что модуль Autodesk Forms перехватывает это и отвечает вместо этого перенаправлением на страницу входа. Я имею в виду, если мой метод ProcessRequest выглядит следующим образом:

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

Затем вместо получения кода ошибки 401 на клиенте, как и я ожидаю, я получаю перенаправление 302 на страницу входа.

Для nornal HTTP-трафика я вижу, как это было бы полезно, но для моей страницы API я хочу, чтобы 401 проходил без изменений, чтобы клиентский клиент мог ответить на него программным способом.

Есть ли способ сделать это?

Ответ 1

ASP.NET 4.5 добавил свойство Boolean HttpResponse.SuppressFormsAuthenticationRedirect.

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

Ответ 2

После небольшого исследования похоже, что FormsAuthenticationModule добавляет обработчик для события HttpApplicationContext.EndRequest. В этом обработчике он проверяет код состояния 401 и в основном выполняет Response.Redirect(loginUrl). Насколько я могу судить, нет способа отменить это поведение, если вы хотите использовать FormsAuthenticationModule.

Как я обошел это, отключив FormsAuthenticationModule в файле web.config так:

<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 - не переадресовываете на страницу входа вообще. Я думаю, если это когда-нибудь станет требованием, я могу поместить элемент в контекст, чтобы отключить авто-перенаправление или что-то еще.

Ответ 3

Я не уверен, что это будет работать для всех, но в IIS7 вы можете вызвать Response.End() после того, как вы установили код состояния и описание. Таким образом, # & $^ # @*! FormsAuthenticationModule не будет перенаправлять.

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

Ответ 4

Чтобы немного поработать над 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 (который вызывается после HTTP-модуля проверки подлинности):

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

Ответ 6

Я знаю, что уже есть ответ с тиком, но при попытке решить подобную проблему я нашел this (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;
}

Браузер/клиент получит правильный код контента и статуса, он отлично работает для меня:)

Если вам интересно узнать больше о статусе кода статуса 418, см. этот вопрос и ответ.

Ответ 7

то, что вы выяснили, верно в отношении форм auth, перехватывающих 401 и выполняющих перенаправление, но мы также можем сделать это, чтобы отменить это.

В основном вам нужен модуль http, чтобы перехватить перенаправление 302 на страницу входа и отменить его до 401.

Эта процедура объясняется в здесь

Данная ссылка относится к службе WCF, но она одинакова во всех сценариях сценариев формы.

Как объясняется в приведенной выше ссылке, вам также нужно очистить заголовки http, но не забудьте вернуть заголовок cookie обратно в ответ, если исходный ответ (т.е. перед перехватом) содержит файлы cookie.

Ответ 9

Вы не устанавливаете заголовок WWW-Authenticate в отображаемом вами коде, поэтому клиент не может выполнять проверку подлинности HTTP вместо проверки подлинности форм. Если это так, вы должны использовать 403 вместо 401, который не будет перехвачен FormsAuthenticaitonModule.

Ответ 10

Забавный хак, если вы используете .NET Framework> = v4.0, но & lt; v4.5. Он использует отражение, чтобы установить значение недоступного свойства SuppressFormsAuthenticationRedirect:

// Set property to "true" using reflection
Response
  .GetType()
  .GetProperty("SuppressFormsAuthenticationRedirect")
  .SetValue(Response, true, null);

Ответ 11

У меня была проблема, что я хотел избежать не только перенаправления, но и самой аутентификации форм, чтобы заставить работать веб-API. Записи в web.config с тегом местоположения для API не помогли. Таким образом, я использовал SuppressFormAuthenticationRedirect и HttpContext.Current.SkipAuthorization для подавления аутентификации в целом. Для идентификации отправителя я использовал, например, UserAgent в заголовке, но, конечно, рекомендуется реализовать дальнейшие шаги аутентификации, например, проверьте по отправляющему IP или отправьте другой ключ с запросом. Ниже вставлено в Global.asax.cs.

protected void Application_BeginRequest(object sender, EventArgs e)
    {
        if (HttpContext.Current.Request.UserAgent == "SECRET-AGENT")
        {
            AppLog.Log("Redirect suppressed");

            HttpApplication context = (HttpApplication)sender;

            context.Response.SuppressFormsAuthenticationRedirect = true;
            HttpContext.Current.SkipAuthorization = true;                
        }
    }

Ответ 12

Просмотрите файл Web.config в configuration\authentication. Если существует субэлемент forms с атрибутом loginUrl, удалите его и повторите попытку.