11

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

3 回答 3

11

.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;
        }
     }
  }
于 2014-09-16T07:45:25.630 回答
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 并与查看流量的任何人共享其凭据。

强制基本身份验证的路径现在在这里是硬编码的,但显然可以放在配置或其他一些源中。就我而言,“集成”文件夹设置为允许匿名用户。

如果用户未通过表单身份验证登录,则此处注释掉的一行涉及CheckUrlAccessForPrincipal使用基本身份验证授予对站点上任何页面的访问权限。

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

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

希望这可以帮助。

于 2014-02-21T12:43:51.873 回答
2

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

网页配置

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

Global.asax.cs 或任何地方(IHttpModule 等)

然后,而不是硬编码特定的处理程序或尝试解析 url 以查看您是否在特定文件夹中,在 中Application_AuthorizeRequest,类似以下的内容默认情况下将使一切安全(forms auth 1st,basic auth if forms auth has been removed通过<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
    }
}

未经测试(只是在这里输入内联),但我已经对自定义会员提供程序做了很多,您的要求是完全可行的。

希望其中一些有用=)

于 2013-11-28T16:04:14.880 回答