CodeZine(コードジン)

特集ページ一覧

ASP.NETサイトナビゲーションの9つの問題のソリューション:パート2

ASP.NET 2.0における高度なサイトマップソリューション

  • ブックマーク
  • LINEで送る
  • このエントリーをはてなブックマークに追加
2006/09/29 00:00

本シリーズでは、一般的なサイトマップ問題と、ASP.NET 2.0の新しいナビゲーション機能を使ってそれらの問題を解決する方法を紹介します。パート2では、サイト内のページへのユーザーアクセスを許可/拒否する方法と、データベースドリブンのデータを使って動的なサイトマップを作成する方法を解説します。

目次

はじめに

 2回シリーズのパート1では、よくある7つのサイトマップナビゲーション問題とそのソリューションを紹介しました。パート2では、残る次の2つの問題を取り上げ、より高度な手法を解説します。

  • 許可されていないページを非表示にする
  • サイトマップデータにデータベースドリブンのコンテンツを取り込む

 1つ目の問題のソリューションでは、ASP.NET 2.0の承認とページレベルのセキュリティを簡単に復習し、2つ目の問題のソリューションでは、サイトマッププロバイダモデルの拡張とASP.NET 2.0の新しいSqlCacheDependencyクラスを使った動的コンテンツのキャッシングを学習します。

#8:許可されていないページを非表示にする

 ASP.NET 1.1で許可されていないページを非表示にするには、LinkButtonコントロールの可視性を設定するか、User.IsInRole()呼び出しを使って、コードのセクションの実行を手動で禁止/可能にする必要がありました。それに対し、ASP.NET 2.0には、構成可能であるだけでなく、拡張も可能で、コード不要のアプローチが用意されています。これを設定するには3つのステップを実行します。

  1. セキュリティトリミングを使うようにSiteMapProviderを構成します。
  2. ロールを取得するようにRoleProviderを構成します。
  3. ページレベルまたはディレクトリレベルの承認規則を構成します。

 以降では、各ステップについて詳しく説明していきます。

ステップ1:セキュリティトリミングを有効にする

 セキュリティトリミングを有効にすると、.NET FrameworkはSiteMapDataSourceによって公開されるsiteMapNodesを承認情報に基づいて制限します。セキュリティトリミングを使うようにSiteMapProviderを構成するには、次のようにアプリケーションの「web.config」ファイル内のXmlSiteMapProviderにsecurityTrimmingEnabled="true"属性を追加します。

<siteMap defaultProvider="XmlSiteMapProvider" enabled="true">
  <providers>
    <add name="XmlSiteMapProvider"
      description="Default SiteMap provider"
      type="System.Web.XmlSiteMapProvider"
      siteMapFile="Web.sitemap"
      securityTrimmingEnabled="true" />
  </providers>
</siteMap>

 「150以上のノードがサイトマップファイルに含まれている場合は、セキュリティトリミング操作の実行にかなりの時間がかかる可能性があります」というMicrosoftの注意事項に注目してください。Microsoftは、roles属性(このソリューションの最後に説明します)を使ってこの潜在的なパフォーマンスの低下問題を緩和することを推奨しています。

ステップ2:ロールを取得する

 .NET Frameworkは、どのユーザーにどのロールを割り当てるかをRoleProvidersに基づいて判断します。ASP.NET 2.0には3つの組み込みロールプロバイダがあり、以降で説明するようにRoleProviderクラスを拡張して独自の承認を扱うことも可能です。3つの組み込みロールプロバイダは次のとおりです。

  • WindowsTokenRoleProvider
  • ユーザーが属するグループからロール情報を取得します。ただし、これはWindows認証にしか使用できません。
  • AuthorizationStoreRoleProvider
  • Microsoft承認マネージャ(AzMan)を利用して、Active DirectoryまたはXMLファイルからロール情報を取得することができます。
  • SqlRoleProvider
  • 「App_Code」ディレクトリに置かれているSQL Server Expressデータベースまたはaspnet_regsql.exeコマンドで作成できるSQL Server内のテーブルのいずれかから承認情報を取得します。

 これらのプロバイダの実装は複雑ではありませんが、本稿では説明を省きます。詳しくは、「関連記事」で紹介している他の記事を参照してください。

カスタムなロールプロバイダの例

 残念ながら、すべてのアプリケーションが組み込みのロールプロバイダを使用できるわけではありません。例えば、ASP.NET 1.1からアプリケーションを移行したり、Oracleやカスタムなデータストアに承認情報を保持したりする場合はどうするのでしょうか。

 User.IsInRole()や宣言セキュリティを使う必要がある場合は、おそらく「Login」ページのデータソースからデータを取得し、該当するロールを認証チケットに格納して、それを新しいGenericPrincipalとして「Global.asax」ファイル内のApplication_AuthenticateRequestイベントのHttpContext.Current.Userに追加することになります。その場合、「Login」ページは次のような関数を呼び出します。

private void SetAuthenticationTicket(string strUserName) {
  string strRoles =
    GetCommaDelimitedRolesFromDataSource(strUserName);
  FormsAuthenticationTicket tkt = new
    FormsAuthenticationTicket(1, strUserName,
    DateTime.Now, DateTime.Now.AddMinutes(20),
    false, strRoles);
  string cookiestr = FormsAuthentication.Encrypt(tkt);
  HttpCookie ck = new HttpCookie(
    FormsAuthentication.FormsCookieName, cookiestr);
  ck.Path = FormsAuthentication.FormsCookiePath;
  Response.Cookies.Add(ck);
}

 このアプローチをサイトマップにリンクするには、RoleProviderを継承する新しいクラスを作成して、GetRolesForUser()メソッドをオーバーライドする必要があります。カスタムクラスでオーバーライドしたGetRolesForUser()メソッドは「Global.asax」ファイルのAuthenticateRequestイベントとほぼ同じ内容になります。

public class CustomRoleProvider : RoleProvider {
  ...
  public override string[] GetRolesForUser(string username) {
    // if the user is authenticated
    if (HttpContext.Current.User != null) {
      // retrieve the list of roles assigned during log in
      FormsIdentity id = (
        FormsIdentity)HttpContext.Current.User.Identity;
      FormsAuthenticationTicket tkt = id.Ticket;
      HttpCookie authcookie = HttpContext.Current.Request.Cookies[
        FormsAuthentication.FormsCookieName];
      FormsAuthenticationTicket authTicket =
        (FormsAuthenticationTicket)FormsAuthentication.Decrypt(
        authcookie.Value);
      string[] astrRoles = authTicket.UserData.Split(',');

      return astrRoles;
    }
  return null;
  }
}

 完全なCustomRoleProviderクラスをリスト1に示します。このクラスは本稿のサンプルファイルにも収録されています。

リスト1 CustomRoleProviderクラス
using System;
using System.Data;
using System.Configuration;
using System.ComponentModel;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Security.Principal;
using System.Collections;
using System.Web.SessionState;

using AutomatedArchitecture.NestedLoopsNorthwind.ServiceInterface;

public class CustomRoleProvider : RoleProvider {
  public override string ApplicationName {
    get {
      return System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath;
    }
    set {
      throw new Exception("The method or operation is not implemented.");
    }
  }

  public override string[] GetRolesForUser(string username) {
    // if the user is authenticated
    if (HttpContext.Current.User != null) {
      if (HttpContext.Current.User.Identity.AuthenticationType.Equals(
        "Forms")) {
        // retrieve the list of available actions assigned during log
        // in from the user data of the security cookie
        FormsIdentity id = (FormsIdentity)
          HttpContext.Current.User.Identity;
        FormsAuthenticationTicket tkt = id.Ticket;
        HttpCookie authcookie =
          System.Web.HttpContext.Current.Request.Cookies[
          FormsAuthentication.FormsCookieName];
        FormsAuthenticationTicket authTicket = (FormsAuthenticationTicket)
          FormsAuthentication.Decrypt(authcookie.Value);
        string[] astrActions = authTicket.UserData.Split(',');

        return astrActions;
      }
      return null;
    }
    return null;
  }

  public override bool IsUserInRole(string username, string roleName) {
    throw new Exception("The method or operation is not implemented.");
  }

  public override void CreateRole(string roleName) {
    throw new Exception("The method or operation is not implemented.");
  }

  public override bool DeleteRole(string roleName,
    bool throwOnPopulatedRole)  {
    throw new Exception("The method or operation is not implemented.");
  }

  public override bool RoleExists(string roleName) {
    throw new Exception("The method or operation is not implemented.");
  }

  public override void AddUsersToRoles(string[] usernames,
    string[] roleNames) {
    throw new Exception("The method or operation is not implemented.");
  }

  public override void RemoveUsersFromRoles(string[] usernames,
    string[] roleNames) {
    throw new Exception("The method or operation is not implemented.");
  }

  public override string[] GetUsersInRole(string roleName) {
    throw new Exception("The method or operation is not implemented.");
  }

  public override string[] GetAllRoles() {
    throw new Exception("The method or operation is not implemented.");
  }

  public override string[] FindUsersInRole(string roleName,
    string usernameToMatch) {
    throw new Exception("The method or operation is not implemented.");
  }
}

 もちろん、CustomRoleProviderの存在をASP.NETに通知する必要があります。これは「Web.config」ファイルで行います。

<roleManager enabled="true" defaultProvider="CustomRoleProvider">
  <providers>
    <clear />
    <add name="CustomRoleProvider" type="CustomRoleProvider" />
  </providers>
</roleManager>

ステップ3:承認規則を構成する

 最後のステップは、どのロールにどのページまたはどのディレクトリへのアクセス権を付与するかの構成です。最善のアプローチは、「Web.config」ファイル内でauthorization allowステートメントとauthorization denyステートメントを使用することです。.NET Frameworkはこの情報に基づいてページへのすべてのユーザーアクセスの許可/拒否を自動的に実行します。素晴らしいのは、SiteMapProviderがこの同じ情報に基づいて、SiteMapDataSourceから提供するsiteMapNodesを決定できることです(セキュリティトリミングを有効にしていることが前提です)。その結果、コードを1行も書かずに、サイトナビゲーション全体において各ユーザーにそれぞれが適切なアクセス権を持っているページだけを表示することができます。

 allow構成ステートメントとdeny構成ステートメントの設定は手間のかかるところです。承認規則が単純な場合は、ロールごとに1つのディレクトリを作成し、次のような構成設定によってディレクトリごとに許可/拒否することができます。

  <location path="AdminOnlyDir">
    <system.Web>
      <authorization>
        <allow roles="AdminRole" />
        <deny users="?" />
        <deny roles="ReadOnlyRole" />
      </authorization>
    </system.Web>
  </location>
</configuration>

 users="?"ステートメントは匿名ユーザーを表します。users="*"ステートメントはすべてのユーザーを表します。従って、この構成では、承認されていないユーザーとReadOnlyRoleに属するユーザーを拒否し、AdminRoleに属するユーザーを許可します。

 では、誰かがClerksRoleという新しいロールを作成したのに、「AdminOnlyDir」ディレクトリへのアクセスを明示的に拒否することを忘れてしまったらどうなるでしょうか。ASP.NET 2.0は、既定ですべてのユーザーに全ディレクトリおよび全ページへのアクセス権が付与されているものと見なします。これは、サイトのルートに<allow users="*" />を挿入したのと同じことを意味します。アクセス許可はディレクトリの下層へとカスケードされることに注意してください。従って、新ロールClerksRoleは、本来は必要ないのに「AdminOnlyDir」ディレクトリへのアクセス権を持つことになります。

 セキュリティに対してもっと厳しいアプローチを実装するには、サイトのルートに<deny users="*" />を設定してから、allowステートメントを使って個々のページまたはディレクトリへのアクセスを許可します。次に例を示します。

<configuration>
  <system.Web>
    ...
    <authorization>
      <allow roles="AdminRole" />
      <deny users="*" />
    </authorization>
    ...
  </system.Web>
  <location path="ReadOnlyDir">
    <system.Web>
      <authorization>
        <allow roles="ReadOnlyRole" />
      </authorization>
    </system.Web>
  </location>
</configuration>

 このアプローチでは、AdminRoleに全ページへのアクセスを許可する一方で、明示的にアクセスが許可されていない場合はその他のすべてのロールに全ページへのアクセスを拒否します。例えば、この構成ではAdminRoleに「AdminOnlyDir」ディレクトリへのアクセス権が付与されますが、ReadOnlyRoleおよび新しいすべてのロールにはこのディレクトリへのアクセス権が付与されていません。ステートメントの順序は重要です。denyステートメントは必ずallowステートメントの後に配置しなければなりません。

roles属性

 「Web.config」セキュリティ承認規則を設定した後で、ユーザーがアクセスを許可されていないページを表示しなければならないこともあります。例えば、サイトの既定ページです。ユーザーにそのページを表示する必要があるのに、ユーザーがアクセスしようとするとログインが要求されます。「Web.config」内のセキュリティ設定は、「Web.sitemap」ファイル内のsiteMapNoderoles属性によってオーバーライドすることができます。

<?xml version="1.0" encoding="utf-8" ?>
<siteMap xmlns=
    "http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
  <siteMapNode url="~/Default.aspx" title="Home" roles="*">
    <siteMapNode url="~/SiteMap.aspx" title="Site Map" >
  </siteMapNode>
</siteMap>

 フォーム認証が正しく設定されていれば、「Default.aspx」に対する承認を拒否されたユーザーはASP.NETによって「Login」ページにリダイレクトされます。

 roles="*"の設定が役立つ状況は他に2つあります。外部リソースを参照する場合とパフォーマンスを向上させる場合です。外部リソースにアクセスするページでは、ASP.NETは「Web.config」から承認情報を取得できないので、ロールの属性を*に設定しなければなりません。また、この手法を取ると、.NET Frameworkは各ページのアクセス許可をチェックする必要がなくなるので、パフォーマンスも向上します。この手法は、150ページを超えるサイトでセキュリティトリミングを有効にする場合のパフォーマンス問題の解決に役立ちます。


  • ブックマーク
  • LINEで送る
  • このエントリーをはてなブックマークに追加

あなたにオススメ

著者プロフィール

  • Lee Richardson(Lee Richardson)

    Automated Architecture Inc.の創設者かつCEO。Blue InkアプリケーションおよびJAGソフトウェア開発方法論の作成者でもある。現在はHeadstrongコンサルティング社でシニアコンサルタントとして勤務し、高速アプリケーション開発(Rapid Application...

  • japan.internet.com(ジャパンインターネットコム)

    japan.internet.com は、1999年9月にオープンした、日本初のネットビジネス専門ニュースサイト。月間2億以上のページビューを誇る米国 Jupitermedia Corporation (Nasdaq: JUPM) のニュースサイト internet.com や EarthWeb.c...

バックナンバー

連載:japan.internet.com翻訳記事

もっと読む

All contents copyright © 2005-2021 Shoeisha Co., Ltd. All rights reserved. ver.1.5