Объединение аутентификации форм и базовой проверки подлинности
У меня есть некоторый основной код ASP, который я хочу раскрывать как защищенными веб-страницами (используя аутентификацию по формам), так и через веб-службы (используя базовую аутентификацию).
Решение, с которым я столкнулся, похоже, работает, но я ничего не теряю здесь?
Во-первых, весь сайт работает под HTTPS.
Сайт настроен на использование проверки подлинности форм в web.config
<authentication mode="Forms">
<forms loginUrl="~/Login.aspx" timeout="2880"/>
</authentication>
<authorization>
<deny users="?"/>
</authorization>
Затем я переопределяю AuthenticateRequest в Global.asax, чтобы запустить базовую проверку подлинности на страницах веб-службы:
void Application_AuthenticateRequest(object sender, EventArgs e)
{
//check if requesting the web service - this is the only page
//that should accept Basic Authentication
HttpApplication app = (HttpApplication)sender;
if (app.Context.Request.Path.StartsWith("/Service/MyService.asmx"))
{
if (HttpContext.Current.User != null)
{
Logger.Debug("Web service requested by user " + HttpContext.Current.User.Identity.Name);
}
else
{
Logger.Debug("Null user - use basic auth");
HttpContext ctx = HttpContext.Current;
bool authenticated = false;
// look for authorization header
string authHeader = ctx.Request.Headers["Authorization"];
if (authHeader != null && authHeader.StartsWith("Basic"))
{
// extract credentials from header
string[] credentials = extractCredentials(authHeader);
// because i'm still using the Forms provider, this should
// validate in the same way as a forms login
if (Membership.ValidateUser(credentials[0], credentials[1]))
{
// create principal - could also get roles for user
GenericIdentity id = new GenericIdentity(credentials[0], "CustomBasic");
GenericPrincipal p = new GenericPrincipal(id, null);
ctx.User = p;
authenticated = true;
}
}
// emit the authenticate header to trigger client authentication
if (authenticated == false)
{
ctx.Response.StatusCode = 401;
ctx.Response.AddHeader(
"WWW-Authenticate",
"Basic realm=\"localhost\"");
ctx.Response.Flush();
ctx.Response.Close();
return;
}
}
}
}
private string[] extractCredentials(string authHeader)
{
// strip out the "basic"
string encodedUserPass = authHeader.Substring(6).Trim();
// that the right encoding
Encoding encoding = Encoding.GetEncoding("iso-8859-1");
string userPass = encoding.GetString(Convert.FromBase64String(encodedUserPass));
int separator = userPass.IndexOf(':');
string[] credentials = new string[2];
credentials[0] = userPass.Substring(0, separator);
credentials[1] = userPass.Substring(separator + 1);
return credentials;
}
Ответы
Ответ 1
.Net 4.5 имеет новое свойство Response: SuppressFormsAuthenticationRedirect. Если установлено значение true, это предотвращает перенаправление ответа 401 на страницу входа на веб-сайт. Вы можете использовать следующий фрагмент кода в файле global.asax.cs, чтобы включить базовую аутентификацию, например. папке /HealthCheck.
/// <summary>
/// Authenticates the application request.
/// Basic authentication is used for requests that start with "/HealthCheck".
/// IIS Authentication settings for the HealthCheck folder:
/// - Windows Authentication: disabled.
/// - Basic Authentication: enabled.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">A <see cref="System.EventArgs"/> that contains the event data.</param>
protected void Application_AuthenticateRequest(object sender, EventArgs e)
{
var application = (HttpApplication)sender;
if (application.Context.Request.Path.StartsWith("/HealthCheck", StringComparison.OrdinalIgnoreCase))
{
if (HttpContext.Current.User == null)
{
var context = HttpContext.Current;
context.Response.SuppressFormsAuthenticationRedirect = true;
}
}
}
Ответ 2
У меня есть решение для работы на основе идей OP и указателей от Samuel Meacham.
В global.asax.cs:
protected void Application_AuthenticateRequest(object sender, EventArgs e)
{
if (DoesUrlNeedBasicAuth() && Request.IsSecureConnection) //force https before we try and use basic authentication
{
if (HttpContext.Current.User != null && HttpContext.Current.User.Identity.IsAuthenticated)
{
_log.Debug("Web service requested by user " + HttpContext.Current.User.Identity.Name);
}
else
{
_log.Debug("Null user - use basic auth");
HttpContext ctx = HttpContext.Current;
bool authenticated = false;
// look for authorization header
string authHeader = ctx.Request.Headers["Authorization"];
if (authHeader != null && authHeader.StartsWith("Basic"))
{
// extract credentials from header
string[] credentials = extractCredentials(authHeader);
//Lookup credentials (we'll do this in config for now)
//check local config first
var localAuthSection = ConfigurationManager.GetSection("apiUsers") as ApiUsersSection;
authenticated = CheckAuthSectionForCredentials(credentials[0], credentials[1], localAuthSection);
if (!authenticated)
{
//check sub config
var webAuth = System.Web.Configuration.WebConfigurationManager.GetSection("apiUsers") as ApiUsersSection;
authenticated = CheckAuthSectionForCredentials(credentials[0], credentials[1], webAuth);
}
}
// emit the authenticate header to trigger client authentication
if (authenticated == false)
{
ctx.Response.StatusCode = 401;
ctx.Response.AddHeader("WWW-Authenticate","Basic realm=\"localhost\"");
ctx.Response.Flush();
ctx.Response.Close();
return;
}
}
}
else
{
//do nothing
}
}
/// <summary>
/// Detect if current request requires basic authentication instead of Forms Authentication.
/// This is determined in the web.config files for folders or pages where forms authentication is denied.
/// </summary>
public bool DoesUrlNeedBasicAuth()
{
HttpContext context = HttpContext.Current;
string path = context.Request.AppRelativeCurrentExecutionFilePath;
if (context.SkipAuthorization) return false;
//if path is marked for basic auth, force it
if (context.Request.Path.StartsWith(Request.ApplicationPath + "/integration", true, CultureInfo.CurrentCulture)) return true; //force basic
//if no principal access was granted force basic auth
//if (!UrlAuthorizationModule.CheckUrlAccessForPrincipal(path, context.User, context.Request.RequestType)) return true;
return false;
}
private string[] extractCredentials(string authHeader)
{
// strip out the "basic"
string encodedUserPass = authHeader.Substring(6).Trim();
// that the right encoding
Encoding encoding = Encoding.GetEncoding("iso-8859-1");
string userPass = encoding.GetString(Convert.FromBase64String(encodedUserPass));
int separator = userPass.IndexOf(':');
string[] credentials = new string[2];
credentials[0] = userPass.Substring(0, separator);
credentials[1] = userPass.Substring(separator + 1);
return credentials;
}
/// <summary>
/// Checks whether the given basic authentication details can be granted access. Assigns a GenericPrincipal to the context if true.
/// </summary>
private bool CheckAuthSectionForCredentials(string username, string password, ApiUsersSection section)
{
if (section == null) return false;
foreach (ApiUserElement user in section.Users)
{
if (user.UserName == username && user.Password == password)
{
Context.User = new GenericPrincipal(new GenericIdentity(user.Name, "Basic"), user.Roles.Split(','));
return true;
}
}
return false;
}
Учетные данные, которым разрешен доступ, хранятся в пользовательском разделе в файле web.config, но вы можете сохранить их как пожелаете.
HTTPS требуется в приведенном выше коде, но это ограничение можно удалить, если хотите.
EDIT. Но, как правильно указано в комментариях, это, вероятно, не очень хорошая идея из-за того, что имя пользователя и пароль кодируются и отображаются в виде обычного текста. Конечно, даже с ограничением HTTPS здесь вы не можете запретить внешнему запросу пытаться использовать ненадежный HTTP и делиться своими учетными данными с любым наблюдающим за трафиком.
Путь к принудительной базовой аутентификации сейчас жестко запрограммирован, но, очевидно, может быть помещен в конфигурацию или какой-либо другой источник. В моем случае папка "интеграция" была настроена для анонимных пользователей.
Здесь прокомментирована строка, включающая CheckUrlAccessForPrincipal
, которая предоставит доступ к любой странице на сайте, используя базовый auth, если пользователь не войдет в систему через проверку подлинности с помощью форм.
Использование Application_AuthenticateRequest
вместо Application_AuthorizeRequest
оказалось важным, поскольку Application_AuthorizeRequest
заставил бы базовый auth, но в любом случае перенаправить на страницу входа в Autodesk Forms. Мне не удалось выполнить эту работу, играя с разрешениями на основе местоположения в web.config и так и не узнав причину этого. Перестановка на Application_AuthenticateRequest
сделала трюк, поэтому я оставил его на этом.
Результат этого оставил мне папку, к которой можно было получить доступ, используя базовый auth через HTTPS внутри приложения, которое обычно использует проверку подлинности на языке. Записанные пользователи могут в любом случае получить доступ к папке.
Надеюсь, что это поможет.
Ответ 3
Ты на правильном пути, я думаю. Однако я не уверен, что вы должны выполнять работу в проверке подлинности. Это когда пользователь идентифицируется, а не когда проверяется разрешение на ресурс (что позже в авторизации). Во-первых, в вашем web.config используйте <location>
для удаления форм auth для ресурсов, где вы хотите использовать базовый auth.
Web.config
<configuration>
<!-- don't require forms auth for /public -->
<location path="public">
<authorization>
<allow users="*" />
</authorization>
</location>
</configuration>
Global.asax.cs или где угодно (IHttpModule и т.д.)
Затем вместо жесткого кодирования конкретных обработчиков или попытки разобрать URL-адрес, чтобы увидеть, находится ли вы в определенной папке, в Application_AuthorizeRequest
, что-то вроде следующего, сделает все по умолчанию (формы auth 1st, basic auth если формы auth были удалены с помощью параметров <location>
в web.config).
/// <summary>
/// Checks to see if the current request can skip authorization, either because context.SkipAuthorization is true,
/// or because UrlAuthorizationModule.CheckUrlAccessForPrincipal() returns true for the current request/user/url.
/// </summary>
/// <returns></returns>
public bool DoesUrlRequireAuth()
{
HttpContext context = HttpContext.Current;
string path = context.Request.AppRelativeCurrentExecutionFilePath;
return context.SkipAuthorization ||
UrlAuthorizationModule.CheckUrlAccessForPrincipal(
path, context.User, context.Request.RequestType);
}
void Application_AuthorizeRequest(object sender, EventArgs e)
{
if (DoesUrlRequireAuth())
{
// request protected by forms auth
}
else
{
// do your http basic auth code here
}
}
Неподтвержденный (просто набрал inline здесь), но я много сделал с пользовательскими поставщиками членства, ваши требования полностью выполнимы.
Надеюсь, что это полезно =)