Реализация поставщика Membership API

180

Исходный код пользовательского поставщика - XmlMembershipProvider

В этой статье создается класс XmlMembershipProvider, который будет играть роль адаптера между пользовательским хранилищем и требованиями Membership API. Здесь описаны наиболее важные стадии создания поставщика членства.

Каждый пользовательский поставщик членства должен быть унаследован от System.Web.Security.MembershipProvider, как показано ниже:

public class XmlMembershipProvider : MembershipProvider
{
    // ...
}

При наследовании от MembershipProvider потребуется реализовать множество свойств и методов, чтобы удовлетворить требованиям Membership API. Эти свойства и методы используются для опроса, создания, обновления и удаления пользователей, а также для извлечения специфической информации о поставщике, такой как требования к паролям. Такие типы свойств запрашиваются элементами управления безопасностью. (Например, свойство RequirePasswordQuestionAndAnswer запрашивается CreateUserWizard для принятия решения о том, где отображать текстовые поля для ввода контрольных вопросов и ответов для паролей.)

Начать следует с реализации свойств поставщика, поскольку это - простейшая часть всей задачи. Для каждого свойства нужно предусмотреть одну приватную переменную, которая будет содержать состояние соответствующего свойства:

using System;
using System.IO;
using System.Web;
using System.Web.Security;
using System.Configuration.Provider;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Security.Permissions;
using System.Security.Cryptography;
using Professorweb.Providers.Store;

namespace Professorweb.Providers
{
    public class XmlMembershipProvider : MembershipProvider
    {
        private string _Name;
        private string _FileName;
        private UserStore _CurrentStore = null;
        private string _ApplicationName;
        private bool _EnablePasswordReset;
        private bool _RequiresQuestionAndAnswer;
        private string _PasswordStrengthRegEx;
        private int _MaxInvalidPasswordAttempts;
        private int _MinRequiredNonAlphanumericChars;
        private int _MinRequiredPasswordLength;
        private MembershipPasswordFormat _PasswordFormat;
        
        public override string ApplicationName
        {
            get
            {
                return _ApplicationName;
            }
            set
            {
                _ApplicationName = value;
                _CurrentStore = null;
            }
        }

        public override bool EnablePasswordReset
        {
            get { return _EnablePasswordReset; }
        }

        public override bool EnablePasswordRetrieval
        {
            get
            {
                if (this.PasswordFormat == MembershipPasswordFormat.Hashed)
                    return false;
                else
                    return true;
            }
        }

        public override int MaxInvalidPasswordAttempts
        {
            get { return _MaxInvalidPasswordAttempts; }
        }

        public override int MinRequiredNonAlphanumericCharacters
        {
            get { return _MinRequiredNonAlphanumericChars; }
        }

        public override int MinRequiredPasswordLength
        {
            get { return _MinRequiredPasswordLength; }
        }

        public override int PasswordAttemptWindow
        {
            get { return 20; }
        }

        public override MembershipPasswordFormat PasswordFormat
        {
            get { return _PasswordFormat; }
        }

        public override string PasswordStrengthRegularExpression
        {
            get
            {
                return _PasswordStrengthRegEx;
            }
        }

        public override bool RequiresQuestionAndAnswer
        {
            get { return _RequiresQuestionAndAnswer; }
        }

        public override bool RequiresUniqueEmail
        {
            get { return true; }
        }
        // ...    
    }
}

Здесь описаны все свойства поставщиков, и они имеют то же значение, что и в лежащей в основе реализации. Многие из них включают только средство доступа get, но не имеют средства доступа set. Но каким образом инфраструктура ASP.NET инициализирует эти свойства значениями, сконфигурированными в web.config?

Ответ можно найти в исходном базовом классе всех поставщиков - ProviderBase. Класс ProviderBase, в свою очередь, является базовым для MembershipProvider, а потому все классы, унаследованные от MembershipProvider, неявно унаследованы и от ProviderBase и обладают базовыми свойствами ProviderBase. Все, что потребуется сделать - это переопределить метод Initialize. Этот метод принимает два параметра: имя (которое конфигурируется атрибутом Name в web.config) и NameValueCollection (которое содержит ключи и их соответствующие значения для всех настроек, сконфигурированных через web.config). Внутри этого метода можно инициализировать приватные члены свойств, показанных ранее.

Рассмотрим содержимое этого метода для XmlMembershipProvider:

public override void Initialize(string name, System.Collections.Specialized.NameValueCollection 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", "XML Membership Provider");
    }

    // Инициализировать базовый класс
    base.Initialize(name, config);

    // Инициализировать значения по умолчанию
    _ApplicationName = "DefaultApp";
    _EnablePasswordReset = false;
    _PasswordStrengthRegEx = @"[\w| !§$%&/()=\-?\*]*";
    _MaxInvalidPasswordAttempts = 3;
    _MinRequiredNonAlphanumericChars = 1;
    _MinRequiredPasswordLength = 5;
    _RequiresQuestionAndAnswer = false;
    _PasswordFormat = MembershipPasswordFormat.Hashed;

    // Пройти по свойствам и инициализировать сконфигурированные значения
    foreach (string key in config.Keys)
    {
        switch (key.ToLower())
        {
            case "name":
                    _Name = config[key];
                    break;
            case "applicationname":
                    _ApplicationName = config[key];
                    break;
            case "filename":
                    _FileName = config[key];
                    break;
            case "enablepasswordreset":
                   _EnablePasswordReset = bool.Parse(config[key]);
                    break;
            case "passwordstrengthregex":
                    _PasswordStrengthRegEx = config[key];
                    break;
            case "maxinvalidpasswordattempts":
                    _MaxInvalidPasswordAttempts = int.Parse(config[key]);
                    break;
            case "minrequirednonalphanumericchars":
                    _MinRequiredNonAlphanumericChars = int.Parse(config[key]);
                    break;
            case "minrequiredpasswordlength":
                    _MinRequiredPasswordLength = int.Parse(config[key]);
                    break;
            case "passwordformat":
                    _PasswordFormat = (MembershipPasswordFormat)Enum.Parse(
                        typeof(MembershipPasswordFormat), config[key]);
                    break;
            case "requiresquestionandanswer":
                    _RequiresQuestionAndAnswer = bool.Parse(config[key]);
                    break;
        }
    }
}

Во-первых, производится проверка, передана ли вообще какая-то конфигурация. Если для поставщика ничего не сконфигурировано, работать он не будет. Во-вторых, если никакого имени не указано, необходимо инициализировать имя по умолчанию, которое требуется инструменту конфигурации для отображения поставщика в своем списке. И, если никакого описания поставщика не сконфигурировано, понадобится добавить описание по умолчанию. Этот заключительный шаг не обязателен, но полезен для инструмента конфигурирования, который запрашивает информацию о поставщике.

Не забудьте вызвать реализацию инициализатора базового класса для корректной инициализации базовых свойств.

После этого мы инициализируем некоторые опции значениями по умолчанию на случай, если они не включены в конфигурационный файл web.config. После инициализации этих значений по умолчанию можно пройти по элементам параметра конфигурации, переданного в метод (он представляет собой просто NameValueCollection). Как видите, можно даже включить пользовательские настройки, такие как имя файла, которые не входят в набор свойств по умолчанию поставщика членства. Свойство имени файла является пользовательским свойством для специфичного поставщика и указывает на XML-файл, содержащий информацию о пользователях. Это имя файла передается классу UserStore в отдельном свойстве, которое будет применяться в остальных функциях реализации.

Кроме того, в поставщике имеется большое количество методов. Эти методы предназначены для создания, обновления и удаления пользователей, а также для доступа и извлечения подробностей о пользователях. Эти методы обращаются к информации через ранее созданные классы хранилища. Эти методы перечислены в следующем фрагменте кода в качестве обзора того, что понадобится реализовать в последующих разделах:

public override MembershipUser CreateUser(string username, string password,
    string email, string passwordQuestion,
    string passwordAnswer, bool isApproved,
    object providerUserKey, out MembershipCreateStatus status)
{    /* ... */    }

public override bool DeleteUser(string username, bool deleteAllRelatedData)
{    /* ... */    }

public override MembershipUser GetUser(string username, bool userIsOnline)
{    /* ... */    }

public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
{    /* ... */    }

public override string GetUserNameByEmail(string email)
{    /* ... */    }

public override void UpdateUser(MembershipUser user)
{    /* ... */    }

public override bool ValidateUser(string username, string password)
{    /* ... */    }

public override bool ChangePassword(string username, 
    string oldPassword, string newPassword)
{    /* ... */    }

public override bool ChangePasswordQuestionAndAnswer(string username, 
    string password, string newPasswordQuestion, string newPasswordAnswer)
{    /* ... */    }

public override MembershipUserCollection FindUsersByEmail(string emailToMatch, 
    int pageIndex, int pageSize, out int totalRecords)
{    /* ... */    }

public override MembershipUserCollection FindUsersByName(string usernameToMatch, 
    int pageIndex, int pageSize, out int totalRecords)
{    /* ... */    }

public override MembershipUserCollection GetAllUsers(int pageIndex, 
    int pageSize, out int totalRecords)
{    /* ... */    }

public override int GetNumberOfUsersOnline()
{    /* ... */    }

public override string GetPassword(string username, string answer)
{    /* ... */    }

public override string ResetPassword(string username, string answer)
{    /* ... */    }

public override bool UnlockUser(string userName)
{    /* ... */    }

Внутри этих методов просто должны вызываться соответствующие методы класса UserStore через ранее представленное свойство CurrentStore. Это и все методы, определенные в поставщике. Любой дополнительный метод, рассмотренный далее, является вспомогательным. Далее будут продемонстрированы наиболее важные реализации этих методов, хотя и не все. Ссылка на полный код находится в начале страницы.

Создание пользователей и добавление их в хранилище

Метод CreateUser() интересен тем, что он должен обеспечить уникальность имени пользователя и адреса электронной почты, а также корректность пароля и соответствие его требованиям надежности.

public override MembershipUser CreateUser(string username, string password,
    string email, string passwordQuestion,
    string passwordAnswer, bool isApproved,
    object providerUserKey, out MembershipCreateStatus status)
{
    try
    {
        // Проверить достоверность имени пользователя и адреса электронной почты
        if (!ValidateUsername(username, email, Guid.Empty))
        {
            // Если имя пользователя неверно, потому что уже существует, 
            // или адрес электронной почты дублируется, а поставщик 
            // сконфигурирован так, что не допускает дублированных адресов, 
            // вернуть состояние InvalidUserName через выходной параметр status
            status = MembershipCreateStatus.InvalidUserName;
            return null;
        }

        // Инициировать событие перед проверкой пароля. 
        // Это событие обрабатывается классом Membership API, 
        // который пересылает его всем подписчикам в пользовательском 
        // коде, позволяя писать код проверки достоверности 
        // форматов пароля без необходимости понимания внутреннего 
        // устройства реализации поставщика членства
        base.OnValidatingPassword(
            new ValidatePasswordEventArgs(
            username, password, true));

        // Проверить достоверность пароля
        if (!ValidatePassword(password))
        {
            status = MembershipCreateStatus.InvalidPassword;
            return null;
        }

        // Все является достоверным, поэтому создать пользователя
        SimpleUser user = new SimpleUser();
        user.UserKey = Guid.NewGuid();
        user.UserName = username;
        user.PasswordSalt = string.Empty;
        user.Password = this.TransformPassword(password, ref user.PasswordSalt);
        user.Email = email;
        user.PasswordQuestion = passwordQuestion;
        user.PasswordAnswer = passwordAnswer;
        user.CreationDate = DateTime.Now;
        user.LastActivityDate = DateTime.Now;
        user.LastPasswordChangeDate = DateTime.Now;

        // Добавить пользователя в хранилище
        CurrentStore.Users.Add(user);
        CurrentStore.Save();

        status = MembershipCreateStatus.Success;
        return CreateMembershipFromInternalUser(user);
    }
    catch
    {
        // Если при сохранении хранилища или при сериализации содержимого 
        // возникает исключение, просто передать его вызывающему коду. 
        // Было бы яснее, если бы работа здесь производилась со специальными 
        // классами исключений, с передачей вызывающему коду детализированной 
        // информации, но для простоты пусть все остается в таком виде
        throw;
    }
}

В первом разделе функция вызывает приватные методы ValidateUserName и ValidatePassword. Эти методы обеспечивают уникальность имени пользователя и адреса электронной почты в рамках хранилища, а также соответствие пароля требованиям устойчивости. После того как эти проверки успешно выполнены, можно создавать пользователя для лежащего в основе хранилища (SimpleUser), добавлять его в хранилище и затем сохранить это хранилище.

В конце метод должен вернуть экземпляр MembershipUser с подробностями о созданном пользователе вызывающему классу Membership. Для этой цели необходимо просто установить свойства MembershipUser в значения свойств экземпляра SimpleUser, как показано в следующей функции:

private MembershipUser CreateMembershipFromInternalUser(SimpleUser user)
{
    MembershipUser muser = new MembershipUser(base.Name,
        user.UserName, user.UserKey, user.Email, user.PasswordQuestion,
        string.Empty, true, false, user.CreationDate, user.LastLoginDate,
        user.LastActivityDate, user.LastPasswordChangeDate, DateTime.MaxValue);

    return muser;
}

Как видите, это отображение создает экземпляр MembershipUser и передает ему соответствующие свойства экземпляра SimpleUser в качестве параметров конструктора. Теперь взглянем на функции для проверки достоверности имени пользователя, адреса электронной почты и пароля:

private bool ValidatePassword(string password)
{
    bool IsValid = true;
    Regex HelpExpression;

    // Проверить простые свойства
    IsValid = IsValid && (password.Length >= this.MinRequiredPasswordLength);

    // Проверить по нецифровым символам
    HelpExpression = new Regex(@"\W");
    IsValid = IsValid && (HelpExpression.Matches(password).Count >= this.MinRequiredNonAlphanumericCharacters);

    // Проверить по регулярному выражению
    HelpExpression = new Regex(this.PasswordStrengthRegularExpression);
    IsValid = IsValid && (HelpExpression.Matches(password).Count > 0);

    return IsValid;
}

При проверке достоверности пароля первым делом верифицируется его длина. Если пароль слишком короткий, возвращается false. Затем через классы регулярных выражений .NET Framework проверяется количество не алфавитно-цифровых символов, чтобы оно было не меньше, чем MinRequireNonAlphanumericCharacters. После этого функция проверки достоверности пароля проверяет пароль через функции регулярных выражений .NET Framework из пространства имен System.Text.RegularExpressions на соответствие пароля регулярному выражению PasswordStrengthRegularExpression. Если все проверки прошли успешно, возвращается true, а в противном случае - false.

Теперь внимательней рассмотрим метод проверки достоверности имени и адреса электронной почты пользователя. Как имя, так и адрес должны быть уникальными в пределах хранилища:

private bool ValidateUsername(string userName, string email, Guid excludeKey)
{
    bool IsValid = true;

    UserStore store = UserStore.GetStore(_FileName);
    foreach (SimpleUser user in store.Users)
    {
        if (user.UserKey.CompareTo(excludeKey) != 0)
        {
            if (string.Equals(user.UserName, userName, StringComparison.OrdinalIgnoreCase))
            {
        IsValid = false;
        break;
            }

            if (string.Equals(user.Email, email, StringComparison.OrdinalIgnoreCase))
            {
        IsValid = false;
        break;
            }
        }
    }

    return IsValid;
}

Как видите, проверка пользователя довольно проста. Код проходит по всем пользователям в CurrentStore и проверяет, нет ли там уже пользователя с таким же именем или адресом электронной почты. Если таковой имеется, функция возвращает false, а в противном случае - true. Интересная часть метода CreateUser() - способ установки пароля пользователя. Благодаря свойству PasswordFormat, каждый поставщик располагает тремя типами хранения пароля: простой текст, хешированный и шифрованный. Как показано ниже, метод CreateUser() использует приватный вспомогательный метод класса XmlMembershipProvider по имени TransformPassword():

user.Password = this.TransformPassword(password, ref user.PasswordSalt);

Этот метод опрашивает текущее значение свойства PasswordFormat и с соответствие с ним оставляет пароль в виде простого текста, создает хеш пароля или же шифрует его:

private string TransformPassword(string password, ref string salt)
{
    string ret = string.Empty;

    switch (PasswordFormat)
    {
        case MembershipPasswordFormat.Clear:
            ret = password;
            break;

        case MembershipPasswordFormat.Hashed:

            if (string.IsNullOrEmpty(salt))
            {
                    byte[] saltBytes = new byte[16];
                    RandomNumberGenerator rng = RandomNumberGenerator.Create();
                    rng.GetBytes(saltBytes);
                    salt = Convert.ToBase64String(saltBytes);
            }
            ret = FormsAuthentication.HashPasswordForStoringInConfigFile(
                    (salt + password), "SHA1");
            break;

        case MembershipPasswordFormat.Encrypted:
            byte[] ClearText = Encoding.UTF8.GetBytes(password);
            byte[] EncryptedText = base.EncryptPassword(ClearText);
            ret = Convert.ToBase64String(EncryptedText);
            break;
    }

    return ret;
}

Если формат пароля установлен в Clear, метод просто возвращает пароль в виде простого текста. В случае установки формата в Hashed с помощью служебного метода аутентификации создается и возвращается простой хеш.

Вариант формата Encrypted обеспечивает сохранение в базе данных пароль, зашифрованный двусторонним алгоритмом шифрования, что позволяет впоследствии извлечь его оттуда через расшифровку. В этом случае для шифрования используется метод EncryptPassword из реализации базового класса. Для шифрования пароля этот метод использует ключ, сохраненный в machine.config. Если это применяется в среде веб-фермы, потребуется синхронизировать ключ, сохраненный в machine.config на каждой машине, чтобы пароль, зашифрованный на одной машине в веб-ферме, мог быть правильно расшифрован на другой машине.

Проверка достоверности пользователей при входе

Класс Membership поддерживает метод программной проверки достоверности паролей, введенных пользователем. Этот метод также используется элементом управления Login. Это значит, что при каждой попытке входа пользователя вызывается метод ValidateUser() класса Membership. И этот метод вызывает одноименный метод лежащего в основе поставщика членства.

В соответствии с установкой свойства PasswordFormat, он должен извлечь пользователя из хранилища по его имени, и затем каким-то образом проверить правильность пароля. Если пароль хранится в виде открытого текста, эта проверка представляет собой простое сравнение строк. Зашифрованные пароли должны быть расшифрованы и затем выполнено сравнение, в то время как сравнение хешированных паролей выполняется созданием хеша введенного значения пароля и последующим сравнением значения хешей:

public override bool ValidateUser(string username, string password)
{
    try
    {
        SimpleUser user = CurrentStore.GetUserByName(username);
        if (user == null)
            return false;

        if (ValidateUserInternal(user, password))
        {
            user.LastLoginDate = DateTime.Now;
            user.LastActivityDate = DateTime.Now;
            CurrentStore.Save();
            return true;
        }
        else
        {
            return false;
        }
    }
    catch
    {
        throw;
    }
}

Этот метод извлекает пользователя из хранилища. Затем выполняется проверка переданного пароля (который введен пользователем) по значению пароля из хранилища с помощью вспомогательного метода ValidateUserInternal().

И, наконец, если имя и пароль пользователя в порядке, обновляются значения LastLoginDate и LastActivityDate, после чего возвращается true. Всегда полезно инкапсулировать функциональность проверки пароля в отдельной функции, поскольку она может применяться в поставщике более одного раза. Типичный пример повторного использования этой функциональности - метод ChangePassword, где пользователь должен вводить старый и новый пароли. Если проверка старого пароля не проходит, поставщик не должен менять пароль, как показано ниже:

public override bool ChangePassword(string username, string oldPassword, string newPassword)
{
    try
    {
        // Получить пользователя из хранилища
        SimpleUser user = CurrentStore.GetUserByName(username);
        if (user == null)
            throw new Exception("Пользователя не существует!");

        if (ValidateUserInternal(user, oldPassword))
        {
            // Инициировать событие до проверки пароля
            base.OnValidatingPassword(
                new ValidatePasswordEventArgs(
                   username, newPassword, false));

            if (!ValidatePassword(newPassword))
                throw new ArgumentException("Пароль не отвечает требованиям надежности пароля!");

            user.PasswordSalt = string.Empty;
            user.Password = TransformPassword(newPassword, ref user.PasswordSalt);
            user.LastPasswordChangeDate = DateTime.Now;
            CurrentStore.Save();

            return true;
        }

        return false;
    }
    catch
    {
        throw;
    }
}

Изменение производится только при условии, что старый пароль введен правильно. Метод ChangePassword() при необходимости опять вызывает метод TransformPassword для генерации защищенной (хешированной, зашифрованной) версии пароля. Можно повторно использовать функцию, представленную ранее в методе CreateUser(). А теперь рассмотрим функциональность проверки достоверности пароля:

private bool ValidateUserInternal(SimpleUser user, string password)
{
    if (user != null)
    {
        string passwordValidate = TransformPassword(password, ref user.PasswordSalt);
        if (string.Compare(passwordValidate, user.Password) == 0)
        {
            return true;
        }
    }

    return false;
}

Этот метод использует метод TransformPassword() для создания защищенной версии пароля, если она необходима. Результат затем сравнивается с помощью обычного сравнения строк (даже зашифрованная версия возвращает закодированную посредством Base64 строку, которая будет сохранена в XML-файле; таким образом, сравнение строк подойдет и здесь). Этим, например, объясняется то, что проверка хешированных паролей вообще работает. Просто пересоздайте хеш и сравните хешированные версии пароля.

Остальные функции поставщика

Инициализация поставщика, создание и проверка пользователей - наиболее важные и трудные в реализации функции поставщика. Остальные его функции предназначены только для чтения информации из хранилища и обновления пользователей в хранилище. Эти функции вызывают лежащие в основе методы класса UserStore или пытаются найти пользователей в коллекции UserStore.Users. Типичным примером может служить метод GetUser(), который извлекает одного пользователя из хранилища данных на основе его имени или ключа:

public override MembershipUser GetUser(string username, bool userIsOnline)
{
    try
    {
        SimpleUser user = CurrentStore.GetUserByName(username);
        if (user != null)
        {
            if (userIsOnline)
            {
                user.LastActivityDate = DateTime.Now;
                CurrentStore.Save();
            }
            return CreateMembershipFromInternalUser(user);
        }
        else
        {
            return null;
        }
    }
    catch
    {
        throw;
    }
}

В этом примере метод принимает параметр с именем пользователя и второй параметр, указывающий признак нахождения пользователя в онлайновом режиме. Данный параметр автоматически инициализируется классом Membership, когда он вызывает метод поставщика. В своем методе можно опросить этот параметр; если он установлен в true, понадобится обновить значение LastActivityDate пользователя в хранилище. Эта функция не делает ничего, кроме нахождения пользователя в лежащем в основе хранилище с помощью метода GetUserByName() класса UserStore. Затем она создает экземпляр MembershipUser на базе информации из хранилища, вызывая приватный служебный метод CreateMembershipFromInternalUser().

Реализация поставщика требует нескольких методов, которые работают подобным образом. Нужно просто вызвать методы UserStore соответствующим образом. Некоторые из этих методов требуют возврата не просто MembershipUser, а целой коллекции MembershipUserCollection, как показано ниже:

public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
{
    try
    {
        List<SimpleUser> matchingUsers =
            CurrentStore.Users.FindAll(delegate(SimpleUser user)
            {
                return user.Email.Equals(emailToMatch, StringComparison.OrdinalIgnoreCase);
            });

        totalRecords = matchingUsers.Count;
        return CreateMembershipCollectionFromInternalList(matchingUsers);
    }
    catch
    {
        throw;
    }
}

Например, метод FindUsersByEmail() находит всех пользователей с определенным адресом электронной почты (что возможно, только если поставщик сконфигурирован так, чтобы не требовать уникальности адреса электронной почты, или если для нахождения адреса электронной почты используются шаблоны в виде регулярных выражений). Он возвращает коллекцию пользователей Membership.

Но как легко заметить, этот метод полагается на метод FindAll класса List<T> и анонимный метод, указывающий критерий фильтра. Таким образом, коллекция, возвращенная этим методом, является коллекцией экземпляров SimpleUser, которая применяется в хранилище заднего плана. Для отображения коллекции этого типа на MembershipUserCollection можно написать другой вспомогательный метод:

private MembershipUserCollection CreateMembershipCollectionFromInternalList(List users)
{
    MembershipUserCollection ReturnCollection = new MembershipUserCollection();

    foreach (SimpleUser user in users)
    {
        ReturnCollection.Add(CreateMembershipFromInternalUser(user));
    }

    return ReturnCollection;
}

И, наконец, свойство LastActivityDate, сохраняемое для каждого пользователя, используется классом Membership для определения количества текущих пользователей, находящихся в онлайновом режиме в данном приложении. Этот метод должен быть реализован в пользовательском поставщике через метод GetNumberOfUsersOnline(), как показано ниже:

public override int GetNumberOfUsersOnline()
{
    int ret = 0;

    foreach (SimpleUser user in CurrentStore.Users)
    {
        if (user.LastActivityDate.AddMinutes(
               Membership.UserIsOnlineTimeWindow) >= DateTime.Now)
        {
            ret++;
        }
    }

    return ret;
}

Этот метод просто проходит по всем пользователям в хранилище и обращается к свойству UserlsOnlineTimeWindow, которое является свойством, управляемым через класс Membership и указывающим количество минут, в течение которых пользователь находится в онлайновом режиме без какой-либо активности. До тех пор пока LastActivityDate с этим числом минут больше, чем текущее дата и время, считается, что пользователь находится в онлайновом режиме. Значение LastActivityDate обновляется автоматически различными перегрузками методов GetUser и ValidateUser.

Реализации остальных функций поставщика не представляют никаких новых концепций, поэтому они не рассматриваются. Они просто обновляют некоторые значения для пользователя и затем вызывают метод CurrentStore.Save() для сохранения их в файле XML внутри файловой системы.

Пройди тесты
Лучший чат для C# программистов