自前のDBでユーザ管理を行うため、ユーザの認証管理を行うメンバシッププロバイダを自作してみました。
手順としてはこんな感じです。
- MembershipProviderを継承したクラスを作成
- Web.configで作成したクラスの呼び出し設定
また、以下のマイクロソフトのサイトを参考にしています。
メンバシッププロバイダクラスは以下のような内容です。
今回は単純なユーザ名、パスワード、ユーザID(自動采番)を持ったACCOUNTテーブルで管理する想定で作ってみました。
using System; using System.Configuration; using System.Web.Security; using System.Collections.Specialized; using System.Collections.Generic; using System.Configuration.Provider; using System.Text; using System.Security.Cryptography; using System.Web.Configuration; using System.Linq; namespace MyWebApp.Models { public class MyWebAppMembershipProvider : MembershipProvider { MyWebAppEntities _db = new MyWebAppEntities(); private string connectionString; private Dictionary<string, MembershipUser> _Users; private MachineKeySection machineKey; private MembershipPasswordFormat pPasswordFormat; private int pMinRequiredPasswordLength; public override string ApplicationName { get{ throw new Exception("対象のメソッドは実装していません。"); } set{ throw new Exception("対象のメソッドは実装していません。"); } } // Initialize実装 public override void Initialize(string name, NameValueCollection config) { if (config == null) throw new ArgumentNullException("config"); if (name == null || name.Length == 0) name = "MyWebAppMembershipProvider"; if (String.IsNullOrEmpty(config["description"])) { config.Remove("description"); config.Add("description", "MyWebApp Membership Provider"); } // ベースクラス初期化 base.Initialize(name, config); // パスワード最小文字数設定 pMinRequiredPasswordLength = Convert.ToInt32(GetConfigValue(config["minRequiredPasswordLength"], "6")); // パスワードフォーマット設定 string temp_format = config["passwordFormat"]; if (temp_format == null) { temp_format = "Hashed"; } switch (temp_format) { case "Hashed": pPasswordFormat = MembershipPasswordFormat.Hashed; break; case "Encrypted": pPasswordFormat = MembershipPasswordFormat.Encrypted; break; case "Clear": pPasswordFormat = MembershipPasswordFormat.Clear; break; default: throw new ProviderException("パスワード形式をサポートしていません。"); } // 接続文字列設定 ConnectionStringSettings ConnectionStringSettings = ConfigurationManager.ConnectionStrings[config["connectionStringName"]]; if (ConnectionStringSettings == null || ConnectionStringSettings.ConnectionString.Trim() == "") { throw new ProviderException("接続文字列が存在しません。"); } connectionString = ConnectionStringSettings.ConnectionString; // 暗号化キー設定 Configuration cfg = WebConfigurationManager.OpenWebConfiguration(System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath); machineKey = (MachineKeySection)cfg.GetSection("system.web/machineKey"); if (machineKey.ValidationKey.Contains("AutoGenerate")) if (PasswordFormat != MembershipPasswordFormat.Clear) throw new ProviderException("Hashed or Encrypted passwords " + "are not supported with auto-generated keys."); } public override bool ChangePassword(string username, string oldPassword, string newPassword) { throw new Exception("対象のメソッドは実装していません。"); } public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer) { throw new Exception("対象のメソッドは実装していません。"); } // CreateUser実装 public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status) { if (String.IsNullOrEmpty(username)) { throw new ArgumentNullException("username"); } if (String.IsNullOrEmpty(password)) { throw new ArgumentNullException("password"); } // ユーザ情報取得 ReadMembershipDataStore(); // ユーザ作成 ACCOUNT ai = new ACCOUNT(); ai.USER_NAME = username; ai.PASSWORD = EncodePassword(password); _db.AddToACCOUNT(ai); _db.SaveChanges(); // ユーザ登録 status = MembershipCreateStatus.Success; MembershipUser user = new MembershipUser(Name, username, username, email, "", password, true, false, DateTime.Now, DateTime.Now, DateTime.Now, DateTime.Now, DateTime.MaxValue); _Users.Add(username, user); return user; } public override bool DeleteUser(string username, bool deleteAllRelatedData) { throw new Exception("対象のメソッドは実装していません。"); } public override bool EnablePasswordReset { get { return false; } } public override bool EnablePasswordRetrieval { get{ return false; } } public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords) { throw new Exception("対象のメソッドは実装していません。"); } public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords) { throw new Exception("対象のメソッドは実装していません。"); } public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords) { throw new Exception("対象のメソッドは実装していません。"); } public override int GetNumberOfUsersOnline() { throw new Exception("対象のメソッドは実装していません。"); } public override string GetPassword(string username, string answer) { throw new Exception("対象のメソッドは実装していません。"); } // GetUser実装 public override MembershipUser GetUser(string username, bool userIsOnline) { if (String.IsNullOrEmpty(username)) { return null; } // ユーザ情報取得 ReadMembershipDataStore(); MembershipUser user; if (_Users.TryGetValue(username, out user)) return user; return null; } public override MembershipUser GetUser(object providerUserKey, bool userIsOnline) { throw new Exception("対象のメソッドは実装していません。"); } public override string GetUserNameByEmail(string email) { throw new Exception("対象のメソッドは実装していません。"); } public override int MaxInvalidPasswordAttempts { get { return 5; } } public override int MinRequiredNonAlphanumericCharacters { get { return 0; } } public override int MinRequiredPasswordLength { get { return pMinRequiredPasswordLength; } } public override int PasswordAttemptWindow { get { throw new Exception("対象のメソッドは実装していません。"); } } public override MembershipPasswordFormat PasswordFormat { get { return pPasswordFormat; } } public override string PasswordStrengthRegularExpression { get { throw new Exception("対象のメソッドは実装していません。"); } } public override bool RequiresQuestionAndAnswer { get { return false; } } public override bool RequiresUniqueEmail { get { return false; } } public override string ResetPassword(string username, string answer) { throw new Exception("対象のメソッドは実装していません。"); } public override bool UnlockUser(string userName) { throw new Exception("対象のメソッドは実装していません。"); } public override void UpdateUser(MembershipUser user) { throw new Exception("対象のメソッドは実装していません。"); } // ValidateUser実装 public override bool ValidateUser(string username, string password) { if (String.IsNullOrEmpty(username) || String.IsNullOrEmpty(password)) { return false; } // ユーザ情報取得 ReadMembershipDataStore(); // ユーザ存在確認 MembershipUser user = GetUser(username, false); if (user != null) { ACCOUNT ai = (from a in _db.ACCOUNT where a.USER_NAME == username select a).First(); // パスワード判定 if (CheckPassword(password, ai.PASSWORD)) { return true; } } return false; } // メンバシップデータ取得 private void ReadMembershipDataStore() { lock (this) { if (_Users == null) { // ユーザ情報取得 _Users = new Dictionary<string, MembershipUser>(); IQueryable<ACCOUNT> aiList = from a in _db.ACCOUNT select a; foreach (ACCOUNT ai in aiList) { MembershipUser user = new MembershipUser( Name, // Provider name ai.USER_NAME, // Username ai.USER_ID, // providerUserKey String.Empty, // Email String.Empty, // passwordQuestion String.Empty, // Comment true, // isApproved false, // isLockedOut new DateTime(0), // creationDate new DateTime(0), // lastLoginDate new DateTime(0), // lastActivityDate new DateTime(0), // lastPasswordChangedDate new DateTime(0) // lastLockoutDate ); _Users.Add(user.UserName, user); } } } } // パスワードチェック private bool CheckPassword(string password, string dbpassword) { string pass1 = password; string pass2 = dbpassword; switch (PasswordFormat) { case MembershipPasswordFormat.Encrypted: pass2 = UnEncodePassword(dbpassword); break; case MembershipPasswordFormat.Hashed: pass1 = EncodePassword(password); break; default: break; } if (pass1 == pass2) { return true; } return false; } // パスワード暗号化 private string EncodePassword(string password) { string encodedPassword = password; switch (PasswordFormat) { case MembershipPasswordFormat.Clear: break; case MembershipPasswordFormat.Encrypted: encodedPassword = Convert.ToBase64String(EncryptPassword(Encoding.Unicode.GetBytes(password))); break; case MembershipPasswordFormat.Hashed: HMACSHA1 hash = new HMACSHA1(); hash.Key = HexToByte(machineKey.ValidationKey); encodedPassword = Convert.ToBase64String(hash.ComputeHash(Encoding.Unicode.GetBytes(password))); break; default: throw new ProviderException("パスワード形式がサポートされていません。"); } return encodedPassword; } // パスワード復号化 private string UnEncodePassword(string encodedPassword) { string password = encodedPassword; switch (PasswordFormat) { case MembershipPasswordFormat.Clear: break; case MembershipPasswordFormat.Encrypted: password = Encoding.Unicode.GetString(DecryptPassword(Convert.FromBase64String(password))); break; case MembershipPasswordFormat.Hashed: throw new ProviderException("パスワード形式がHashedの場合、複合化は出来ません。"); default: throw new ProviderException("パスワード形式がサポートされていません。"); } return password; } private byte[] HexToByte(string hexString) { byte[] returnBytes = new byte[hexString.Length / 2]; for (int i = 0; i < returnBytes.Length; i++) returnBytes[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16); return returnBytes; } private string GetConfigValue(string configValue, string defaultValue) { if (String.IsNullOrEmpty(configValue)) return defaultValue; return configValue; } } }
必要なメソッドのみ実装し、未実装のものは例外を投げています。
実装したメソッドは以下の通りです。
- Initialize・・・プロバイダクラス初期化
- CreateUser・・・ユーザ作成
- GetUser・・・ユーザ取得
- ValidateUser・・・ユーザ名とパスワード有効チェック
ChangePassword、DeleteUser等は必要に応じて実装していく予定です。
続いて、Web.configです。
<machineKey validationKey="C50B3C89CB21F4F1422FF158A5B42D0E8DB8CB5CDA1742572A487D9401E3400267682B202B746511891C1BAF47F8D25C07F6C39A104696DB51F17C529AD3CABE" decryptionKey="8A9BE8FD67AF6979E7D20198CFEA50DD3D3799C77AF2B72F" validation="SHA1" /> <membership> <providers> <clear /> <add name="AspNetSqlMembershipProvider" type="MyWebApp.ModelsModels.MyWebAppMembershipProvider" connectionStringName="ApplicationServices" passwordFormat="Hashed" minRequiredPasswordLength="6" applicationName="/" /> </providers> </membership>
machineKeyは暗号化キーとして使うために必要なものです。
membership内では自作クラスの呼び出しを設定しています。
ついでに権限の管理を行うカスタムロールプロバイダも作成しました。
たいした内容ではないのですが、時間があればまとめたいと思います。