Отключение аутентификации 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();
}
}
}
Части этого кода пришли из здесь. Удачи с вашим приложением!
Ответ 7
В .NET 4.5 теперь вы можете установить
Response.SuppressFormsAuthenticationRedirect = true
Проверьте эту страницу: https://msdn.microsoft.com/en-us/library/system.web.httpresponse.suppressformsauthenticationredirect.aspx