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

时间:2013-05-30 07:22:06

标签: c# asp.net authentication

我有一些核心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;
}

3 个答案:

答案 0 :(得分:9)

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

答案 1 :(得分:4)

我根据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使用基本身份验证来访问通常使用表单身份验证的应用程序。登录用户无论如何都可以访问该文件夹。

希望这有帮助。

答案 2 :(得分:2)

我认为你走的是正确的道路。但是,我不确定您是否应该在身份验证请求中执行此操作。这是在识别用户时,而不是在检查资源许可时(稍后在授权请求中)。首先,在您的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等)

然后,代替硬编码特定处理程序或尝试解析网址以查看您是否在特定文件夹中,在Application_AuthorizeRequest中,以下内容将使所有内容在默认情况下都安全(表单auth 1st,如果已通过web.config中的<location>设置删除了表单身份验证,则为基本身份验证。

/// <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
    }
}

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

希望其中一些有用=)