カスタムロールプロバイダを作る。その1(モデル定義) で作成したロールモデル(RoleInfo,RoleInfoList)を使用するXmlRoleProviderを作成してみます。ロール情報はXmlファイルにストアされます。

カスタムメンバシッププロバイダの作成にあたり次のサイトを参考にさせてもらいました。

- ロール プロバイダの実装
http://msdn.microsoft.com/ja-jp/library/8fw7xh74.aspx
- 方法 : サンプル ロール プロバイダを実装する
http://msdn.microsoft.com/ja-jp/library/317sza4k.aspx

 動作確認環境

  • サーバ : Visual Studio 2008組み込みサーバ(OS はWindows Vista Enterprise)
  • クライアント : IE7
  • 開発環境 : Visual Studio 2008 Professional 英語版
  • .NET 3.5

一部でLINQ, ラムダ式、拡張メソッドなどC#3.0の機能を使用していますが、その部分を書き換えれば.NET 2.0でも動作します。

1. XmlRoleProviderを作成する

カスタムロールプロバイダを作る。その1(モデル定義) から続いて、ClassLibraryプロジェクトのCustomProviderにXmlRoleProvider.csを作成します。XmlRoleProviderをRoleProviderを基本クラスとするようにし、アブストラクトメソッドを実装します。エディタ上で基本クラスとしたRoleProviderを右クリック→Implement Abstract Classを選択すると必須の実装メソッドのスケルトンが作成されるので便利です。

もっとも重要なメソッドのひとつ仮想メソッドのInitializeのスケルトンは作成されないので、自身で仮想関数をオーバーライドする必要があります。カスタムメンバシッププロバイダは初期化時にInitializeメソッドが呼び出されるので、このメソッド内で構成ファイルの内容を読み込んで各構成値をプロパティなどに設定します。

XmlRoleProvider.csのコードを掲載します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Security;
using System.Collections.Specialized;
using System.Configuration.Provider;
using System.IO;
using System.Web;
using System.Xml;
using System.Xml.XPath;
using System.Text.RegularExpressions;
using System.Xml.Serialization;
using System.Web.Caching;

namespace CustomProvider
{
    public class XmlRoleProvider : RoleProvider
    {
        public override void Initialize(string name, NameValueCollection config)
        {
            //
            // Initialize values from web.config.
            //
            if (config == null)
                throw new ArgumentNullException("config");

            if (string.IsNullOrEmpty(name))
                name = "XmlMembershipProvider";

            if (String.IsNullOrEmpty(config["description"]))
            {
                config.Remove("description");
                config.Add("description", "Custom Xml Role provider");
            }

            // Initialize the abstract base class.
            base.Initialize(name, config);

            _applicationName = GetConfigValue(config["applicationName"], System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath);
            _roleFile = GetConfigValue(config["roleFile"], "~/App_Data/Role.xml");
            _reloadFileIfModified = Convert.ToBoolean(GetConfigValue(config["reloadFileIfModified"], "false"));
        }

        private string _applicationName = string.Empty;
        public override string ApplicationName
        {
            get
            {
                return _applicationName;
            }
            set
            {
                _applicationName = value;
            }
        }
        private string _roleFile = string.Empty;
        public string RoleFile
        {
            get { return _roleFile; }
            set { _roleFile = value; }
        }
        public string PhysicalRoleFilePath
        {
            get {
                if (RoleFile.StartsWith("|DataDirectory|"))
                {
                    return RoleFile.Replace("|DataDirectory|", AppDomain.CurrentDomain.GetData("DataDirectory") as string);
                }
                else
                {
                    return HttpContext.Current.Server.MapPath(RoleFile);
                }
            }
        }

        private bool _reloadFileIfModified = false;
        public bool ReloadFileIfModified
        {
            get { return _reloadFileIfModified; }
            set { _reloadFileIfModified = value; }
        }
        public override void AddUsersToRoles(string[] usernames, string[] rolenames)
        {
            EnsureRoleInfoList();
            foreach (string rolename in rolenames)
            {
                if (!RoleExists(rolename))
                {
                    throw new ProviderException("ロールが存在しません.");
                }
            }
            foreach (string username in usernames)
            {
                if (username.Contains(","))
                {
                    throw new ArgumentException("ユーザ名に,を含めることができません.");
                }
                foreach (string rolename in rolenames)
                {
                    if(IsUserInRole(username, rolename))
                        throw new ProviderException("ユーザはすでにロールに含まれています");
                }
            }
            foreach (string rolename in rolenames)
            {
                RoleInfo role = _roles.GetRoleInfo(ApplicationName, rolename);
                role.AddUsers(usernames);
            }
            SaveRoleInfoList();
        }

        public override void CreateRole(string roleName)
        {
            if (roleName.Contains(","))
                throw new ArgumentException("ロール名に,を含めることはできません。");

            EnsureRoleInfoList();
            if(RoleExists(roleName))
                throw new ProviderException("ロールはすでに存在します");

            RoleInfo role = RoleInfo.CreateRoleInfo(ApplicationName, roleName);
            _roles.AddRoleInfo(role);

            SaveRoleInfoList();
        }

        public override bool DeleteRole(string roleName, bool throwOnPopulatedRole)
        {
            EnsureRoleInfoList();
            if (!RoleExists(roleName))
            {
                throw new ProviderException("存在しないロール名です");
            }
            if (throwOnPopulatedRole && GetUsersInRole(roleName).Length > 0)
            {
                throw new ProviderException("ユーザが存在するロールは削除できません。");
            }
            _roles.RemoveRoleInfo(ApplicationName, roleName);

            SaveRoleInfoList();

            return true;
        }

        public override string[] FindUsersInRole(string roleName, string usernameToMatch)
        {
            EnsureRoleInfoList();
            if (!RoleExists(roleName))
            {
                throw new ProviderException("存在しないロール名です");
            }
            RoleInfo role = _roles.GetRoleInfo(ApplicationName, roleName);

            var v = from username in role.GetAllUserInRole()
                    where Regex.IsMatch(username,  usernameToMatch)
                    select username;

            return v.ToArray();
        }

        public override string[] GetAllRoles()
        {
            EnsureRoleInfoList();
            return _roles.GetAllRoles(ApplicationName);
        }

        public override string[] GetRolesForUser(string username)
        {
            EnsureRoleInfoList();
            return _roles.GetRolesForUser(ApplicationName,  username);
        }

        public override string[] GetUsersInRole(string roleName)
        {
            EnsureRoleInfoList();
            if (!RoleExists(roleName))
                throw new ProviderException("存在しないロール名です");

            return _roles.GetRoleInfo(ApplicationName, roleName).GetAllUserInRole();
        }

        public override bool IsUserInRole(string username, string roleName)
        {
            EnsureRoleInfoList();
            if(!RoleExists(roleName))
                throw new ProviderException("存在しないロール名です");

            RoleInfo role = _roles.GetRoleInfo(ApplicationName, roleName);

            return role.ContainUser(username);
        }

        public override void RemoveUsersFromRoles(string[] usernames, string[] rolenames)
        {
            foreach (string rolename in rolenames)
            {
                if (!RoleExists(rolename))
                {
                    throw new ProviderException("存在しないロール名です.");
                }
            }

            foreach (string username in usernames)
            {
                foreach (string rolename in rolenames)
                {
                    if (!IsUserInRole(username, rolename))
                    {
                        throw new ProviderException("ユーザがロールに存在しません.");
                    }
                }
            }
            foreach (string rolename in rolenames)
            {
                RoleInfo role = _roles.GetRoleInfo(ApplicationName, rolename);
                foreach (string username in usernames)
                {
                    role.RemoveUser(username);
                }
            }
            SaveRoleInfoList();
        }

        public override bool RoleExists(string roleName)
        {
            EnsureRoleInfoList();
            return _roles.RoleExists(ApplicationName, roleName);
        }
        //
        // コンフィグ値を取得するヘルパーファンクション
        //
        private string GetConfigValue(string configValue, string defaultValue)
        {
            if (String.IsNullOrEmpty(configValue))
                return defaultValue;

            return configValue;
        }
        private RoleInfoList _roles = null;
        protected virtual void EnsureRoleInfoList()
        {
            lock (this)
            {
                if (ReloadFileIfModified)
                {
                    RoleInfoList roles = HttpContext.Current.Cache["RoleInfoList"] as RoleInfoList;
                    if (roles == null)
                    {
                        roles = LoadData();
                        HttpContext.Current.Cache.Insert("RoleInfoList", roles, new CacheDependency(PhysicalRoleFilePath));
                    }
                    _roles = roles;
                }
                else
                {
                    if (_roles == null) _roles = LoadData();
                }
            }
        }
        protected virtual void SaveRoleInfoList()
        {
            lock (this)
            {
                SaveData(_roles);
            }
        }
        protected virtual void PurgeCache()
        {
            HttpContext.Current.Cache.Remove("RoleInfoList");
        }

        protected virtual RoleInfoList LoadData()
        {
            FileStream fs = null;
            try
            {
                XmlSerializer serializer = new XmlSerializer(typeof(RoleInfoList));
                fs = System.IO.File.OpenRead(PhysicalRoleFilePath);

                return (serializer.Deserialize(fs) as RoleInfoList);
            }
            catch (IOException)
            {
                return new RoleInfoList();
            }
            finally
            {
                if (fs != null) fs.Close();
            }
        }
        protected virtual void SaveData(RoleInfoList roleInfoList)
        {
            FileStream fs = null;
            try
            {
                XmlSerializer serializer = new XmlSerializer(typeof(RoleInfoList));
                fs = new FileStream(PhysicalRoleFilePath, FileMode.Create);

                serializer.Serialize(fs, roleInfoList);
                fs.Flush();
            }
            finally
            {
                if (fs != null) fs.Close();
            }
        }

    }
}

カスタムメンバシッププロバイダと同様に機能ごとに作成→テストして作っていくのがベストだと思いますが、今回は実装すべき機能がメンバシッププロバイダと較べて比較的少なかったので、挫折せずに一気に実装しました。なのでXmlRoleProvider2というクラス名になっていません。

実装方法としてはカスタムメンバシッププロバイダーを作る。その2(メンバシッププロバイダ作成) と同じで、ロール情報の変更、削除やユーザ追加、削除などのあらゆる変更アクティビティが発生するたびに、更新内容をファイルに保存するようにしています。また、アプリケーション外からユーザ情報のファイルが更新された場合に最新の情報を再取得するようにキャッシュを使用しています。この機能を有効にするにはWeb.configのXmlMembershipProvider2の設定で、reloadFileIfModifiedにtrueを設定してやるようにします。

排他制御に関してはあまり意識していないです。変更が発生するメソッド呼び出しがある場合はロックを取得するようにしたほうがよいと思います。

2. Webサイト管理ツールで動作確認

Web.configのロールプロバイダ、メンバシッププロバイダを次のように編集してWebサイト管理ツールを起動してみす。

<membership defaultProvider="XmlMembershipProvider">
        <providers>
	  <add name="XmlMembershipProvider" type="CustomProvider.XmlMembershipProvider2,CustomProvider" memberFile="|DataDirectory|/User.xml" applicationName="/" passwordFormat="Hashed" enablePasswordRetrieval="false" reloadFileIfModified="false"/>
        </providers>
    </membership>
    <roleManager enabled="true" defaultProvider="XmlRoleProvider">
      <providers>
        <clear />
        <add name="XmlRoleProvider" applicationName="/" type="CustomProvider.XmlRoleProvider,CustomProvider" roleFile="|DataDirectory|/Role.xml" reloadFileIfModified="false"/>
      </providers>
    </roleManager>

 起動例です。管理ツールのSecurityタブを使用して、メンバシップとロールの管理を行えます。下図はSecurityタブを押した場合と、ロールの管理画面を表示した図です。

 

 

 

ユーザを作成したり、ロールを作成、削除したりしていますが動いているみたいです。

今回の説明は以上です。問題点指摘点とうがあればご連絡ください。