问题 结合表单身份验证和基本身份验证


我有一些核心ASP代码,我希望通过安全网页(使用表单身份验证)和通过Web服务(使用基本身份验证)公开。

我提出的解决方案似乎有效,但我在这里遗漏了什么吗?

首先,整个站点在HTTPS下运行。

站点设置为在web.config中使用表单身份验证

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

然后我覆盖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;
}

923
2018-05-30 07:22


起源

只需使用一个 <location> 具有深入的安全性更改的配置。这看起来很危险。 - Grant Thomas
你能详细说说吗? <location>不允许您为不同的位置选择不同的提供程序(这是asp.net的限制)。 - steve cook
不是我知道的: <location><system.web><authentication></authentication></system.web></location> - Grant Thomas
看这里: msdn.microsoft.com/en-us/library/532aee0e(v=vs.71).aspx “配置ASP.NET身份验证支持。此元素只能在机器,站点或应用程序级别声明。任何在子目录或页面级别的配置文件中声明它的尝试都将导致解析器错误消息。” - steve cook


答案:


.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;
        }
     }
  }

7
2017-09-16 07:45





我根据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;
    }

允许访问的凭据存储在web.config的自定义部分中,但您可以按照自己的意愿存储。

上面的代码中需要HTTPS,但如果您愿意,可以删除此限制。 编辑 但正如在评论中正确指出的那样,由于用户名和密码在纯文本中被编码和可见,因此这可能不是一个好主意。当然,即使在此处使用HTTPS限制,您也无法阻止外部请求尝试使用不安全的HTTP并与观看流量的任何人共享其凭据。

现在,强制基本身份验证的路径在这里是硬编码的,但显然可以放在配置或其他来源中。在我的例子中,'integration'文件夹设置为允许匿名用户。

这里有一条评论线 CheckUrlAccessForPrincipal 如果用户未通过表单身份验证登录,则将使用基本身份验证授予对站点上任何页面的访问权限。

运用 Application_AuthenticateRequest 代替 Application_AuthorizeRequest 最终成为重要的 Application_AuthorizeRequest 会强制执行基本身份验证,但无论如何都会重定向到Forms身份验证登录页面。我没有成功通过在web.config中使用基于位置的权限来完成这项工作,但从未找到原因。交换到 Application_AuthenticateRequest 做了这个伎俩所以我把它留在了那里。

这样做的结果给我留下了一个文件夹,可以在通常使用表单身份验证的应用程序中使用基本身份验证通过HTTPS访问。登录用户无论如何都可以访问该文件夹。

希望这可以帮助。


4
2018-02-21 12:43



>>上面的代码中需要HTTPS,但是如果您希望<<<这是一个非常糟糕的想法,则可以删除此限制:)基本身份验证使用纯文本登录/密码详细信息,因此如果您不知道您的密码很容易被截获不要使用HTTPS。 - steve cook
我同意,我从来没有说过这是一个好主意:p我会编辑它以使其更清晰。 - GlacialSpoon


我认为你走的是正确的道路。我不确定你应该做的工作 认证 但请求。这是在识别用户的时候,而不是在检查资源的权限时(稍后的) 授权 请求)。首先,在你的web.config中,使用 <location> 删除要使用基本身份验证的资源的表单身份验证。

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,基本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
    }
}

未经测试(仅在此处键入内联),但我已经完成了很多自定义成员资格提供程序,您的要求是完全可行的。

希望其中一些有用=)


2
2017-11-28 16:04