XMLファイルでメンバーを管理するカスタムメンバシッププロバイダXmlMembershipProvider2を作成してみます。メンバシップデータのストアとして、Xmlファイルを使用し、シリアライズには前回のカスタムメンバシッププロバイダーを作る。その1 で作成したUserInfoListクラスとUserInfoクラスを使用します。
カスタムメンバシッププロバイダの作成に関しては、以下のサイトと書籍を参考にさせてもらいました。
参考URLと書籍
-メンバシップ プロバイダの実装
http://msdn.microsoft.com/ja-jp/library/f1kyba5e(VS.80).aspx
-方法 : サンプル メンバシップ プロバイダを実装する
http://msdn.microsoft.com/ja-jp/library/6tc47t75(VS.80).aspx
-Professional ASP.NET Server Control And Component Development
Professional ASP.NET 2.0 Server Control And Component Development (Wrox Professional Guides)
動作環境
- サーバ:Visual Studio 2008組み込みサーバ(OS はWindows Vista Enterprise)
- クライアント: IE 7
- 開発環境: Visual Studio 2008 Professional 英語版
- .NET 3.5
1. Xmlメンバシッププロバイダの作成
カスタムメンバシッププロバイダーを作る。その1から引き続き作業をします。CustomProviderプロジェクトにクラスを新規作成します。クラス名はXmlMebershipProvider2とし、MembershipProviderを基本クラスとして作成します。あとは、エディタ上でMembershipProviderを右クリック→Implement Abstract Classを選択すると必須の実装メソッドのスケルトンが作成されます。
もっとも重要なメソッドのひとつ仮想メソッドのInitializeのスケルトンは作成されないので、自身で仮想関数を定義する必要があります。カスタムメンバシッププロバイダは初期化時にInitializeメソッドが呼び出されるので、このメソッド内で構成ファイルの内容を読み込んで各構成値をプロパティなどに設定します。
また、一度に全部機能を実装する必要はありません。InitializeメソッドとValidateUserを実装すれば、ユーザの検証が行えるようにります(Membership.ValidateUserが使えるようになるという意味です)。あわてず、各機能ごとに実装→テストをしていけばよいと思います。そうでないと、私みたいに挫折するかもしれません(そのため、クラス名がXmlMembershipProvider2という名前になっています。)。
とりあえず、XmlMembershipProvider2.csの実装の中身を掲載します。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web.Security; using System.Security.Cryptography; using System.Configuration.Provider; using System.Web; using System.Xml.Serialization; using System.IO; using System.Text.RegularExpressions; using System.Web.Configuration; using System.Web.Caching; namespace CustomProvider { class XmlMembershipProvider2 : MembershipProvider { private MachineKeySection machineKey; public override void Initialize(string name, System.Collections.Specialized.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 Membership provider"); } // Initialize the abstract base class. base.Initialize(name, config); _applicationName = GetConfigValue(config["applicationName"], System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath); _maxInvalidPasswordAttempts = Convert.ToInt32(GetConfigValue(config["maxInvalidPasswordAttempts"], "5")); _passwordAttemptWindow = Convert.ToInt32(GetConfigValue(config["passwordAttemptWindow"], "10")); _minRequiredNonAlphanumericCharacters = Convert.ToInt32(GetConfigValue(config["minRequiredNonAlphanumericCharacters"], "1")); _minRequiredPasswordLength = Convert.ToInt32(GetConfigValue(config["minRequiredPasswordLength"], "7")); _passwordStrengthRegularExpression = GetConfigValue(config["passwordStrengthRegularExpression"], ""); _enablePasswordReset = Convert.ToBoolean(GetConfigValue(config["enablePasswordReset"], "true")); _enablePasswordRetrieval = Convert.ToBoolean(GetConfigValue(config["enablePasswordRetrieval"], "true")); _requiresQuestionAndAnswer = Convert.ToBoolean(GetConfigValue(config["requiresQuestionAndAnswer"], "false")); _requireUniqueEmail = Convert.ToBoolean(GetConfigValue(config["requiresUniqueEmail"], "true")); _memberFile = GetConfigValue(config["memberFile"], "~/App_Data/Membership.xml"); _reloadFileIfModified = Convert.ToBoolean( GetConfigValue(config["reloadFileIfModified"], "false")); string passwordFormat = config["passwordFormat"]; if (passwordFormat == null) { passwordFormat = "Hashed"; } switch (passwordFormat) { case "Hashed": _passwordFormat = MembershipPasswordFormat.Hashed; break; case "Encrypted": _passwordFormat = MembershipPasswordFormat.Encrypted; break; case "Clear": _passwordFormat = MembershipPasswordFormat.Clear; break; default: throw new ProviderException("サポートされないパスワードのフォーマットです"); } if (PasswordFormat == MembershipPasswordFormat.Hashed && EnablePasswordRetrieval) { throw new ProviderException("passwordFormatがHashの場合、enablePasswordRetrievalをtrueにできません"); } //Configuration configuration = WebConfigurationManager.OpenWebConfiguration(System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath); //machineKey = configuration.GetSection("system.web/machineKey") as MachineKeySection; machineKey = WebConfigurationManager.GetSection("system.web/machineKey") as MachineKeySection; if (machineKey.ValidationKey.Contains("AutoGenerate")) { if (PasswordFormat == MembershipPasswordFormat.Encrypted) { throw new ProviderException("machineKeyがAutoGeneratoの場合、パスワードの形式にEncryptedは指定できません。"); } } } private string _memberFile = string.Empty; /// <summary> /// ユーザデータストア用ファイル /// </summary> public string MemberFile { get { return _memberFile; } set { _memberFile = value; } } public string PhysicalMemberFilePath { get { if (MemberFile.StartsWith("|DataDirectory|")) { return MemberFile.Replace("|DataDirectory|", AppDomain.CurrentDomain.GetData("DataDirectory") as string); } else { return HttpContext.Current.Server.MapPath(MemberFile); } } } private bool _reloadFileIfModified = false; /// <summary> /// キャッシュを使用するかのフラグ /// </summary> public bool ReloadFileIfModified { get { return _reloadFileIfModified; } set { _reloadFileIfModified = value; } } private string _applicationName = string.Empty; public override string ApplicationName { get { return _applicationName; } set { _applicationName = value; } } /// <summary> /// パスワードを変更します。 /// </summary> /// <param name="username"></param> /// <param name="oldPassword"></param> /// <param name="newPassword"></param> /// <returns></returns> public override bool ChangePassword(string username, string oldPassword, string newPassword) { EnsureUserInfoList(); UserInfo user = null; if (!ValidateUserInfo(username, oldPassword, false, false, out user)) { return false; } if (newPassword.Length < this.MinRequiredPasswordLength) { throw new ArgumentException("パスワードが短すぎます"); } int nonAlphanumericCharacterCount = 0; for (int i = 0; i < newPassword.Length; ++i) { if (!char.IsLetterOrDigit(newPassword, i)) ++nonAlphanumericCharacterCount; } if (nonAlphanumericCharacterCount < MinRequiredNonAlphanumericCharacters) throw new ArgumentException("非英数字文字列の数が必要数を満たしていません"); if ((PasswordStrengthRegularExpression.Length > 0) && !Regex.IsMatch(newPassword, PasswordStrengthRegularExpression)) throw new ArgumentException("パスワードがパスワードの正規表現に一致しません"); string encodedPassword = GetEncodedPassword(newPassword, user.PasswordSalt, PasswordFormat); ValidatePasswordEventArgs args = new ValidatePasswordEventArgs(username, newPassword, false); this.OnValidatingPassword(args); if (args.Cancel) { if (args.FailureInformation != null) { throw args.FailureInformation; } throw new ArgumentException("パスワードの検証が失敗しました"); } user.EncodedPassword = encodedPassword; SaveUserInfoList(); return true; } public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer) { EnsureUserInfoList(); string encodedNewAnswer; UserInfo user = null; if (!this.ValidateUserInfo(username, password, false, false, out user)) return false; if (newPasswordAnswer != null) newPasswordAnswer = newPasswordAnswer.Trim(); if (!string.IsNullOrEmpty(newPasswordAnswer)) encodedNewAnswer = GetEncodedPassword(newPasswordAnswer.ToLower(), user.PasswordSalt, PasswordFormat); else encodedNewAnswer = newPasswordAnswer; user.EncodedPasswordAnswer = encodedNewAnswer; user.PasswordQuestion = newPasswordQuestion; SaveUserInfoList(); return true; } public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status) { if ((providerUserKey != null) && !(providerUserKey is Guid)) { status = MembershipCreateStatus.InvalidProviderUserKey; return null; } if (password.Length < MinRequiredNonAlphanumericCharacters) { status = MembershipCreateStatus.InvalidPassword; return null; } int nonAlphanumericCharacterCount = 0; for (int i = 0; i < password.Length; ++i) { if (!char.IsLetterOrDigit(password, i)) ++nonAlphanumericCharacterCount; } if (nonAlphanumericCharacterCount < MinRequiredNonAlphanumericCharacters) { status = MembershipCreateStatus.InvalidPassword; return null; } if ((PasswordStrengthRegularExpression.Length > 0) && !Regex.IsMatch(password, PasswordStrengthRegularExpression)) { status = MembershipCreateStatus.InvalidPassword; return null; } ValidatePasswordEventArgs args = new ValidatePasswordEventArgs(username, password, true); this.OnValidatingPassword(args); if (args.Cancel) { status = MembershipCreateStatus.InvalidPassword; return null; } byte[] randumNumber = new byte[1]; RNGCryptoServiceProvider gen = new RNGCryptoServiceProvider(); gen.GetBytes(randumNumber); string passwordSalt = Convert.ToBase64String(randumNumber); string encodedPassword = GetEncodedPassword(password, passwordSalt, PasswordFormat); string encodedPasswordAnswer; if (passwordAnswer != null) passwordAnswer = passwordAnswer.Trim(); if (!string.IsNullOrEmpty(passwordAnswer)) encodedPasswordAnswer = GetEncodedPassword(passwordAnswer.ToLower(), passwordSalt, PasswordFormat); else encodedPasswordAnswer = passwordAnswer; EnsureUserInfoList(); if (_users.GetUserInfo(username) != null) { status = MembershipCreateStatus.DuplicateUserName; return null; } if (providerUserKey == null) providerUserKey = Guid.NewGuid(); UserInfo user = UserInfo.CreateUserInfo(ApplicationName, username, isApproved, false, string.Empty, email, (Guid)providerUserKey, passwordQuestion, encodedPasswordAnswer, encodedPassword, passwordSalt); _users.AddUserInfo(user); SaveUserInfoList(); status = MembershipCreateStatus.Success; return user.CreateMembershipUser(Name); } public override bool DeleteUser(string username, bool deleteAllRelatedData) { EnsureUserInfoList(); if (_users.RemoveUserInfo(username)) { SaveUserInfoList(); return true; } return false; } private bool _enablePasswordReset; public override bool EnablePasswordReset { get { return _enablePasswordReset; } } private bool _enablePasswordRetrieval; public override bool EnablePasswordRetrieval { get { return _enablePasswordRetrieval; } } public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords) { EnsureUserInfoList(); return _users.FindUsersByEmail(Name, emailToMatch, pageIndex, pageSize, out totalRecords); } public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords) { if (pageIndex < 0) throw new ArgumentException("ページインデックスは0より小さくできません"); if (pageSize < 1) throw new ArgumentException("ページサイズは1より小さくできません"); EnsureUserInfoList(); return _users.FindUsersByName(Name, usernameToMatch, pageIndex, pageSize, out totalRecords); } public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords) { EnsureUserInfoList(); return _users.GetAllUsers(Name, pageIndex, pageSize, out totalRecords); } public override int GetNumberOfUsersOnline() { EnsureUserInfoList(); return _users.GetNumberOfUsersOnline(new TimeSpan(0, Membership.UserIsOnlineTimeWindow, 0)); } public override string GetPassword(string username, string answer) { if (!this.EnablePasswordRetrieval) throw new NotSupportedException("パスワードを取得することができません"); if (string.IsNullOrEmpty(answer) && RequiresQuestionAndAnswer) throw new ArgumentException("答えを入力して下さい"); UserInfo user = _users.GetUserInfo(username); string encodedPaswordAnser = null; if (!string.IsNullOrEmpty(answer)) encodedPaswordAnser = GetEncodedPassword(answer, user.PasswordSalt, PasswordFormat); else encodedPaswordAnser = answer; if (this.RequiresQuestionAndAnswer) { if (!user.EncodedPasswordAnswer.Equals(encodedPaswordAnser)) throw new ProviderException("答えが違います"); } string password = null; if (PasswordFormat == MembershipPasswordFormat.Encrypted && user.EncodedPassword != null) { byte[] decodedPassword = Convert.FromBase64String(user.EncodedPassword); byte[] decryptedPassword = DecryptPassword(decodedPassword); byte[] saltPassword = Convert.FromBase64String(user.PasswordSalt); if (decryptedPassword == null) return null; else password = Encoding.Unicode.GetString(decryptedPassword, saltPassword.Length, decryptedPassword.Length - saltPassword.Length); } else { password = user.EncodedPassword; } return password; } public override MembershipUser GetUser(string username, bool userIsOnline) { EnsureUserInfoList(); UserInfo user = _users.GetUserInfo(username); if (user == null) return null; if (userIsOnline) { user.LastActivityDate = DateTime.Now; SaveUserInfoList(); } return user.CreateMembershipUser(Name); } public override MembershipUser GetUser(object providerUserKey, bool userIsOnline) { if (providerUserKey == null) throw new ArgumentException("providerUserKeyはnullにできません"); if (!(providerUserKey is Guid)) throw new ArgumentException("providerUserKeyはGuid型である必要があります"); EnsureUserInfoList(); UserInfo user = _users.GetUserInfo((Guid)providerUserKey); if (user == null) return null; if (userIsOnline) { user.LastActivityDate = DateTime.Now; SaveUserInfoList(); } return user.CreateMembershipUser(Name); } public override string GetUserNameByEmail(string email) { EnsureUserInfoList(); return _users.GetUserNameByEmail(email); } private int _maxInvalidPasswordAttempts; public override int MaxInvalidPasswordAttempts { get { return _maxInvalidPasswordAttempts; } } private int _minRequiredNonAlphanumericCharacters; public override int MinRequiredNonAlphanumericCharacters { get { return _minRequiredNonAlphanumericCharacters; } } private int _minRequiredPasswordLength; public override int MinRequiredPasswordLength { get { return _minRequiredPasswordLength; } } private int _passwordAttemptWindow; public override int PasswordAttemptWindow { get { return _passwordAttemptWindow; } } private MembershipPasswordFormat _passwordFormat; public override MembershipPasswordFormat PasswordFormat { get { return _passwordFormat; } } private string _passwordStrengthRegularExpression; public override string PasswordStrengthRegularExpression { get { return _passwordStrengthRegularExpression; } } private bool _requiresQuestionAndAnswer; public override bool RequiresQuestionAndAnswer { get { return _requiresQuestionAndAnswer; } } private bool _requireUniqueEmail; public override bool RequiresUniqueEmail { get { return _requireUniqueEmail; } } public override string ResetPassword(string username, string answer) { if (!this.EnablePasswordReset) throw new NotSupportedException(); EnsureUserInfoList(); UserInfo user = _users.GetUserInfo(username); if (user == null) throw new ProviderException("存在しないユーザです"); string encodedPaswordAnser = null; if (!string.IsNullOrEmpty(answer)) encodedPaswordAnser = GetEncodedPassword(answer, user.PasswordSalt, PasswordFormat); else encodedPaswordAnser = answer; if (this.RequiresQuestionAndAnswer) { if (!user.EncodedPasswordAnswer.Equals(encodedPaswordAnser)) throw new ProviderException("答えが違います"); } string newPassword = Membership.GeneratePassword(MinRequiredPasswordLength, MinRequiredNonAlphanumericCharacters); ValidatePasswordEventArgs args = new ValidatePasswordEventArgs(username, newPassword, false); this.OnValidatingPassword(args); if (args.Cancel) { if (args.FailureInformation != null) throw args.FailureInformation; throw new ProviderException("パスワード検証に失敗しました"); } user.EncodedPasswordAnswer = GetEncodedPassword(newPassword, user.PasswordSalt, PasswordFormat); SaveUserInfoList(); return newPassword; } public override bool UnlockUser(string userName) { EnsureUserInfoList(); UserInfo user = _users.GetUserInfo(userName); if (user == null) return false; user.IsLockedOut = false; SaveUserInfoList(); return true; } public override void UpdateUser(MembershipUser user) { if (user == null) throw new ArgumentException("user is null"); EnsureUserInfoList(); UserInfo userInfo = _users.GetUserInfo(user.UserName); if (userInfo == null) throw new ProviderException("存在しないユーザです"); userInfo.Email = user.Email; userInfo.Comment = user.Comment; userInfo.IsApproved = user.IsApproved; userInfo.LastLoginDate = user.LastLoginDate; userInfo.LastActivityDate = user.LastActivityDate; SaveUserInfoList(); } /// <summary> /// ユーザを検証します /// </summary> /// <param name="username"></param> /// <param name="password"></param> /// <returns></returns> public override bool ValidateUser(string username, string password) { EnsureUserInfoList(); UserInfo user; return ValidateUserInfo(username, password, true, true, out user); } protected virtual bool ValidateUserInfo(string username, string password, bool updateLastLoginActivityDate, bool failIfNotApproved, out UserInfo user) { user = null; if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password)) return false; EnsureUserInfoList(); user = _users.GetUserInfo(username); if (user == null || (!user.IsApproved && failIfNotApproved)) { return false; } string encodedPassword = GetEncodedPassword(password, user.PasswordSalt, PasswordFormat); bool isPasswordCorrect = encodedPassword.Equals(user.EncodedPassword); if (updateLastLoginActivityDate) { user.LastActivityDate = DateTime.Now; user.LastLoginDate = DateTime.Now; } if (isPasswordCorrect) return true; return false; } // // コンフィグ値を取得するヘルパーファンクション // private string GetConfigValue(string configValue, string defaultValue) { if (String.IsNullOrEmpty(configValue)) return defaultValue; return configValue; } private UserInfoList _users = null; protected virtual void EnsureUserInfoList() { lock (this) { if (ReloadFileIfModified) { UserInfoList users = HttpContext.Current.Cache["UserInfoList"] as UserInfoList; if (users == null) { users = LoadData(); HttpContext.Current.Cache.Insert("UserInfoList", users, new CacheDependency(PhysicalMemberFilePath)); } _users = users; } else { if (_users == null) _users = LoadData(); } } } protected virtual void SaveUserInfoList() { lock (this) { SaveData(_users); } } protected virtual void PurgeCache() { HttpContext.Current.Cache.Remove("UserInfoList"); } protected virtual UserInfoList LoadData() { FileStream fs = null; try { XmlSerializer serializer = new XmlSerializer(typeof(UserInfoList)); fs = System.IO.File.OpenRead(PhysicalMemberFilePath); return (serializer.Deserialize(fs) as UserInfoList); } catch (IOException) { return new UserInfoList(); } finally { if (fs != null) fs.Close(); } } protected virtual void SaveData(UserInfoList userInfoList) { FileStream fs = null; try { XmlSerializer serializer = new XmlSerializer(typeof(UserInfoList)); fs = new FileStream(PhysicalMemberFilePath, FileMode.Create); serializer.Serialize(fs, userInfoList); fs.Flush(); } finally { if (fs != null) fs.Close(); } } /// <summary> /// サルトを先頭に設定したバイト配列のパスワードを作成する /// </summary> /// <param name="password">エンコード前のパスワードの文字列</param> /// <param name="salt">Base64形式にエンコードされたサルト</param> /// <returns></returns> private byte[] GetSaltedPassword(string password, string salt) { byte[] bPassword = Encoding.Unicode.GetBytes(password); byte[] bSalt = Convert.FromBase64String(salt); byte[] saltedPassword = new byte[bPassword.Length + bSalt.Length]; Buffer.BlockCopy(bSalt, 0, saltedPassword, 0, bSalt.Length); Buffer.BlockCopy(bPassword, 0, saltedPassword, bSalt.Length, bPassword.Length); return saltedPassword; } /// <summary> /// エンコードされたパスワードを返す /// </summary> /// <param name="password">エンコード前パスワード</param> /// <param name="passwordSalt">Base64形式にエンコードされたサルト</param> /// <param name="passwordFormat">パスワードフォーマット</param> /// <returns></returns> private string GetEncodedPassword(string password, string passwordSalt, MembershipPasswordFormat passwordFormat) { string encodedPassword; byte[] buff; byte[] saltedPassword; switch (passwordFormat) { case MembershipPasswordFormat.Clear: encodedPassword = password; break; case MembershipPasswordFormat.Encrypted: saltedPassword = GetSaltedPassword(password, passwordSalt); buff = EncryptPassword(saltedPassword); encodedPassword = Convert.ToBase64String(buff); break; case MembershipPasswordFormat.Hashed: saltedPassword = GetSaltedPassword(password, passwordSalt); HashAlgorithm hashAlgorithm = HashAlgorithm.Create(Membership.HashAlgorithmType); buff = hashAlgorithm.ComputeHash(saltedPassword); encodedPassword = Convert.ToBase64String(buff); break; default: throw new ProviderException("サポートされないパスワードの形式です"); } return encodedPassword; } } }
実装の戦略としては、ユーザにユーザ情報の変更や、ユーザ追加、削除などのあらゆるアクティビティが発生するたびに、更新内容をファイルに保存するようにしています。また、アプリケーション外からユーザ情報のファイルが更新された場合に最新の情報を再取得するようにキャッシュを使用しています。この機能を有効にするにはWeb.configのXmlMembershipProvider2の設定で、reloadFileIfModifiedにtrueを設定してやるようにします。
排他制御に関してはあまり意識していないです。変更が発生するメソッド呼び出しがある場合はロックを取得するようにしたほうがよいと思います。
2. テストプログラムの作成
ソリューションを右クリック→Add→New Web Siteで新しくWebサイトプロジェクトを起動し、CustomProviderプロジェクトを参照に追加します。
2.1 Web.configの編集
Web.configのメンバシッププロバイダの設定箇所を例として次のように記載します。
<system.web> <!-- 省略 --> <membership defaultProvider="XmlMembershipProvider"> <providers> <add name="XmlMembershipProvider" type="CustomProvider.XmlMembershipProvider2,CustomProvider" memberFile="~/App_Data/User.xml" applicationName="/" passwordFormat="Hashed" enablePasswordRetrieval="false" reloadFileIfModified="false"/> </providers> </membership> </system.web>
2.2 Default.aspxの編集
既定で作成されるDefault.aspx.csを次のように編集します。Default.aspxにはDetailsViewが貼り付けてあります。
public partial class _Default : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { Membership.CreateUser("test3", "g00gle@@"); Membership.DeleteUser("test3"); Response.Write(Membership.ValidateUser("test2", "g00gle@@")); DetailsView1.DataSource = Membership.GetAllUsers(); DetailsView1.DataBind(); } }
デバッグ実行して動作することを確認します。
説明は以上です。ある程度動かして確認はしましたが、誤り等があればご指摘ください。
さんのコメント: さんのコメント: