Проектирование пользовательского поставщика
120ASP.NET --- Безопасность в ASP.NET --- Проектирование пользовательского поставщика
Исходный код доступа к хранилищу данных - UserAndRole
Теперь рассмотрим, как реализовать собственного поставщика для служб членства и ролей. Создание пользовательского поставщика предусматривает выполнение следующих шагов:
Проектирование и создание лежащего в основе хранилища данных. (Рассматривается в этой статье.)
Создание служебных классов для доступа к хранилищу. (Рассматривается в этой статье.)
Создание класса-наследника MembershipProvider. (Рассматривается в статье «Реализация поставщика Membership API».)
Создание класса-наследника RoleProvider. (Рассматривается в статье «Реализация поставщика Roles API».)
Создание тестового приложения для испытания поставщиков. (Рассматривается в статье «Использование классов пользовательских поставщиков».)
Конфигурирование пользовательских поставщиков в тестовом приложении.
Использование новых пользовательских поставщиков в рабочем приложении.
Реализация пользовательских поставщиков достаточно прямолинейна, но требует некоторого времени, т.к. требуется реализовать множество методов и свойств. В этой и последующих статьях будет создан пользовательский поставщик членства и ролей, работающий с XML-файлом и лежащим в основе источником данных. Файлы XML - не слишком хорошее решение для масштабируемых приложений, но может оказаться неплохой альтернативой, если разрабатывается простое приложение, которое должно размещаться на сайте поставщика, не имеющего доступа к базе данных SQL Server.
Обзор проектного решения пользовательского поставщика
Перед созданием пользовательского поставщика следует продумать общую структуру всего решения. Цель состоит в том, чтобы сохранить лежащую в основе функциональность насколько возможно простой. Это позволит сосредоточиться на действительной реализации поставщиков членства и ролей. В случае XML простейший способ загружать и сохранять данные в файлах XML - это XML-сериализация. Она дает возможность сохранять полный граф объекта в файле одним вызовом функции и читать его также одним вызовом другой функции.
Помните, что в момент создания экземпляра сериализатора XmlSerializer ему необходимо сообщить тип, который требуется сериализовать и десериализовать. Перед использованием классов XmlTextReader, XmlTextWriter и XmlSerializer также не забудьте импортировать в коде пространства имен System.Xml и System.Xml.Serialization.
Поскольку такие классы, как MembershipUser, не предоставляют доступа к некоторой информации - например, к паролю - применять их с XML-сериализацией непосредственно нельзя. XML-сериализация требует, чтобы все сохраняемые свойства и члены были общедоступными. Таким образом, потребуется создать собственные представления пользователей и ролей в виде служебных классов для хранилища заднего плана. Эти классы никогда не будут передаваться в приложение, которое просто полагается на существующие классы членства. (Будет предусмотрена некоторая простая логика отображения между внутренним представлением пользователя и классом MembershipUser.)
На рисунке ниже показано проектное решение для пользовательского поставщика:
Как уже упоминалось, классы SimpleUser и SimpleRole обеспечивают возможность XML-сериализации. Хотя это требует определенной логики отображения для поддержки MembershipUser, вся реализация значительно облегчается. UserStore и RoleStore - служебные классы для инкапсуляции доступа к XML-файлу. Эти классы включают функции для загрузки и сохранения XML-файлов вместе с некоторыми базовыми служебными функциями поиска информации в хранилище.
И, наконец, модель включает классы XmlMembershipProvider и XmlRoleProvider. Класс XmlMembershipProvider наследует базовую функциональность от MembershipProvider, в то время как XmlRoleProvider - от RoleProvider. Оба базовых класса определены в пространстве имен System.Web.Security.
Проектирование и реализация пользовательского хранилища
После того как построена общая архитектура, можно приступать к лежащему в основе хранилищу данных. В данном примере хранилище данных будет состоять из XML-файла для хранения пользователей и XML-файла для хранения ролей. Чтобы максимально облегчить доступ к этим файлам, в качестве первичного механизма чтения и записи этих файлов применяется XML-сериализация. Таким образом, понадобятся некоторые классы для представления данных, сохраняемых в XML-файлах, либо в виде общедоступных полей, либо свойства, как показано ниже:
using System;
using System.Collections.Specialized;
namespace Professorweb.Providers.Store
{
public class SimpleUser
{
public Guid UserKey = Guid.Empty;
public string UserName = "";
public string Password = "";
public string PasswordSalt = "";
public string Email = "";
public DateTime CreationDate = DateTime.Now;
public DateTime LastActivityDate = DateTime.MinValue;
public DateTime LastLoginDate = DateTime.MinValue;
public DateTime LastPasswordChangeDate = DateTime.MinValue;
public string PasswordQuestion = "";
public string PasswordAnswer = "";
public string Comment;
}
public class SimpleRole
{
public string RoleName = "";
public StringCollection AssignedUsers = new StringCollection();
}
}
В данном примере в качестве ProviderUserKey используется GUID-идентификатор для уникальной идентификации пользователей в хранилище, подобно первичному ключу, применяемому для уникальной идентификации записей в таблице базы данных. Для каждого пользователя будет сохраняться имя, пароль (хешированный), адрес электронной почты, некоторая информация, связанная с датами, контрольный вопрос вместе с ответом, а также комментарии.
Что касается ролей, то будет храниться имя и ассоциации к пользователям. Для простоты каждая роль будет хранить массив имен пользователей (в виде строк), ассоциированных с ролью. Сериализированная версия массива пользователей будет хранилищем пользователей, в то время как сериализированная версия массива ролей будет хранилищем ролей, как показано на рисунке ниже:
Обратите внимание, что на этом рисунке показаны сериализированные версии пользователей и ролей из окончательной версии разрабатываемого поставщика. Как вы узнаете далее, используются пароли со случайным хешем. Более того, может возникнуть вопрос о том, почему в файл XML не сериализируются комментарии? Дело в том, что XmlSerializer сериализирует поля только в том случае, если их значение отлично от null (за исключением случаев, когда указано обратное через атрибуты XmlSerializer, примененные к свойствам класса).
Другой аспект проектного решения, который должен быть обдуман - способ обращения к хранилищу. Для каждого хранилища необходим только один экземпляр в памяти (UserStore и RoleStore), чтобы сэкономить ресурсы и избежать слишком частой загрузки XML-файлов. Это можно реализовать с помощью шаблона Singleton (Одиночка) - решения, гарантирующего, что внутри процесса существует только один экземпляр класса. Это обеспечивает приватный конструктор и общедоступный статический метод для получения экземпляра.
Общедоступный метод проверяет, не существует ли уже экземпляр, и если нет, автоматически создает его и возвращает. Может возникнуть вопрос, почему бы просто не использовать статический класс с множеством статических методов и членов. Если вы взглянете на следующий фрагмент кода, то поймете, что информация хранилища сохраняется в памяти, и именно поэтому можно поддерживать одновременно открытыми больше изолированных хранилищ без постоянной перезагрузки XML-файлов.
Логика инкапсулирована в шаблоне Singleton, который реализован классами хранилища, что видно в следующем коде. Если посмотреть на следующий фрагмент кода, станет ясно, что для каждого хранилища XML-файла, например, множества пользователей, в памяти удерживается один экземпляр UserStore, который инкапсулирует доступ к одному хранилищу в изолированной манере (та же логика применена в RoleStore для хранения ролей). Хотя это средство в поставщике не используется, упомянутый шаблон может оказаться интересным для других сценариев. Рассмотрим все эти аспекты на основе класса UserStore:
using System;
using System.Collections.Specialized;
using System.Text;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
using System.Web;
using System.Web.Security;
using System.Collections.Generic;
namespace Professorweb.Providers.Store
{
public class UserStore
{
private string _FileName;
private List<SimpleUser> _Users;
private XmlSerializer _Serializer;
private static Dictionary<string, UserStore> _RegisteredStores;
private UserStore(string fileName)
{
_FileName = fileName;
_Users = new List<SimpleUser>();
_Serializer = new XmlSerializer(typeof(List<SimpleUser>));
LoadStore(_FileName);
}
public static UserStore GetStore(string fileName)
{
// Создать зарегистрированное хранилище, если оно еще не существует
if (_RegisteredStores == null)
_RegisteredStores = new Dictionary<string, UserStore>();
if (!_RegisteredStores.ContainsKey(fileName))
{
_RegisteredStores.Add(fileName, new UserStore(fileName));
}
// Вернуть соответствующее хранилище no переданному имени файла
return _RegisteredStores[fileName];
}
// ...
}
}
Класс содержит несколько приватных членов - для имени файла хранилища, списка пользователей и экземпляра XmlSerializer, используемого для чтения и записи файлов.
Поскольку конструктор приватный, экземпляры не могут создаваться за пределами класса. Внешние классы могут получать экземпляры только через вызов общедоступного статического метода GetStore().
Реализация шаблона Singieton в данном случае особенна. Она создает одиночные экземпляры на основе имен файлов. Для каждого файла, обрабатываемого поставщиком, создается по одному экземпляру класса UserStore. Если в одном и том же процессе более одного веб-приложения использует этого поставщика, необходимо обеспечить, чтобы разные экземпляры создавались для разных имен файлов. Таким образом, класс не управляет одной статической переменной для единственного экземпляра; вместо этого он включает словарь, содержащий все экземпляры класса, по одному для каждого имени файла.
Поскольку для записи и загрузки данных из хранилища применяется XML-сериализация, функции загрузки и сохранения достаточно просты:
public class UserStore
{
// ...
private void LoadStore(string fileName)
{
try
{
// Обратите внимание, что если файл не существует, в этот момент он игнорируется.
// При операции записи файл хранилища создается автоматически
// реализацией хранилища
if (System.IO.File.Exists(fileName))
{
using (XmlTextReader reader = new XmlTextReader(fileName))
{
_Users = (List<SimpleUser>)_Serializer.Deserialize(reader);
}
}
}
catch (Exception ex)
{
throw new Exception(
string.Format("Не удалось загрузить файл {0}", fileName), ex);
}
}
private void SaveStore(string fileName)
{
try
{
if (System.IO.File.Exists(fileName))
System.IO.File.Delete(fileName);
using (XmlTextWriter writer = new XmlTextWriter(fileName, Encoding.UTF8))
{
_Serializer.Serialize(writer, _Users);
}
}
catch (Exception ex)
{
throw new Exception(
string.Format("Не удалось сохранить файл {0}", fileName), ex);
}
}
// ...
}
Обе функции являются приватными, поскольку вызываются только внутри самого класса. Метод LoadStore() вызывается в конструкторе класса UserStore. В этом методе инициализируется приватная переменная Users. Каждый последующий запрос обращается к коллекции _Users класса хранилища. С другой стороны, метод SaveStore() просто сериализирует коллекцию Users в файл, указанный в приватной переменной-члене _FileName, которая передается через конструктор (и непрямо - через статический метод GetStore()).
И, наконец, класс поддерживает несколько методов для опроса информации из коллекции Users:
public class UserStore
{
// ...
public List<SimpleUser> Users
{
get { return _Users; }
}
public void Save()
{
SaveStore(_FileName);
}
public SimpleUser GetUserByName(string name)
{
return _Users.Find(
user => string.Equals(name, user));
}
public SimpleUser GetUserByEmail(string email)
{
return _Users.Find(
user => string.Equals(email, user.Email));
}
public SimpleUser GetUserByKey(Guid key)
{
return _Users.Find(
user => user.UserKey.CompareTo(key) == 0);
}
}
Свойство Users - это простое свойство, которое позволяет действительному поставщику (XmlMembershipProvider) иметь доступ к пользователям хранилища. После того как реализация поставщика изменяет что-то внутри хранилища (например, изменит свойства пользователя), она вызывает общедоступный метод Save(), который внутри вызывает SaveStore() для сериализации информации обратно в файл, указанный в приватной переменной FileName данного экземпляра.
Остальные методы предназначены для поиска пользователей на основе различных критериев. Для этой цели обобщенный List<> включает метод Find. Метод Find принимает ссылку на другой метод, который вызывается для сравнения каждого элемента в процессе итерации по списку. Если функция сравнения возвращает для элемента true, такой элемент включается в результаты.
В коде метода GetUserByKey() передается делегат (представляющий собой ссылку на функцию), который сравнивает внутренний ключ SimpleUser с переданным ему ключом. Если сравнение дает true, текущий пользователь, переданный как параметр из List<>, возвращается в результате; в противном случае продолжается итерация по элементам List<>. В данном случае мы используем лямбда-выражения C# для передачи делегата.
UserStore содержит реализацию для сохранения только информации пользователя. Роли не включаются. Для этой цели понадобится реализовать класс RoleStore (подобный классу UserStore), как показано ниже:
public class RoleStore
{
XmlSerializer _Serializer;
private string _FileName;
List<SimpleRole> _Roles;
private static Dictionary<string, RoleStore> _RegisteredStores;
public static RoleStore GetStore(string fileName)
{
// Создать зарегистрированные хранилища
if (_RegisteredStores == null)
_RegisteredStores = new Dictionary<string, RoleStore>();
// Вернуть соответствующее хранилище
if (!_RegisteredStores.ContainsKey(fileName))
{
_RegisteredStores.Add(fileName, new RoleStore(fileName));
}
return _RegisteredStores[fileName];
}
private RoleStore(string fileName)
{
_Roles = new List<SimpleRole>();
_FileName = fileName;
_Serializer = new XmlSerializer(typeof(List<SimpleRole>));
LoadStore(_FileName);
}
private void LoadStore(string fileName)
{
try
{
// Хранилище создается автоматически при его сохранении
if (System.IO.File.Exists(fileName))
{
using (XmlTextReader reader = new XmlTextReader(fileName))
{
_Roles = (List<SimpleRole>)_Serializer.Deserialize(reader);
}
}
}
catch (Exception ex)
{
throw new Exception(string.Format(
"Не удалось загрузить файл {0}", fileName), ex);
}
}
private void SaveStore(string fileName)
{
try
{
if (System.IO.File.Exists(fileName))
System.IO.File.Delete(fileName);
using (XmlTextWriter writer = new XmlTextWriter(fileName, Encoding.UTF8))
{
_Serializer.Serialize(writer, _Roles);
}
}
catch (Exception ex)
{
throw new Exception(string.Format(
"Не удалось сохранить файл {0}", fileName), ex);
}
}
public List<SimpleRole> Roles
{
get { return _Roles; }
}
public void Save()
{
SaveStore(_FileName);
}
public List<SimpleRole> GetRolesForUser(string userName)
{
List<SimpleRole> Results = new List<SimpleRole>();
foreach (SimpleRole r in Roles)
{
if (r.AssignedUsers.Contains(userName))
Results.Add(r);
}
return Results;
}
public string[] GetUsersInRole(string roleName)
{
SimpleRole Role = GetRole(roleName);
if (Role != null)
{
string[] Results = new string[Role.AssignedUsers.Count];
Role.AssignedUsers.CopyTo(Results, 0);
return Results;
}
else
{
throw new Exception(string.Format("Роль с именем \"{0}\" не существует!", roleName));
}
}
public SimpleRole GetRole(string roleName)
{
return Roles.Find(
role => role.RoleName.Equals(
roleName, StringComparison.OrdinalIgnoreCase));
}
}
Эта реализация выглядит очень похоже на UserStore. Главное отличие состоит в том, что в ней используется класс SimpleRole вместо SimpleUser, а класс XmlSerializer инициализируется другим типом. Кроме того, отличается функция запроса к хранилищу. В то время как UserStore реализует функции нахождения пользователей по адресам электронной почты, уникальным идентификаторам либо именам, класс хранилища находит роли по именам, позволяя вернуть пользовательские роли, и опрашивает все роли для одного выделенного пользователя.
Обратите внимание, что в предыдущем методе GetRole() имена ролей сравнивались с помощью метода Equals экземпляра строки с передачей ему в качестве параметра StringComparison.OrdinalIgnoreCase. Это значит, что сравнение имен ролей производилось без учета регистра. Поэтому, если название роли будет передано с буквами разного регистра, она все равно будет найдена.
Теперь классы для доступа к хранилищам готовы, а это значит, что можно приступать к реализации классов пользовательских поставщиков.