はじめに
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つのステップを実行します。
- セキュリティトリミングを使うようにSiteMapProviderを構成します。
- ロールを取得するようにRoleProviderを構成します。
- ページレベルまたはディレクトリレベルの承認規則を構成します。
以降では、各ステップについて詳しく説明していきます。
ステップ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
- AuthorizationStoreRoleProvider
- SqlRoleProvider
これらのプロバイダの実装は複雑ではありませんが、本稿では説明を省きます。詳しくは、「関連記事」で紹介している他の記事を参照してください。
カスタムなロールプロバイダの例
残念ながら、すべてのアプリケーションが組み込みのロールプロバイダを使用できるわけではありません。例えば、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に示します。このクラスは本稿のサンプルファイルにも収録されています。
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」ファイル内のsiteMapNode
のroles
属性によってオーバーライドすることができます。
<?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ページを超えるサイトでセキュリティトリミングを有効にする場合のパフォーマンス問題の解決に役立ちます。