ste*_*ook 11 c# asp.net authentication
我有一些核心ASP代码,我希望通过安全网页(使用表单身份验证)和通过Web服务(使用基本身份验证)公开.
我提出的解决方案似乎有效,但我在这里遗漏了什么吗?
首先,整个站点在HTTPS下运行.
站点设置为在web.config中使用表单身份验证
<authentication mode="Forms">
<forms loginUrl="~/Login.aspx" timeout="2880"/>
</authentication>
<authorization>
<deny users="?"/>
</authorization>
Run Code Online (Sandbox Code Playgroud)
然后我覆盖Global.asax中的AuthenticateRequest,以在Web服务页面上触发基本身份验证:
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's 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;
}
Run Code Online (Sandbox Code Playgroud)
.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;
}
}
}
Run Code Online (Sandbox Code Playgroud)
我根据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's 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;
}
Run Code Online (Sandbox Code Playgroud)
允许访问的凭据存储在 web.config 的自定义部分中,但您可以按照自己的意愿存储这些凭据。
上面的代码中需要 HTTPS,但如果您愿意,可以删除此限制。 编辑但正如评论中正确指出的那样,这可能不是一个好主意,因为用户名和密码被编码并以纯文本形式可见。当然,即使此处存在 HTTPS 限制,您也无法阻止外部请求尝试使用不安全的 HTTP 并与监视流量的任何人共享其凭据。
目前,强制进行基本身份验证的路径已在此处进行硬编码,但显然可以将其放入配置或其他源中。就我而言,“集成”文件夹设置为允许匿名用户。
这里注释掉了一行,CheckUrlAccessForPrincipal如果用户未通过表单身份验证登录,则将使用基本身份验证授予对网站上任何页面的访问权限。
使用Application_AuthenticateRequest而不是Application_AuthorizeRequest最终很重要,因为Application_AuthorizeRequest会强制进行基本身份验证,但无论如何都会重定向到表单身份验证登录页面。我没有成功地通过使用 web.config 中基于位置的权限来完成这项工作,并且从未找到原因。交换到Application_AuthenticateRequest做到了这一点,所以我就这样了。
结果给我留下了一个文件夹,可以在通常使用表单身份验证的应用程序内使用基于 HTTPS 的基本身份验证来访问该文件夹。登录用户无论如何都可以访问该文件夹。
希望这可以帮助。
我认为你走在正确的道路上。但是,我不确定您应该在身份验证请求中完成这项工作。这是识别用户的时间,而不是检查资源权限的时间(稍后在授权请求中)。首先,在 web.config 中,使用<location>删除要使用基本身份验证的资源的表单身份验证。
<configuration>
<!-- don't require forms auth for /public -->
<location path="public">
<authorization>
<allow users="*" />
</authorization>
</location>
</configuration>
Run Code Online (Sandbox Code Playgroud)
然后,不要对特定处理程序进行硬编码或尝试解析 url 以查看您是否位于特定文件夹中,而是Application_AuthorizeRequest像下面这样的内容默认情况下会使一切安全(表单身份验证第一个,如果表单身份验证已被删除,则为基本身份验证)通过<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
}
}
Run Code Online (Sandbox Code Playgroud)
未经测试(只是在此处内联输入),但我已经对自定义会员提供商做了很多工作,您的要求完全可行。
希望其中一些有帮助=)