web-dev-qa-db-ja.com

1つのサブディレクトリのASP.NetWebForms認証をオフにする

WebFormsとMVCページの両方を含む大規模なエンタープライズアプリケーションがあります。変更したくない既存の認証と承認の設定があります。

WebForms認証は、web.configで構成されます。

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

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

これまでのところかなり標準的です。この大きなアプリケーションの一部であるRESTサービスがあり、この1つのサービスの代わりにHTTP認証を使用したいと思います。

したがって、ユーザーがRESTサービスからJSONデータを取得しようとすると、HTTP401ステータスと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>

authorizationビットは正常に機能しますが、authentication行(<authentication mode="None" />)により例外が発生します。

AllowDefinition = 'MachineToApplication'として登録されているセクションをアプリケーションレベルを超えて使用するとエラーになります。

私はこれをアプリケーションレベルで構成しています-それはルートweb.configにあります-そしてそのエラーはサブディレクトリのweb.configsにあります。

authenticationをオーバーライドして、サイトの残りのすべてがWebForms認証を使用し、この1つのディレクトリが何も使用しないようにするにはどうすればよいですか?

これは別の質問に似ています: ASP.NET MVCを使用したjsonリクエストの401応答コード 、しかし私は同じ解決策を探していません-WebForms認証を削除して追加したくない新しいカスタムコードはグローバルに展開されており、リスクと作業が非常に多くなります。構成内の1つのディレクトリのみを変更したい。

更新

単一のWebアプリケーションをセットアップし、すべてのWebFormsページとMVCビューでWebForms認証を使用する必要があります。 1つのディレクトリで基本HTTP認証を使用したい。

私が話しているのは認証ではなく認証であることに注意してください。 REST呼び出しでHTTPヘッダーにユーザー名とパスワードを指定し、WebFormとMVCページに.Netからの認証Cookieを指定する必要があります-いずれの場合も、承認はDB。

WebForms認証を書き直して、自分のCookieをロールバックしたくありません。HTTPで承認されたRESTサービスをアプリケーションに追加する唯一の方法はばかげているようです。

アプリケーションや仮想ディレクトリを追加することはできません。1つのアプリケーションとして使用する必要があります。

16
Keith

私はこれを厄介な方法で回避しました-既存のすべてのページのglobal.asaxでフォーム認証をスプーフィングします。

私はまだこれが完全に機能しているわけではありませんが、次のようになります。

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サービス。

4
Keith

「rest」が単にルート内のフォルダである場合は、ほぼそこにあります。認証行を削除します。

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

または、残りのフォルダーにweb.configを追加して、次のようにすることもできます。

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

チェック this 1つ。

8
gbs

私はまったく同じ問題を抱えていることに気づきました。次の記事は私を正しい方向に向けました: 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に追加することを忘れないでください(SOはコードを貼り付けさせませんでした)。ダウンロードでWebプロジェクトから例を取得できます。

この設定では、「API /」で始まるURLに対して行われたすべての要求は、フォーム認証によって生成された301ではなく401応答を受け取ります。

4
Yosoyadri

以前のプロジェクトでこれを機能させることができましたが、アカウントの検証はWindowsではなくデータベースに対して行われるため、HTTPモジュールを使用してカスタム基本認証を実行する必要がありました。

テストWebサイトのルートにある1つの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>

また、httpモジュールをRESTディレクトリの構成内の適切な場所に接続する必要がありました。このモジュールは、下のbinディレクトリに移動する必要があります RESTディレクトリ。DominickBaierのカスタム基本認証モジュールを使用しました。そのコードは ここ にあります。そのバージョンはもっとIIS 6具体的ですが、 codeplex にもIIS 7のバージョンがありますが、そのバージョンはテストしていません(警告:IIS6バージョンのアセンブリ名と名前はIIS7バージョンと同じではありません。)ASP.NETのメンバーシップモデルに直接プラグインされるため、この基本認証モジュールが本当に気に入っています。

最後のステップは、ルートアプリケーションとIIS内のRESTアプリケーション)の両方に匿名アクセスのみが許可されるようにすることでした。

完全を期すために、以下に完全な構成を含めました。テストアプリは、VS2010から生成されたASP.NETWebフォームアプリケーションであり、メンバーシッププロバイダーに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フォルダーの内容haveが新しいプロジェクトではありませんでした。ディレクトリにアプリケーションが関連付けられた後で構成ファイルをドロップするだけで機能するはずです。そのプロジェクトの構成は次のとおりです。

<?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>

これがあなたのニーズを満たすことを願っています。

2
arcain

.NET 4.5では、設定できるようになりました

Response.SuppressFormsAuthenticationRedirect = true

このページを確認してください: https://msdn.Microsoft.com/en-us/library/system.web.httpresponse.suppressformsauthenticationredirect.aspx

1
Bebben

これは最もエレガントなソリューションではないかもしれませんが、良いスタートだと思います

1)HttpModuleを作成します。

2)AuthenticateRequestイベントを処理します。

3)イベントハンドラーで、アクセスを許可するディレクトリへのリクエストであることを確認します。

4)次に、認証Cookieを手動で設定する場合:(または、制御が可能で、認証がまだ行われていないため、別の方法を見つけることができるかどうかを確認します)

FormsAuthentication.SetAuthCookie("Anonymous", false);

5)ほとんど忘れてしまいましたが、アクセスを許可したいディレクトリへのリクエストではない場合は、認証Cookieがクリアされていることを確認する必要があります。

1
nixon

以前の回答に対するコメントを確認した後、WebアプリでRESTディレクトリへのアプリケーションの展開を自動化できるかどうか疑問に思いました。これにより、1秒のメリットが得られます。アプリケーション、およびシステム管理者の展開の負担も軽減します。

私の考えでは、global.asaxのApplication_Startメソッドにルーチンを入れて、RESTディレクトリが存在し、アプリケーションがまだ関連付けられていないことを確認することができます。テストでtrueが返された場合、新しいアプリケーションをRESTディレクトリに関連付けるプロセスが発生します。

私が持っていたもう1つの考えは、 [〜#〜] 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();
    }
  }
}

このコードの一部は ここ から来ました。あなたのアプリで頑張ってください!

1
arcain