Отключение аутентификации ASP.Net WebForms для одного подкаталога

У меня есть большое корпоративное приложение, содержащее как WebForms, так и страницы MVC. Он имеет существующие настройки аутентификации и авторизации, которые я не хочу изменять.

Аутентификация WebForms настраивается в файле web.config:

 <authentication mode="Forms">
  <forms blah... blah... blah />
 </authentication>

 <authorization>
  <deny users="?" />
 </authorization>

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

Итак, когда пользователь пытается получить данные JSON из службы REST, он возвращает статус HTTP 401 и заголовок WWW-Authenticate. Если они отвечают правильно сформированным ответом HTTP Authorization, он позволяет им.

Проблема в том, что WebForms переопределяет это на низком уровне - если вы вернетесь 401 (неавторизованный), он переопределяет это с 302 (перенаправление на страницу входа). Это хорошо в браузере, но бесполезно для службы REST.

Я хочу отключить параметр проверки подлинности в файле web.config, переопределив папку "rest":

 <location path="rest">
  <system.web>
   <authentication mode="None" />
   <authorization><allow users="?" /></authorization>
  </system.web>
 </location>

Бит авторизации работает нормально, но строка аутентификации (<authentication mode="None" />) вызывает исключение:

Ошибка использования раздела, зарегистрированного как allowDefinition = 'MachineToApplication', превышающего уровень приложения.

Я настраиваю это на уровне приложения, хотя это - в корневой web.config - и эта ошибка для web.configs в подкаталогах.

Как переопределить аутентификацию, чтобы весь остальной сайт использовал аутентификацию WebForms, и этот один каталог не использует?

Это похоже на другой вопрос: 401 код ответа для json-запросов с ASP.NET MVC, но я не ищу того же решения - t хотите просто удалить аутентификацию WebForms и добавить новый настраиваемый код по всему миру, что может привести к большому риску и работе. Я хочу изменить только один каталог в конфигурации.

Обновление

Я хочу настроить одно веб-приложение и хочу, чтобы все страницы WebForms и MVC отображали аутентификацию WebForms. Я хочу, чтобы один каталог использовал базовую HTTP-аутентификацию.

Обратите внимание, что я говорю об аутентификации, а не о авторизации. Я хочу, чтобы звонки REST приходили с именем пользователя и паролем в HTTP-заголовке, и я хочу, чтобы страницы WebForm и MVC поставлялись с файлом cookie аутентификации .Net - в любом случае авторизация выполняется в отношении нашей БД.

Я не хочу переписывать аутентификацию WebForms и откатывать свои собственные файлы cookie. Кажется, это смешно, что это единственный способ добавить HTTP-службу REST в приложение.

Я не могу добавить дополнительное приложение или виртуальный каталог - это должно быть как одно приложение.

Ответы

Ответ 1

Я работал над этим беспорядочным способом - путем подмены аутентификации форм в global.asax для всех существующих страниц.

Я все еще не совсем полностью работаю, но это выглядит примерно так:

protected void Application_BeginRequest(object sender, EventArgs e)
{
    // lots of existing web.config controls for which webforms folders can be accessed
    // read the config and skip checks for pages that authorise anon users by having
    // <allow users="?" /> as the top rule.

    // check local config
    var localAuthSection = ConfigurationManager.GetSection("system.web/authorization") as AuthorizationSection;

    // this assumes that the first rule will be <allow users="?" />
    var localRule = localAuthSection.Rules[0];
    if (localRule.Action == AuthorizationRuleAction.Allow &&
        localRule.Users.Contains("?"))
    {
        // then skip the rest
        return;
    }

    // get the web.config and check locations
    var conf = WebConfigurationManager.OpenWebConfiguration("~");
    foreach (ConfigurationLocation loc in conf.Locations)
    {
        // find whether we're in a location with overridden config
        if (this.Request.Path.StartsWith(loc.Path, StringComparison.OrdinalIgnoreCase) ||
            this.Request.Path.TrimStart('/').StartsWith(loc.Path, StringComparison.OrdinalIgnoreCase))
        {
            // get the location config
            var locConf = loc.OpenConfiguration();
            var authSection = locConf.GetSection("system.web/authorization") as AuthorizationSection;
            if (authSection != null)
            {
                // this assumes that the first rule will be <allow users="?" />
                var rule = authSection.Rules[0];
                if (rule.Action == AuthorizationRuleAction.Allow &&
                    rule.Users.Contains("?"))
                {
                    // then skip the rest
                    return;
                }
            }
        }
    }

    var cookie = this.Request.Cookies[FormsAuthentication.FormsCookieName];
    if (cookie == null ||
        string.IsNullOrEmpty(cookie.Value))
    {
        // no or blank cookie
        FormsAuthentication.RedirectToLoginPage();
    }

    // decrypt the 
    var ticket = FormsAuthentication.Decrypt(cookie.Value);
    if (ticket == null ||
        ticket.Expired)
    {
        // invalid cookie
        FormsAuthentication.RedirectToLoginPage();
    }

    // renew ticket if needed
    var newTicket = ticket;
    if (FormsAuthentication.SlidingExpiration)
    {
        newTicket = FormsAuthentication.RenewTicketIfOld(ticket);
    }

    // set the user so that .IsAuthenticated becomes true
    // then the existing checks for user should work
    HttpContext.Current.User = new GenericPrincipal(new FormsIdentity(newTicket), newTicket.UserData.Split(','));

}

Я не очень доволен этим как исправление - похоже, это ужасный взлом и повторное изобретение колеса, но похоже, что это единственный способ для моих страниц, прошедших проверку подлинности на основе форм, и службы REST с проверкой подлинности HTTP для работы в одном приложении.

Ответ 2

Если "rest" - это просто папка в вашем корне, вы почти там: удалить строку аутентификации i.e.

<location path="rest">
  <system.web>
      <authorization>
        <allow users="*" />
      </authorization>
  </system.web>
 </location>

В качестве альтернативы вы можете добавить web.config в свою папку для отдыха и просто иметь это:

<system.web>
     <authorization>
          <allow users="*" />
     </authorization>
</system.web>

Отметьте этот один.

Ответ 3

Я столкнулся с одной и той же проблемой, следующая статья указала мне в правильном направлении: http://msdn.microsoft.com/en-us/library/aa479391.aspx

MADAM выполняет именно то, что вам нужно, в частности, вы можете настроить модуль FormsAuthenticationDispositionModule, чтобы отключить "обман" аутентификации форм, и не допустить изменения кода ответа с 401 по 302. Это должно привести к тому, что ваш клиент для отдыха получит право auth вызов.

Страница загрузки MADAM: http://www.raboof.com/projects/madam/

В моем случае вызовы REST выполняются для контроллеров (это приложение на основе MVC) в "API", площадь. Дискриминатор MADAM устанавливается со следующей конфигурацией:

<formsAuthenticationDisposition>
  <discriminators all="1">
    <discriminator type="Madam.Discriminator">
      <discriminator
          inputExpression="Request.Url"
          pattern="api\.*" type="Madam.RegexDiscriminator" />
    </discriminator>
  </discriminators>
</formsAuthenticationDisposition>

Затем все, что вам нужно сделать, это добавить модуль MADAM к вашему web.config

<modules runAllManagedModulesForAllRequests="true">
  <remove name="WebDAVModule" /> <!-- allow PUT and DELETE methods -->
  <add name="FormsAuthenticationDisposition" type="Madam.FormsAuthenticationDispositionModule, Madam" />
</modules>

Не забудьте добавить допустимые разделы в web.config(SO не позволял мне вставлять код), вы можете получить пример из веб-проекта при загрузке.

            

С помощью этой настройки любые запросы, поступающие к URL-адресам, начинающимся с "API/", получат ответ 401 вместо 301, созданного с помощью проверки подлинности форм.

Ответ 4

Мне удалось заставить это работать над предыдущим проектом, но для выполнения пользовательской базовой проверки подлинности потребовалось использование HTTP-модуля, поскольку проверка учетной записи связана с базой данных, а не с Windows.

Я установил тест, как вы указали с одним веб-приложением в корневом веб-сайте теста, и папкой, содержащей службу REST. Конфигурация корневого приложения была настроена на отказ в доступе:

<authentication mode="Forms">
  <forms loginUrl="Login.aspx" timeout="2880" />
</authentication>
<authorization>
  <deny users="?"/>
</authorization>

Затем мне пришлось создать приложение для папки REST в IIS и поместить файл web.config в папку REST. В этой конфигурации я указал следующее:

<authentication mode="None"/>
<authorization>
  <deny users="?"/>
</authorization>

Мне также пришлось подключить http-модуль в соответствующих местах в конфигурации каталога REST. Этот модуль должен войти в каталог bin в каталоге REST. Я использовал собственный базовый модуль аутентификации Dominick Baier, и этот код находится здесь. Эта версия более специфична для IIS 6, однако для IIS 7 также существует версия для codeplex, но я не тестировал ее ( предупреждение: версия IIS6 не имеет того же имени сборки и пространства имен, что и версия IIS7.) Мне очень нравится этот базовый модуль auth, поскольку он подключается прямо к модели членства ASP.NET.

Последний шаг состоял в том, чтобы разрешить только анонимный доступ как для корневого приложения, так и для приложения REST в IIS.

Для полноты я включил полные конфигурации ниже. Приложение-тест было просто приложением веб-формы ASP.NET, сгенерированным из VS 2010, оно использовало AspNetSqlProfileProvider для поставщика членства; здесь config:

<?xml version="1.0"?>

<configuration>
  <connectionStrings>
    <add name="ApplicationServices"
      connectionString="data source=.\SQLEXPRESS;Integrated Security=SSPI;Database=sqlmembership;"
    providerName="System.Data.SqlClient" />
  </connectionStrings>

  <system.web>
    <compilation debug="true" targetFramework="4.0" />

    <authentication mode="Forms">
      <forms loginUrl="~/Account/Login.aspx" timeout="2880" />
    </authentication>

    <authorization>
      <deny users="?"/>
    </authorization>

    <membership>
      <providers>
        <clear/>
        <add name="AspNetSqlMembershipProvider" type="System.Web.Security.SqlMembershipProvider" connectionStringName="ApplicationServices"
          enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" requiresUniqueEmail="false"
          maxInvalidPasswordAttempts="5" minRequiredPasswordLength="6" minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10"
        applicationName="/" />
      </providers>
    </membership>

    <profile>
      <providers>
        <clear/>
        <add name="AspNetSqlProfileProvider" type="System.Web.Profile.SqlProfileProvider" connectionStringName="ApplicationServices" applicationName="/"/>
      </providers>
    </profile>

    <roleManager enabled="false">
      <providers>
        <clear/>
        <add name="AspNetSqlRoleProvider" type="System.Web.Security.SqlRoleProvider" connectionStringName="ApplicationServices" applicationName="/" />
        <add name="AspNetWindowsTokenRoleProvider" type="System.Web.Security.WindowsTokenRoleProvider" applicationName="/" />
      </providers>
    </roleManager>

  </system.web>

  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true"/>
  </system.webServer>
</configuration>

В каталоге REST содержался пустой проект ASP.NET, сгенерированный из VS 2010, и я вложил в него один ASPX файл, однако содержимое папки REST не должно было быть новым проектом. Просто отбрасывание конфигурационного файла после того, как в каталоге было связанное с ним приложение, должно работать. Конфигурация для этого проекта:

<?xml version="1.0"?>
<configuration>
  <configSections>
    <section name="customBasicAuthentication" type="Thinktecture.CustomBasicAuthentication.CustomBasicAuthenticationSection, Thinktecture.CustomBasicAuthenticationModule"/>
  </configSections>
  <customBasicAuthentication
    enabled="true"
    realm="testdomain"
    providerName="AspNetSqlMembershipProvider"
    cachingEnabled="true"
    cachingDuration="15"
  requireSSL="false" />

  <system.web>
    <authentication mode="None"/>
    <authorization>
      <deny users="?"/>
    </authorization>

    <compilation debug="true" targetFramework="4.0" />
    <httpModules>
      <add name="CustomBasicAuthentication" type="Thinktecture.CustomBasicAuthentication.CustomBasicAuthenticationModule, Thinktecture.CustomBasicAuthenticationModule"/>
    </httpModules>
  </system.web>
</configuration>

Я надеюсь, что это удовлетворит ваши потребности.

Ответ 5

Это не самые элегантные решения, но я думаю, что это хорошее начало.

1) Создайте HttpModule.

2) обрабатывать событие AuthenticateRequest.

3) в обработчике событий проверьте, что запрос относится к каталогу, к которому вы хотите разрешить доступ.

4) Если в этом случае вручную установлен файл cookie auth: (или посмотрите, можете ли вы найти другой способ, когда у вас есть контроль и аутентификация еще не произошло)

FormsAuthentication.SetAuthCookie("Anonymous", false);

5) О, почти забыл, вы бы хотели убедиться, что файл cookie auth был очищен, если запрос не был в каталоге, к которому вы хотели бы предоставить доступ.

Ответ 6

После просмотра ваших комментариев к моему предыдущему ответу, я задался вопросом, может ли ваше веб-приложение автоматизировать развертывание приложения в вашем каталоге REST. Это позволит вам воспользоваться преимуществами второго приложения, а также снизить нагрузку на развертывание для ваших системных администраторов.

Моя мысль заключалась в том, что вы могли бы поместить подпрограмму в метод Application_Start для global.asax, который будет проверять наличие каталога REST и что у него еще нет приложения, связанного с ним. Если тест возвращает true, тогда возникает процесс связывания нового приложения с каталогом REST.

Еще одна мысль о том, что вы можете использовать WIX (или другую технологию развертывания) для создания пакета установки, который могли бы использовать ваши администраторы запустить для создания приложения, однако я не думаю, что так же автоматически, как приложение настроить его зависимость.

Ниже я включил примерную реализацию, которая проверяет IIS для заданного каталога и применяет к нему приложение, если оно еще не имеет этого. Код был протестирован с IIS 7, но должен работать и с IIS 6.

//This is part of global.asax.cs
//This approach may require additional user privileges to query IIS

//using System.DirectoryServices;
//using System.Runtime.InteropServices;

protected void Application_Start(object sender, EventArgs evt)
{
  const string iisRootUri = "IIS://localhost/W3SVC/1/Root";
  const string restPhysicalPath = @"C:\inetpub\wwwroot\Rest";
  const string restVirtualPath = "Rest";

  if (!Directory.Exists(restPhysicalPath))
  {
    // there is no rest path, so do nothing
    return;
  }

  using (var root = new DirectoryEntry(iisRootUri))
  {
    DirectoryEntries children = root.Children;

    try
    {
      using (DirectoryEntry rest = children.Find(restVirtualPath, root.SchemaClassName))
      {
        // the above call throws an exception if the vdir does not exist
        return;
      }
    }
    catch (COMException e)
    {
      // something got unlinked incorrectly, kill the vdir and application
      foreach (DirectoryEntry entry in children)
      {
        if (string.Compare(entry.Name, restVirtualPath, true) == 0)
        {
          entry.DeleteTree();
        }     
      }
    }
    catch (DirectoryNotFoundException e)
    {
      // the vdir and application do not exist, add them below
    }

    using (DirectoryEntry rest = children.Add(restVirtualPath, root.SchemaClassName))
    {
      rest.CommitChanges();
      rest.Properties["Path"].Value = restPhysicalPath;
      rest.Properties["AccessRead"].Add(true);
      rest.Properties["AccessScript"].Add(true);
      rest.Invoke("AppCreate2", true);
      rest.Properties["AppFriendlyName"].Add(restVirtualPath);
      rest.CommitChanges();
    }
  }
}

Части этого кода пришли из здесь. Удачи с вашим приложением!