简体   繁体   English

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

[英]Combining Forms Authentication and Basic Authentication

I have some core ASP code that I want to expose both by secure web pages (using Forms Authentication) and via web services (using Basic Authentication). 我有一些核心ASP代码,我希望通过安全网页(使用表单身份验证)和通过Web服务(使用基本身份验证)公开。

The solution that I've come up with seems to work, but am I missing anything here? 我提出的解决方案似乎有效,但我在这里遗漏了什么吗?

First, the whole site runs under HTTPS. 首先,整个站点在HTTPS下运行。

Site is set to use Forms authentication in web.config 站点设置为在web.config中使用表单身份验证

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

Then I override the AuthenticateRequest in Global.asax, to trigger Basic Authentication on the web service pages: 然后我覆盖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;
}

.Net 4.5 has a new Response property: SuppressFormsAuthenticationRedirect . .Net 4.5有一个新的Response属性: SuppressFormsAuthenticationRedirect When set to true it prevents redirecting a 401 response to the login page of the website. 设置为true时,它会阻止将401响应重定向到网站的登录页面。 You can use the following code snippet in your global.asax.cs to enable Basic Authentication for eg the /HealthCheck folder. 您可以在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;
        }
     }
  }

I got a solution to work based on the OP's ideas and the pointers from Samuel Meacham. 我根据OP的想法和Samuel Meacham的指示获得了解决方案。

In global.asax.cs: 在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;
    }

The credentials that are allowed access are stored in a custom section in the web.config but you can store these how you wish. 允许访问的凭据存储在web.config的自定义部分中,但您可以按照您希望的方式存储这些凭据。

HTTPS is required in the code above but this restriction can be removed if you wish. 上面的代码中需要HTTPS,但如果您愿意,可以删除此限制。 EDIT But as correctly pointed out in the comments this probably isn't a good idea due to the username and password being encoded and visible in plain text. 编辑但正如在评论中正确指出的那样,由于用户名和密码在纯文本中被编码和可见,因此这可能不是一个好主意。 Of course, even with the HTTPS restriction here, you can't stop an external request from trying to use insecure HTTP and sharing their credentials with anyone watching the traffic. 当然,即使这里有HTTPS限制,您也无法阻止外部请求尝试使用不安全的HTTP并与观看流量的任何人共享其凭据。

A path on which to force basic authentication is hardcoded here for now but obviously could be put in config or some other source. 强制基本身份验证的路径目前在这里是硬编码的,但显然可以放在配置或其他来源中。 In my case the 'integration' folder was set to allow anonymous users. 在我的例子中,'integration'文件夹设置为允许匿名用户。

There's a line commented out here involving CheckUrlAccessForPrincipal that will grant access to any page on the site using basic auth if a user is not logged in via Forms Authentication. 这里有一行注释涉及CheckUrlAccessForPrincipal ,如果用户未通过表单身份验证登录,它将使用基本身份验证授予对站点上任何页面的访问权限。

Using Application_AuthenticateRequest instead of Application_AuthorizeRequest ended up being important as Application_AuthorizeRequest would force basic auth but then redirect to the Forms Authentication login page anyway. 使用Application_AuthenticateRequest而不是Application_AuthorizeRequest最终变得很重要,因为Application_AuthorizeRequest会强制进行基本身份验证,但无论如何都会重定向到Forms身份验证登录页面。 I didn't succeed in making this work by playing with the location based permissions in web.config and never found the reason for this. 我没有成功通过在web.config中使用基于位置的权限来完成这项工作,但从未找到原因。 Swapping to Application_AuthenticateRequest did the trick so I left it at that. 交换到Application_AuthenticateRequest做了伎俩,所以我把它留在了那里。

The result of this left me with a folder that could be accessed using basic auth over HTTPS inside an application that normally uses Form Authentication. 这样做的结果给我留下了一个文件夹,可以在通常使用表单身份验证的应用程序中使用基本身份验证通过HTTPS访问。 Logged in users can access the folder anyway. 登录用户无论如何都可以访问该文件夹。

Hope this helps. 希望这可以帮助。

You're on the right path I think. 我认为你走的是正确的道路。 I'm not sure you should be doing the work in authenticate request, however. 但是,我不确定您是否应该在身份验证请求中执行此操作。 That's when the user is identified, not when permission to the resource is checked (that's later in authorize request). 这是在识别用户时,而不是在检查资源许可时(稍后在授权请求中)。 First, in your web.config, use <location> to remove forms auth for resources where you want to use basic auth. 首先,在您的web.config中,使用<location>删除要使用基本身份验证的资源的表单身份验证。

Web.config Web.config文件

<configuration>
    <!-- don't require forms auth for /public -->
    <location path="public">
        <authorization>
            <allow users="*" />
        </authorization>
    </location>
</configuration>

Global.asax.cs or wherever (an IHttpModule, etc.) Global.asax.cs或任何地方(IHttpModule等)

Then, instead of hard coding specific handlers or trying to parse the url to see if you're in a specific folder, in Application_AuthorizeRequest , something like the following will make everything secure by default (forms auth 1st, basic auth if forms auth has been removed via <location> settings in web.config). 然后,在Application_AuthorizeRequest ,不是硬编码特定处理程序或尝试解析URL以查看您是否在特定文件夹中,默认情况下,以下内容将使一切安全(表单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
    }
}

Untested (just typed inline here), but I've done a lot with custom membership providers, your requirements are totally doable. 未经测试(仅在此处键入内联),但我已经使用自定义成员资格提供程序做了很多,您的要求完全可行。

Hope some of this is helpful =) 希望其中一些有用=)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM