16

我有一个包含 WebForms 和 MVC 页面的大型企业应用程序。它具有我不想更改的现有身份验证和授权设置。

WebForms 身份验证在 web.config 中配置:

 <authentication mode="Forms">
  <forms blah... blah... blah />
 </authentication>

 <authorization>
  <deny users="?" />
 </authorization>

到目前为止相当标准。我有一个 REST 服务,它是这个大型应用程序的一部分,我想对这个服务使用 HTTP 身份验证。

因此,当用户尝试从 REST 服务获取 JSON 数据时,它会返回 HTTP 401 状态和WWW-Authenticate标头。如果他们以正确格式的 HTTPAuthorization响应进行响应,它将允许他们进入。

问题是 WebForms 在低级别覆盖了它 - 如果您返回 401(未经授权),它会用 302(重定向到登录页面)覆盖它。这在浏览器中很好,但对 REST 服务无用。

我想关闭 web.config 中的身份验证设置,覆盖“rest”文件夹:

 <location path="rest">
  <system.web>
   <authentication mode="None" />
   <authorization><allow users="?" /></authorization>
  </system.web>
 </location>

授权位工作正常,但身份验证行 ( <authentication mode="None" />) 会导致异常:

在应用程序级别之外使用注册为 allowDefinition='MachineToApplication' 的部分是错误的。

我在应用程序级别配置它 - 它在根 web.config 中 - 并且该错误是针对子目录中的 web.configs 的。

如何覆盖身份验证,以便站点的所有其余部分都使用 WebForms 身份验证,而这个目录不使用任何身份验证?

这类似于另一个问题:401 response code for json requests with ASP.NET MVC,但我不是在寻找相同的解决方案 - 我不想只是删除 WebForms 身份验证并在全局范围内添加新的自定义代码,还有很远涉及很多风险和工作。我只想更改配置中的一个目录。

更新

我想设置一个 Web 应用程序,并且我希望所有 WebForms 页面和 MVC 视图都使用 WebForms 身份验证。我希望一个目录使用基本的 HTTP 身份验证。

请注意,我说的是身份验证,而不是授权。我希望 REST 调用带有 HTTP 标头中的用户名和密码,并且我希望 WebForm 和 MVC 页面带有来自 .Net 的身份验证 cookie - 在任何一种情况下,授权都是针对我们的数据库进行的。

我不想重写 WebForms 身份验证并滚动我自己的 cookie - 将 HTTP 授权的 REST 服务添加到应用程序的唯一方法似乎很荒谬。

我不能添加额外的应用程序或虚拟目录 - 它必须作为一个应用程序。

4

7 回答 7

10

如果“rest”只是根目录中的一个文件夹,那么您几乎就在那里:删除身份验证行,即

<location path="rest">
  <system.web>
      <authorization>
        <allow users="*" />
      </authorization>
  </system.web>
 </location>

或者,您可以将 web.config 添加到您的 rest 文件夹中,然后就可以了:

<system.web>
     <authorization>
          <allow users="*" />
     </authorization>
</system.web>

检查这个

于 2011-01-06T16:49:01.917 回答
4

我已经以混乱的方式解决了这个问题 - 通过在 global.asax 中为所有现有页面欺骗 Forms 身份验证。

我仍然没有完全工作,但它是这样的:

protected void Application_BeginRequest(object sender, EventArgs e)
{
    // lots of existing web.config controls for which webforms folders can be accessed
    // read the config and skip checks for pages that authorise anon users by having
    // <allow users="?" /> as the top rule.

    // check local config
    var localAuthSection = ConfigurationManager.GetSection("system.web/authorization") as AuthorizationSection;

    // this assumes that the first rule will be <allow users="?" />
    var localRule = localAuthSection.Rules[0];
    if (localRule.Action == AuthorizationRuleAction.Allow &&
        localRule.Users.Contains("?"))
    {
        // then skip the rest
        return;
    }

    // get the web.config and check locations
    var conf = WebConfigurationManager.OpenWebConfiguration("~");
    foreach (ConfigurationLocation loc in conf.Locations)
    {
        // find whether we're in a location with overridden config
        if (this.Request.Path.StartsWith(loc.Path, StringComparison.OrdinalIgnoreCase) ||
            this.Request.Path.TrimStart('/').StartsWith(loc.Path, StringComparison.OrdinalIgnoreCase))
        {
            // get the location's config
            var locConf = loc.OpenConfiguration();
            var authSection = locConf.GetSection("system.web/authorization") as AuthorizationSection;
            if (authSection != null)
            {
                // this assumes that the first rule will be <allow users="?" />
                var rule = authSection.Rules[0];
                if (rule.Action == AuthorizationRuleAction.Allow &&
                    rule.Users.Contains("?"))
                {
                    // then skip the rest
                    return;
                }
            }
        }
    }

    var cookie = this.Request.Cookies[FormsAuthentication.FormsCookieName];
    if (cookie == null ||
        string.IsNullOrEmpty(cookie.Value))
    {
        // no or blank cookie
        FormsAuthentication.RedirectToLoginPage();
    }

    // decrypt the 
    var ticket = FormsAuthentication.Decrypt(cookie.Value);
    if (ticket == null ||
        ticket.Expired)
    {
        // invalid cookie
        FormsAuthentication.RedirectToLoginPage();
    }

    // renew ticket if needed
    var newTicket = ticket;
    if (FormsAuthentication.SlidingExpiration)
    {
        newTicket = FormsAuthentication.RenewTicketIfOld(ticket);
    }

    // set the user so that .IsAuthenticated becomes true
    // then the existing checks for user should work
    HttpContext.Current.User = new GenericPrincipal(new FormsIdentity(newTicket), newTicket.UserData.Split(','));

}

我对此并不满意——这似乎是一个可怕的黑客和轮子的重新发明,但看起来这是我的表单认证页面和 HTTP 认证 REST 服务工作的唯一方法相同的应用程序。

于 2011-01-18T08:55:04.427 回答
4

我发现自己遇到了同样的问题,以下文章为我指明了正确的方向:http: //msdn.microsoft.com/en-us/library/aa479391.aspx

MADAM 完全符合您的要求,具体来说,您可以配置 FormsAuthenticationDispositionModule 以使表单身份验证“诡计”静音,并阻止其将响应代码从 401 更改为 302。这应该会导致您的其余客户端收到正确的身份验证挑战。

MADAM 下载页面:http ://www.raboof.com/projects/madam/

就我而言,REST 调用是对“API”区域中的控制器(这是一个基于 MVC 的应用程序)进行的。使用以下配置设置 MADAM 鉴别器:

<formsAuthenticationDisposition>
  <discriminators all="1">
    <discriminator type="Madam.Discriminator">
      <discriminator
          inputExpression="Request.Url"
          pattern="api\.*" type="Madam.RegexDiscriminator" />
    </discriminator>
  </discriminators>
</formsAuthenticationDisposition>

然后您所要做的就是将 MADAM 模块添加到您的 web.config

<modules runAllManagedModulesForAllRequests="true">
  <remove name="WebDAVModule" /> <!-- allow PUT and DELETE methods -->
  <add name="FormsAuthenticationDisposition" type="Madam.FormsAuthenticationDispositionModule, Madam" />
</modules>

请记住将有效部分添加到 web.config(所以没有让我粘贴代码),您可以从下载的 web 项目中获取示例。

通过此设置,对以“API/”开头的 URL 发出的任何请求都将获得 401 响应,而不是 Forms Authentication 产生的 301。

于 2012-01-21T04:52:19.627 回答
2

我能够让它在以前的项目中工作,但它确实需要使用 HTTP 模块来执行自定义基本身份验证,因为帐户验证是针对数据库而不是 Windows。

我按照您在测试网站根目录下的一个 web 应用程序和一个包含 REST 服务的文件夹来设置测试。根应用程序的配置被配置为拒绝所有访问:

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

然后我必须在 IIS 中为 REST 文件夹创建一个应用程序,并将 web.config 文件放入 REST 文件夹中。在该配置中,我指定了以下内容:

<authentication mode="None"/>
<authorization>
  <deny users="?"/>
</authorization>

我还必须在 REST 目录的配置中的适当位置连接 http 模块。该模块必须进入 REST 目录下的 bin 目录。我使用了 Dominick Baier 的自定义基本身份验证模块,该代码位于此处。该版本更特定于 IIS 6,但是在codeplex上也有 IIS 7 的版本,但我没有测试过那个版本(警告: IIS6 版本与 IIS7 版本没有相同的程序集名称和命名空间。)我真的很喜欢这个基本的身份验证模块,因为它直接插入到 ASP.NET 的成员模型中。

最后一步是确保只允许匿名访问 IIS 中的根应用程序和 REST 应用程序。

为了完整起见,我在下面包含了完整的配置。测试应用程序只是一个从 VS 2010 生成的 ASP.NET Web 表单应用程序,它使用 AspNetSqlProfileProvider 作为会员提供程序;这是配置:

<?xml version="1.0"?>

<configuration>
  <connectionStrings>
    <add name="ApplicationServices"
      connectionString="data source=.\SQLEXPRESS;Integrated Security=SSPI;Database=sqlmembership;"
    providerName="System.Data.SqlClient" />
  </connectionStrings>

  <system.web>
    <compilation debug="true" targetFramework="4.0" />

    <authentication mode="Forms">
      <forms loginUrl="~/Account/Login.aspx" timeout="2880" />
    </authentication>

    <authorization>
      <deny users="?"/>
    </authorization>

    <membership>
      <providers>
        <clear/>
        <add name="AspNetSqlMembershipProvider" type="System.Web.Security.SqlMembershipProvider" connectionStringName="ApplicationServices"
          enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" requiresUniqueEmail="false"
          maxInvalidPasswordAttempts="5" minRequiredPasswordLength="6" minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10"
        applicationName="/" />
      </providers>
    </membership>

    <profile>
      <providers>
        <clear/>
        <add name="AspNetSqlProfileProvider" type="System.Web.Profile.SqlProfileProvider" connectionStringName="ApplicationServices" applicationName="/"/>
      </providers>
    </profile>

    <roleManager enabled="false">
      <providers>
        <clear/>
        <add name="AspNetSqlRoleProvider" type="System.Web.Security.SqlRoleProvider" connectionStringName="ApplicationServices" applicationName="/" />
        <add name="AspNetWindowsTokenRoleProvider" type="System.Web.Security.WindowsTokenRoleProvider" applicationName="/" />
      </providers>
    </roleManager>

  </system.web>

  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true"/>
  </system.webServer>
</configuration>

REST 目录包含一个从 VS 2010 生成的空 ASP.NET 项目,我将一个 ASPX 文件放入其中,但是 REST 文件夹的内容不必一个新项目。在目录有与之关联的应用程序之后放入配置文件应该可以工作。该项目的配置如下:

<?xml version="1.0"?>
<configuration>
  <configSections>
    <section name="customBasicAuthentication" type="Thinktecture.CustomBasicAuthentication.CustomBasicAuthenticationSection, Thinktecture.CustomBasicAuthenticationModule"/>
  </configSections>
  <customBasicAuthentication
    enabled="true"
    realm="testdomain"
    providerName="AspNetSqlMembershipProvider"
    cachingEnabled="true"
    cachingDuration="15"
  requireSSL="false" />

  <system.web>
    <authentication mode="None"/>
    <authorization>
      <deny users="?"/>
    </authorization>

    <compilation debug="true" targetFramework="4.0" />
    <httpModules>
      <add name="CustomBasicAuthentication" type="Thinktecture.CustomBasicAuthentication.CustomBasicAuthenticationModule, Thinktecture.CustomBasicAuthenticationModule"/>
    </httpModules>
  </system.web>
</configuration>

我希望这能满足您的需求。

于 2011-01-18T07:31:41.780 回答
2

在 .NET 4.5 中,您现在可以设置

Response.SuppressFormsAuthenticationRedirect = true

检查此页面:https ://msdn.microsoft.com/en-us/library/system.web.httpresponse.suppressformsauthenticationredirect.aspx

于 2016-02-21T18:30:25.693 回答
1

这可能不是最优雅的解决方案,但我认为这是一个好的开始

1)创建一个HttpModule。

2) 处理 AuthenticateRequest 事件。

3)在事件处理程序中检查请求是否发送到您要允许访问的目录。

4)如果是然后手动设置身份验证cookie:(或者看看你现在是否可以找到另一种方式,因为你已经控制并且身份验证尚未发生)

FormsAuthentication.SetAuthCookie("Anonymous", false);

5)哦,差点忘了,如果请求不是针对您要授予访问权限的目录,您需要确保清除 auth cookie。

于 2011-01-17T16:59:13.653 回答
1

在查看了您对我之前的回答的评论后,我想知道您是否可以让您的 Web 应用程序自动在您的 REST 目录上部署应用程序。这将使您获得第二个应用程序的好处,并且还可以减轻系统管理员的部署负担。

我的想法是,您可以将例程放入Application_Startglobal.asax 的方法中,以检查 REST 目录是否存在,并且它还没有与之关联的应用程序。如果测试返回 true,则将新应用程序关联到 REST 目录的过程发生。

我的另一个想法是,您可以使用WIX(或其他部署技术)来构建管理员可以运行以创建应用程序的安装包,但我认为这不像让应用程序配置其依赖项那样自动。

下面,我包含了一个示例实现,它检查给定目录的 IIS,如果还没有应用程序,则将应用程序应用到它。该代码已使用 IIS 7 进行了测试,但也可以在 IIS 6 上运行。

//This is part of global.asax.cs
//This approach may require additional user privileges to query IIS

//using System.DirectoryServices;
//using System.Runtime.InteropServices;

protected void Application_Start(object sender, EventArgs evt)
{
  const string iisRootUri = "IIS://localhost/W3SVC/1/Root";
  const string restPhysicalPath = @"C:\inetpub\wwwroot\Rest";
  const string restVirtualPath = "Rest";

  if (!Directory.Exists(restPhysicalPath))
  {
    // there is no rest path, so do nothing
    return;
  }

  using (var root = new DirectoryEntry(iisRootUri))
  {
    DirectoryEntries children = root.Children;

    try
    {
      using (DirectoryEntry rest = children.Find(restVirtualPath, root.SchemaClassName))
      {
        // the above call throws an exception if the vdir does not exist
        return;
      }
    }
    catch (COMException e)
    {
      // something got unlinked incorrectly, kill the vdir and application
      foreach (DirectoryEntry entry in children)
      {
        if (string.Compare(entry.Name, restVirtualPath, true) == 0)
        {
          entry.DeleteTree();
        }     
      }
    }
    catch (DirectoryNotFoundException e)
    {
      // the vdir and application do not exist, add them below
    }

    using (DirectoryEntry rest = children.Add(restVirtualPath, root.SchemaClassName))
    {
      rest.CommitChanges();
      rest.Properties["Path"].Value = restPhysicalPath;
      rest.Properties["AccessRead"].Add(true);
      rest.Properties["AccessScript"].Add(true);
      rest.Invoke("AppCreate2", true);
      rest.Properties["AppFriendlyName"].Add(restVirtualPath);
      rest.CommitChanges();
    }
  }
}

这段代码的一部分来自这里。祝你的应用好运!

于 2011-01-18T20:40:39.443 回答