65.9K
CodeProject 正在变化。 阅读更多。
Home

ASP.NET 的服务化会员提供程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (45投票s)

2013年8月12日

CPOL

29分钟阅读

viewsIcon

137251

downloadIcon

8690

基于服务的、多应用程序的 ASP.NET 自定义会员、角色和配置文件提供程序,带分层角色系统。

和 API 文档(可选)

请将所有文档包根目录中的项目解压到数据服务站点的根目录中。但是,如果您不想安装文档,也可以在此处获取。

注意:数据服务现在运行在 .Net 4.5.1 和 Asp.Net Mvc 5 下。应该能够替换旧服务,而不会影响仍在 .Net 4.0 下运行的任何客户端应用程序。

相关文章

引言

用户识别/身份验证和基于角色的组授权是许多多用户应用程序所需的功能。ASP.NET 网站就是此类应用程序的特殊示例。在以下文章中,我们将支持这组功能的系统称为会员管理系统。

会员管理系统至少包括一个用于存储用户、角色和用户配置文件信息的数据源,以及一组应用程序可以与数据源交互以实现会员管理的 API。

ASP.NET 框架具有内置的、易于使用的声明式会员管理机制,前提是应用程序已实现并配置了一组派生自一组预定义基类的提供程序类。在下文中,它们被称为自定义提供程序。

本文描述了一组用于基于服务的会员管理系统的自定义提供程序,如下图所示

Service based membership manager

图:基于服务的会员管理系统,其中面向对象的会员、角色和配置文件数据引擎作为独立服务运行,并使用远程服务调用与应用程序通信。

数据服务本身不是本文详细讨论的主题。简而言之,它是一个基于以下所示特定关系数据架构的自定义构建服务

Database schema

图:关系数据架构示意图。这里的箭头表示相应数据依赖关系和一对多关系的方向。箭头的方向与相应 UML 图中的相似箭头相反。

它具有完整的面向对象服务 API,客户端应用程序可以使用该 API 操作和查询关系数据源,而无需使用任何对象关系映射(ORM)框架。此外,它不依赖于特定的数据库引擎。下载文件中包含该服务的演示版,该版本也在线托管(参见此处),它基于我们自己的一个非常简单的可持久化内存关系数据库,用于演示或测试目的。如果需要,它可以重新连接到关系数据库引擎。

演示站点最初配置为使用在线提供的演示服务。但是,出于性能和/或其他原因,读者可能对下载并将其安装到自己的机器上以使用上述下载链接或演示站点内的链接进行更仔细的检查感兴趣。如果读者尝试运行提供程序的测试项目,尤其如此:首先,这些测试旨在仅允许一个测试进程针对一个数据服务运行;一次针对共享数据服务运行多个测试进程的结果是不可预测的;其次,某些测试对时间敏感,足够长的网络延迟可能导致它们失败。

演示数据服务站点包含有关数据服务的更详细信息。以下是本方法与本文相关的一些独特功能

  1. 本会员管理系统中的应用程序和用户具有多对多关系。一个用户可以是多个应用程序的成员,一个应用程序可以有多个成员。此功能在许多应用程序中都非常需要。不使用这种关系将违背将成员管理系统作为服务运行的主要目的之一。
  2. 角色系统是分层的。即,一个角色可以有一个父角色和/或子角色。尽管分层角色系统本身很复杂,但它可以显著简化客户端软件中详细的授权和访问控制逻辑。
  3. 集中式、基于服务的会员管理是许多现代应用程序场景中所需的功能。

自定义提供程序充当上述数据服务的客户端,该数据服务具有简单直观的访问 API。API 的文档包含在上述演示数据服务网站中。

在安全性方面,哈希算法 MD5 和 SHA1 被认为已被破解。本会员提供程序使用推荐的 SHA256(精确地说是 HMACSHA256)来增强哈希密码的安全性。

本文包括自定义提供程序库的程序集、完整的源代码、提供程序的单元测试代码、演示网站的代码以及用于相应关系数据源和 API 文档的自定义数据服务网站。

背景

Microsoft 提供了 Visual Studio 附带的默认 SQL 提供程序

  • ASP.NET MVC 4.0 之前的版本文档在此处此处此处。它们基于 SQL Server。
  • ASP.NET MVC 4.0 使用不同的默认会员提供程序,称为 SimpleMembership(参见此处)。它包括对 OAuth 的支持。它也基于不同版本的 SQL Server。
  • 通用提供程序可通过 NuGet 获取(另请参见此处)。

参考此处提供了有关它们的更详细信息。

它们是用于入门目的的、不太复杂的版本。例如,它们是基于用户属于一个应用程序(多对一关系)的假设实现的,如下图所示。该图是根据通用提供程序支持数据库的数据架构构建的。在这里,使用不同应用程序的同一个真实用户必须在同一系统中拥有不同的身份,这可能会让用户和安全或人事部门感到困惑,并且它不支持可扩展的多应用程序系统。此外,其中的角色系统也过于简单,无法支持可扩展系统。下文将更详细地解释这一点。

Data schema for universal providers

图:ASP.NET 默认提供程序和许多其他自定义提供程序中假定的应用程序 <=> 用户关系。在这里,一个应用程序有多个用户,但一个用户只能属于一个应用程序。

对于有更高要求的用户(例如 1. 审计;2. 单点登录,此处也讨论了此处;3. 系统可扩展性或可伸缩性等),底层框架由于这些限制无法满足其需求,至少在不对其自身代码库进行大量调整的情况下无法满足。这会产生维护问题。

除了 Microsoft 提供的默认提供程序外,网络上还有许多这些提供程序的自定义实现,包括 CP 上的那些。大多数带有源代码的实现都不是提供程序 API 的完整实现。

一些 ASP.NET 用户可能已经实现了自己的系统,而另一些用户则试图通过笨拙地绕过默认提供程序的限制来使用它们。希望至少对于后者,本贡献能够提供一定的价值。

会员提供程序

它实现了抽象类 MembershipProvider 基类。本节介绍了一些方法,引导用户了解如何访问数据服务 API。有关数据服务 API 的详细信息,请参阅此处的文档。Visual Studio 的 Intellisense 也可以用于发现可用内容。

初始化

首次引用提供程序时会调用此方法

public sealed class AspNetMembershipProvider :  MembershipProvider
{
    ....
    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 = "AspNetMembershipServiceProvider";
        }

        if (String.IsNullOrEmpty(config["description"]))
        {
            config.Remove("description");
            config.Add("description", 
               "Asp.Net Membership Service Provider");
        }
        // Initialize the abstract base class.
        base.Initialize(name, config);
        pApplicationName = GetConfigValue(config["applicationName"],
                           System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath);
                                  
        ... setting up other parameters according to values set inside the site 
        ... Web.config file ...
            
        lock (syncRoot)
        {
            if (_cctx == null)
                _cctx = svc.SignInService(new CallContext(), null);
            CallContext cctx = _cctx.CreateCopy();
            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.");
                }
            }
            Application_ServiceProxy apprepo = new Application_ServiceProxy();
            // must use direct access since all three providers is trying to create the app 
            // at the same time.
            cctx.DirectDataAccess = true;
            List<Application_> apps = apprepo.LoadEntityByNature(cctx, ApplicationName);
            if (apps == null || apps.Count == 0)
            {
                cctx.OverrideExisting = true;
                var tuple = apprepo.AddOrUpdateEntities(cctx, new Application_Set(), 
                               new Application_[] { 
                                    new Application_ { Name = ApplicationName } 
                               });
                app =tuple.ChangedEntities.Length == 1 &&
                                  IsValidUpdate(tuple.ChangedEntities[0].OpStatus) ? 
                                  tuple..ChangedEntities[0].UpdatedItem : null;
                cctx.OverrideExisting = false;
            }
            else
                app = apps[0];
        }
        if (app == null)
            throw new Exception("Member provider initialization failed.");
    }

    ... other methods ...

}

这里需要创建一个 CallContext 类型的变量并登录服务,如下所示

lock (syncRoot)
{
    if (_cctx == null)
        _cctx = svc.SignInService(new CallContext(), null);
    ...
}

返回的值(CallContext 类型)被分配给全局变量 _cctx,每次提供程序尝试访问 API 方法时,都会使用它来创建一个副本。它包含每个客户端的信息,并且必须在服务侧初始化(当前版本的数据服务对此不是很严格,但未来版本将是)。由于网站是多线程的,所有三个提供程序可能同时尝试访问相同的登录方法,锁确保只需要一次调用。然后,初始化程序通过调用 LoadEntityByNature 方法检查 Web.config 文件中命名的应用程序(由提供程序节点的“applicationName”属性的值指定)是否存在于数据源中。如果未找到,则通过调用 AddOrUpdateEntities 方法为其创建一个条目

CallContext cctx = _cctx.CreateCopy();
                 
 ...
 
Application_ServiceProxy apprepo = new Application_ServiceProxy();
// must use direct access since all three providers is trying to create the app 
// at the same time.
cctx.DirectDataAccess = true;
List<Application_> apps = apprepo.LoadEntityByNature(cctx, ApplicationName);
if (apps == null || apps.Count == 0)
{
    cctx.OverrideExisting = true;
    var tuple = apprepo.AddOrUpdateEntities(cctx, new Application_Set(), 
                   new Application_[] { 
                        new Application_ { Name = ApplicationName } 
                   });
    app =tuple.ChangedEntities.Length == 1 &&
                                       IsValidUpdate(tuple.ChangedEntities[0].OpStatus) ? 
                                       tuple..ChangedEntities[0].UpdatedItem : null;
    cctx.OverrideExisting = false;
}
else
    app = apps[0];

Application_ServiceProxyApplication_ 集合服务的代理类。服务代理的类名遵循以下模式

EntityServiceClassName := <实体类型名称> + "ServiceProxy"

其中 <实体类型名称> 是数据源中实体的类名。对于本会员管理系统,它们是

{ Application_, Role, User,UserProfileType, UserProfile, UsersInRole, UserAppMember }

DataSourceServiceClassName := <数据源名称> + "ServiceProxy"

其中 <数据源名称> 是数据源的名称。对于此系统,它是“AspNetMember”。此服务接口用于操作数据源的整体方面。

您可能已经注意到 LoadEntityByNature 方法,它接受应用程序名称作为参数之一。此函数根据实体的固有标识符(intrinsic id)集加载实体。固有 ID 的概念是我们数据系统中引入的关系数据架构的扩展之一。固有 ID 集反映了实体的性质;不允许实体集具有多个具有相同固有 ID 集的实体。固有 ID 是不可变的,即使在分布式数据存储中也是如此。因此它们不能是自动生成的主键,但可以是其他类型的主键,例如 GUID。

在这里,Application_ 自然由其名称标识。上面列出的其他实体集也在扩展关系数据架构中分配了它们自己的固有 ID。AddOrUpdateEntities 方法将强制执行以下逻辑

  • 如果客户端创建了一个实体并调用此方法将其添加到数据源,则
    • 如果已存在具有相同固有 ID 集的实体,则将抛出异常,除非 cctxOverrideExisting 属性设置为 true。在后一种情况下,现有实体将被覆盖。
    • 否则,实体将添加到数据集中。
  • 如果实体首先从数据源加载,然后经过某些处理后发送到此方法,则如果有任何更改,它将被更新。

创建用户

如文档所述,它应将当前应用程序中用户的新会员添加到数据源。

public override MembershipUser CreateUser(string username,
         string password,
         string email,
         string passwordQuestion,
         string passwordAnswer,
         bool isApproved,
         object providerUserKey,
         out MembershipCreateStatus status)
{
    ValidatePasswordEventArgs args = new ValidatePasswordEventArgs(username, 
                                                                   password, 
                                                                   true);
    OnValidatingPassword(args);
    if (args.Cancel)
    {
        status = MembershipCreateStatus.InvalidPassword;
        return null;
    }
    CallContext cctx = _cctx.CreateCopy();
    try
    {
        UserSet us = new UserSet();
        UserAppMemberSet ums = new UserAppMemberSet();
        UserServiceProxy usvc = new UserServiceProxy();
        User udata = null;
        List<User> lu = usvc.LoadEntityByNature(cctx, username);
        if (lu == null || lu.Count == 0)
        {
            if (providerUserKey != null && 
                    usvc.LoadEntityByKey(cctx, providerUserKey.ToString()) != null)
            {
                status = MembershipCreateStatus.DuplicateProviderUserKey;
                return null;
            }
            if (RequiresUniqueEmail)
            {
                var x = GetUserNameByEmail(email);
                if (!string.IsNullOrEmpty(x))
                {
                    status = MembershipCreateStatus.DuplicateEmail;
                    return null;
                }
            }
            DateTime createDate = DateTime.UtcNow;
            if (providerUserKey == null)
            {
                providerUserKey = Guid.NewGuid();
            }
            else
            {
                if (!(providerUserKey is Guid))
                {
                    status = MembershipCreateStatus.InvalidProviderUserKey;
                    return null;
                }
            }
            udata = new User();
            udata.IsPersisted = false;
            udata.ID = providerUserKey.ToString();
            udata.Username = username;
            udata.Password = EncodePassword(password);
            udata.PasswordFormat = pPasswordFormat.ToString();
            udata.Email = email;
            udata.PasswordQuestion = passwordQuestion;
            udata.PasswordAnswer = passwordAnswer;
            udata.IsApproved = isApproved;
            udata.CreateOn = createDate;
            udata.LastPasswordChangedDate = createDate;
            udata.FailedPasswordAttemptCount = 0;
            udata.FailedPasswordAttemptWindowStart = createDate;
            udata.FailedPasswordAnswerAttemptCount = 0;
            udata.FailedPasswordAnswerAttemptWindowStart = createDate;
            udata.Status = us.StatusValues[0];
            UserAppMember memb = new UserAppMember();
            memb.ApplicationID = app.ID;
            memb.UserID = udata.ID;
            memb.MemberStatus = ums.MemberStatusValues[0];
            memb.LastStatusChange = createDate;
            memb.LastActivityDate = createDate;
            memb.Comment = "";
            udata.ChangedUserAppMembers = new UserAppMember[] { memb };
            var v = usvc.AddOrUpdateEntities(cctx, us, new User[] { udata });
            status = v.ChangedEntities.Length == 1 && IsValidUpdate(v.ChangedEntities[0].OpStatus) ? 
                         MembershipCreateStatus.Success : MembershipCreateStatus.DuplicateUserName;
            MembershipUser user = GetUserFromModel(udata, memb);
            return user;
        }
        else if (CheckPassword(password, lu[0].Password, lu[0].PasswordFormat))
        {
            // case of an existing user trying to join an application
            DateTime createDate = DateTime.UtcNow;
            udata = lu[0];
            if (udata.Email != email)
            {
                udata.Email = email;
                udata.IsEmailModified = true;
                usvc.EnqueueNewOrUpdateEntities(cctx, us, new User[] { udata });
            }
            UserAppMemberServiceProxy membsvc = new UserAppMemberServiceProxy();
            UserAppMember memb = membsvc.LoadEntityByKey(cctx, app.ID, udata.ID);
            if (memb != null)
            {
                status = MembershipCreateStatus.Success;
                return GetUserFromModel(udata, memb);
            }
            else
            {
                memb = new UserAppMember();
                memb.IsPersisted = false;
                memb.ApplicationID = app.ID;
                memb.UserID = udata.ID;
                memb.MemberStatus = ums.MemberStatusValues[0];
                memb.LastActivityDate = createDate;
                membsvc.AddOrUpdateEntities(cctx, ums, new UserAppMember[] { memb });
                status = MembershipCreateStatus.Success;
                return GetUserFromModel(udata, memb);
            }
        }
        else
        {
            status = MembershipCreateStatus.DuplicateUserName;
            return null;
        }
    }
    catch (Exception e)
    {
        if (WriteExceptionsToEventLog)
        {
            WriteToEventLog(e, "CreateUser");
        }
        status = MembershipCreateStatus.UserRejected;
        return null;
    }
    finally
    {
    }
}

在验证密码后,该方法首先检查是否存在 Username(它是唯一的固有 ID)等于 username 的用户

UserServiceProxy usvc = new UserServiceProxy();
User udata = null;
List<User> lu = usvc.LoadEntityByNature(cctx, username);
if (lu == null || lu.Count == 0)
{
    ... case 1: not found ...
}
else if (CheckPassword(password, lu[0].Password, lu[0].PasswordFormat))
{
    ... case 2: found and supplied a valid existing password ...
}
else
    ... case 3: reject ...

然后根据找到的结果进行不同的处理。有三种可能性

  1. 情况 1:未找到具有指定 username 的用户。该方法将尝试为当前应用程序向数据源添加用户记录和会员记录
  2. User udata = null;
    ...
    {
        ... check the validity of various input parameters and set the 
        ... status if neccessary according to the documents ...
        udata = new User();
        
        .. assign various properties of udata ...
        
        UserAppMember memb = new UserAppMember();
        
        .. assign various properties of memb ...
        
        // add it the the Changed dependency set to have it add or updated
        udata.ChangedUserAppMembers = new UserAppMember[] { memb };
        
        var v = usvc.AddOrUpdateEntities(cctx, us, new User[] { udata });
        status = v.ChangedEntities.Length == 1 && IsValidUpdate(v.ChangedEntities[0].OpStatus) ? 
                      MembershipCreateStatus.Success : MembershipCreateStatus.DuplicateUserName;
        MembershipUser user = GetUserFromModel(udata, memb);
        return user;
    }

    请注意,根据数据架构,User 实体具有一个依赖集 UserAppMembersUser 实体的相应 ChangedUserAppMembers 属性用于构建相互依赖的实体图,以便将其添加或更新到数据源。任何实体的依赖集(如果有)都具有以相同模式命名的相应属性,可用于更新任何复杂度的实体图。

  3. 情况 2:找到用户,这可能是系统中的现有用户正在尝试加入当前应用程序。为了防止某个用户知道另一个用户的用户名并试图接管后者的帐户,该方法会检查提供的密码。如果密码匹配,则
  4. UserAppMemberServiceProxy membsvc = new UserAppMemberServiceProxy();
    UserAppMember memb = membsvc.LoadEntityByKey(cctx, app.ID, udata.ID);
    if (memb != null)
    {
        //already a member, doing nothing ...
        status = MembershipCreateStatus.Success;
        return GetUserFromModel(udata, memb);
    }
    else
    {
        memb = new UserAppMember();
        
        .. assign various properties of memb ...
        
        membsvc.AddOrUpdateEntities(cctx, ums, new UserAppMember[] { memb });
        status = MembershipCreateStatus.Success;
        return GetUserFromModel(udata, memb);
    }
  5. 情况 3:有人提供了错误的密码,试图注册现有成员,拒绝!

删除用户

从数据源中应用程序的会员记录中删除用户。由于在当前系统中,用户和应用程序具有多对多关系,因此删除实际用户不是特定应用程序的责任。它必须在更高层级完成,例如在数据服务管理器内部。

public override bool DeleteUser(string username, bool deleteAllRelatedData)
{
    CallContext cctx = _cctx.CreateCopy();
    UserServiceProxy usvc = new UserServiceProxy();
    try
    {
        List<User> l = usvc.LoadEntityByNature(cctx, username);
        if (l == null || l.Count == 0)
            return false;
        User u = l[0];
        UserAppMemberServiceProxy msvc = new UserAppMemberServiceProxy();
        UserAppMember memb = msvc.LoadEntityByKey(cctx, app.ID, u.ID);
        msvc.DeleteEntities(cctx, new UserAppMemberSet(), new UserAppMember[] { memb });
        if (deleteAllRelatedData)
        {
            // delete all profiles for the user under the current application
            UserProfileServiceProxy upsvc = new UserProfileServiceProxy();
            UserProfileSet ps = new UserProfileSet();
            UserProfileSetConstraints upcond = new UserProfileSetConstraints
            {
                ApplicationIDWrap = new ForeignKeyData<string> { KeyValue = app.ID },
                TypeIDWrap = null, // all types of the profiles will be included.
                UserIDWrap = new ForeignKeyData<string> { KeyValue = u.ID }
            };
            var pl = upsvc.ConstraintQuery(cctx, ps, upcond, null);
            if (pl.Count() > 0)
            {
                upsvc.DeleteEntities(cctx, ps, pl.ToArray());
            }
            // delete all role assignments associated with the user
            UsersInRoleServiceProxy uisvc = new UsersInRoleServiceProxy();
            UsersInRoleSetConstraints uircond = new UsersInRoleSetConstraints 
            { 
                RoleIDWrap = null, 
                UserIDWrap = new ForeignKeyData<string> { KeyValue = u.ID } 
            };
            var lir = uisvc.ConstraintQuery(cctx, new UsersInRoleSet(), uircond, null);
            if (lir.Count() > 0)
            {
                uisvc.DeleteEntities(cctx, new UsersInRoleSet(), lir.ToArray());
            }
        }
        return true;
    }
    catch (Exception e)
    {
        if (WriteExceptionsToEventLog)
        {
            WriteToEventLog(e, "DeleteUser");
            throw new ProviderException(exceptionMessage);
        }
        else
        {
            throw e;
        }
    }
    finally
    {
    }
}

每个实体服务代理的 DeleteEntities 方法不仅删除实体本身,还删除直接依赖于它的所有实体集(即,在上述模式图中箭头指向的实体),并且它是递归完成的,直到从初始实体开始的整个实体图被移除。这有时是不够的,因为尽管根据数据模式,被删除的实体 UserAppMember 没有直接的依赖集,但应用程序的成员具有间接依赖,即 UserProfileUsersInRole 集。DeleteUser 方法的 deleteAllRelatedData 参数控制是否应删除这些与成员相关的数据。要查找所有关联实体,应首先调用相应服务的 ConstraintQuery 方法列出所有关联数据,然后删除该集合

UserProfileServiceProxy upsvc = new UserProfileServiceProxy();
UserProfileSet ps = new UserProfileSet();
UserProfileSetConstraints upcond = new UserProfileSetConstraints
{
     ApplicationIDWrap = new ForeignKeyData<string> { KeyValue = app.ID },
     TypeIDWrap = null, // all types of the profiles will be included.
     UserIDWrap = new ForeignKeyData<string> { KeyValue = u.ID }
};
var pl = upsvc.ConstraintQuery(cctx, ps, upcond, null);
if (pl.Count() > 0)
{
    upsvc.DeleteEntities(cctx, ps, pl.ToArray());
}</string>

UsersInRoleServiceProxy uisvc = new UsersInRoleServiceProxy();
UsersInRoleSetConstraints uircond = new UsersInRoleSetConstraints 
{ 
    RoleIDWrap = null, 
    UserIDWrap = new ForeignKeyData<string> { KeyValue = u.ID } 
};
var lir = uisvc.ConstraintQuery(cctx, new UsersInRoleSet(), uircond, null);
if (lir.Count() > 0)
{
    uisvc.DeleteEntities(cctx, new UsersInRoleSet(), lir.ToArray());
}

查询用户

对于数据服务,查询关系数据源有一个统一的“语法”。它独立于底层数据存储或数据库。

服务 API 的查询方法接收 QueryExpression 实例,它不是字符串表达式,而是由 QToken 类型的令牌数据列表组成的。为了调用成功,它必须正确构建。熟悉关系数据源和本系统模式的程序员可以轻松构建它。对于其他人,可能需要一定量的反复试验才能正确。

然而,有一种更简单的方法。用户可以转到服务管理器的“数据源”选项卡页面,选择所需的数据集,并使用智能查询指导系统在那里交互式地构建排序和过滤条件(例如,此处)。找到合适的表达式后,单击右侧相应的按钮生成表达式的 C# 代码块。

让我们更详细地研究一下 GetNumberOfUsersOnline 方法

public override int GetNumberOfUsersOnline()
{
    AspNetMemberServiceProxy svc = new AspNetMemberServiceProxy();
    TimeSpan onlineSpan = new TimeSpan(0, 
                         System.Web.Security.Membership.UserIsOnlineTimeWindow, 0);
    DateTime compareTime = DateTime.UtcNow.Subtract(onlineSpan);
    UserAppMemberServiceProxy umsvc = new UserAppMemberServiceProxy();
    // creating a local copy of a global variable for thread safty purposes.
    CallContext cctx = _cctx.CreateCopy();
    try
    {
        QueryExpresion qexpr = new QueryExpresion();
        qexpr.OrderTks = new List<QToken>(new QToken[] 
        { 
           new QToken { TkName = "LastActivityDate" }, 
           new QToken { TkName = "desc" } 
        });
        //
        // Note that not all users are a member of the present application, 
        // we must filter the user set to find only those who are members only
        //
        qexpr.FilterTks = new List<QToken>(new QToken[]{
            new QToken { TkName = "ApplicationID" },
            new QToken { TkName = "==" },
            new QToken { TkName = "\"" + app.ID + "\"" },
            new QToken { TkName = "&&" }
            new QToken { TkName = "LastActivityDate" },
            new QToken { TkName = ">" },
            new QToken { TkName = svc.FormatRepoDateTime(compareTime) }
        });
        int users = (int)umsvc.QueryEntityCount(cctx, new UserAppMemberSet(), qexpr);
        return users;
    }
    catch (Exception e)
    {
        if (WriteExceptionsToEventLog)
        {
            WriteToEventLog(e, "GetNumberOfUsersOnline");
            throw new ProviderException(exceptionMessage);
        }
        else
        {
            throw e;
        }
    }
    finally
    {
    }
}

这个表达式是什么意思?例如,当上述代码块中参数为 app.ID = "713a...." 且 compareTime = 2013年7月28日 00:05:12 本地时间(时区 +8:00)时,过滤表达式的字符串形式是 ApplicationID == "713a...." && LastActivityDate > 2013-07-28 00:05:12 Ltc,字面意思是:“在用户集中查找所有用户,其中”[ApplicationID 等于 "713a...." 且 LastActivityDate 大于 2013/7/27 16:05:12 协调世界时]。这里 [] 中的字面字符串也可以由系统生成。这里的 Ltc 表示采用本地时间坐标,Utc 表示采用协调世界时坐标。

请注意,如果我们在服务管理器中交互式地使用上述参数构造查询表达式,则生成的表达式会更复杂

QueryExpresion qexpr = new QueryExpresion();
qexpr.OrderTks = new List<QToken>(new QToken[] { 
        new QToken { TkName = "LastActivityDate" },
        new QToken { TkName = "desc" }
});
qexpr.FilterTks = new List<QToken>(new QToken[] { 
        new QToken { TkName = "ApplicationID" },
        new QToken { TkName = "==" },
        new QToken { TkName = "\"713ab5b4-0a24-499d-bca9-a29c72227d82\"" },
        new QToken { TkName = "&&" },
        new QToken { TkName = "LastActivityDate" },
        new QToken { TkName = ">" },
        new QToken { TkName = "2013" },
        new QToken { TkName = "-" },
        new QToken { TkName = "07" },
        new QToken { TkName = "-" },
        new QToken { TkName = "28" },
        new QToken { TkName = "00" },
        new QToken { TkName = ":" },
        new QToken { TkName = "05" },
        new QToken { TkName = ":" },
        new QToken { TkName = "12" },
        new QToken { TkName = "Ltc" }
});

这与上面的代码不同,因为日期和时间值不是一个令牌,而是由一系列较小的令牌组成。这不是问题,因为上面等效于以下内容

QueryExpresion qexpr = new QueryExpresion();
qexpr.OrderTks = new List<QToken>(new QToken[] { 
        new QToken { TkName = "LastActivityDate" },
        new QToken { TkName = "desc" }
});
qexpr.FilterTks = new List<QToken>(new QToken[] { 
        new QToken { TkName = "ApplicationID" },
        new QToken { TkName = "==" },
        new QToken { TkName = "\"713ab5b4-0a24-499d-bca9-a29c72227d82\"" },
        new QToken { TkName = "&&" },
        new QToken { TkName = "LastActivityDate" },
        new QToken { TkName = ">" },
        new QToken { TkName = "2013 - 07 - 28 00 : 05 : 12 Ltc" }
});

即,可以通过将几个标记用一个或多个空格字符分隔开来连接起来,从而将较小的标记合并为一个较大的标记。如果你喜欢,它也可以用更传统的方式表示为一个字符串,即

QueryExpresion qexpr = new QueryExpresion();
qexpr.OrderTks = new List<QToken>(new QToken[] { 
        new QToken { TkName = "LastActivityDate" },
        new QToken { TkName = "desc" }
});
qexpr.FilterTks = new List<QToken>(new QToken[] { 
        new QToken 
        { 
            TkName = 
                "ApplicationID == \"713ab5b4-0a24-499d-bca9-a29c72227d82\" && 
                LastActivityDate > 2013-07-28 00:05:12 Ltc" 
        }
});

这里,“Ltc”表示本地时间,日期(2013-07-28)和时间(00:05:12)部分实际上不需要用空格分隔。

您可能已经注意到,当前系统使用特殊格式来表达日期和时间。对于其他格式的日期和时间,建议从中创建一个 .Net DateTime 对象,然后通过调用服务中用于整个关系数据源的 FormatRepoDateTime 方法来格式化 DateTime 对象。

角色提供程序

在当前解决方案中,角色系统是分层的。分层实体集中的一个实体可能依赖于同一集中的另一个实体。因此,一个角色可以有一个父角色和/或一组子角色。这使得提供程序的实现稍微复杂一些。

然而,这允许本地实现以下逻辑:即拥有某个角色的用户也拥有该角色所有(如果有)直接或间接父角色对应的所有角色。例如,拥有角色 Administrators.System 的用户不仅可以访问需要 Administrators.System 角色的资源,还可以访问需要 Administrators 角色的资源。但是,拥有“Administrators”角色的用户不能访问需要 Administrators.System 角色的更严格的资源。

这是一个更具可扩展性的系统。假设一个使用当前提供程序的站点已经部署,并且客户端想要在 Administrators 角色(类别)下添加一个新的角色,例如 HumanResource 角色,那么为了允许新角色中的用户访问所有 Administrators 角色(类别)下的用户被允许访问的资源,就不需要做任何额外的工作。而在扁平角色系统中,要么需要将所有父角色添加到被分配到新角色的用户中,这随着系统的演进而容易出错;要么必须修改站点的代码以将新角色(即 Administrators.HumanResource 角色)添加到代码中的 Authorize 属性中,而这对于客户端是不可用的。

让我们检查几个方法来看看它是如何工作的。

GetRolesForUser

获取指定用户在已配置的 applicationName 中所属角色的列表。

public override string[] GetRolesForUser(string username)
{
    CallContext cctx = _cctx.CreateCopy();
    try
    {
        User u = findUser(username);
        if (u == null)
            return new string[] { };
        RoleServiceProxy rsvc = new RoleServiceProxy();
        QueryExpresion qexpr = new QueryExpresion();
        qexpr.OrderTks = new List<QToken>(new QToken[] { 
            new QToken { TkName = "RoleName" } 
        });
        qexpr.FilterTks = new List<QToken>(new QToken[]{
            new QToken { TkName = "ApplicationID" },
            new QToken { TkName = "==" },
            new QToken { TkName = "\"" + app.ID + "\"" },
            new QToken { TkName = "&&" },
            new QToken { TkName = "UsersInRole." },
            new QToken { TkName = "UserID" },
            new QToken { TkName = "==" },
            new QToken { TkName = "\"" + u.ID + "\"" }
        });
        var roles = rsvc.QueryDatabase(cctx, new RoleSet(), qexpr);
        List<string> lrns = new List<string>();
        foreach (Role r in roles)
        {
            //
            // if a user is in a role, then he/she is in the parent roles 
            // (if any) of that role as well, this rule is also applied to the 
            //  parent role ....
            //
            if (r.ParentID != null)
            {
                Stack<Role> srs = new Stack<Role>();
                Role pr = r;
                while (pr != null)
                {
                    srs.Push(pr);
                    var p = rsvc.MaterializeUpperRef(cctx, pr);
                    pr.UpperRef = p;
                    pr = p;
                }
                while (srs.Count > 0)
                {
                    string rp = rolePath(srs.Pop());
                    if (!lrns.Contains(rp))
                        lrns.Add(rp);
                }
            }
            else
            {
                string rp = rolePath(r);
                if (!lrns.Contains(rp))
                    lrns.Add(rp);
            }
        }
        return lrns.ToArray();
    }
    finally
    {
    }
}

查询表达式

QueryExpresion qexpr = new QueryExpresion();
qexpr.OrderTks = new List<QToken>(new QToken[] { 
    new QToken { TkName = "RoleName" } 
});
qexpr.FilterTks = new List<QToken>(new QToken[]{
    new QToken { TkName = "ApplicationID" },
    new QToken { TkName = "==" },
    new QToken { TkName = "\"" + app.ID + "\"" },
    new QToken { TkName = "&&" },
    new QToken { TkName = "UsersInRole." },
    new QToken { TkName = "UserID" },
    new QToken { TkName = "==" },
    new QToken { TkName = "\"" + u.ID + "\"" }
});

它的字面意思是:从 Roles 实体集中查找当前应用程序(ApplicationID == app.ID)中当前用户所属(UsersInRole.UserID == u.ID)的所有角色,并按 RoleName 排序。调用 QueryDatabase 后,将获得显式分配给用户的一系列角色。这不足以实现我们的逻辑。因为如果一个用户在一个角色中,那么他/她也在该显式角色的所有父角色中。因此对于这些显式角色中的每一个

//
// if a user is in a role, then he/she is in the parent roles 
// (if any) of that role as well, this rule is also applied to the 
//  parent role ....
//
if (r.ParentID != null)
{
    Stack<Role> srs = new Stack<Role>();
    Role pr = r;
    while (pr != null)
    {
        srs.Push(pr);
        var p = rsvc.MaterializeUpperRef(cctx, pr);
        pr.UpperRef = p;
        pr = p;
    }
    while (srs.Count > 0)
    {
        string rp = rolePath(srs.Pop());
        if (!lrns.Contains(rp))
            lrns.Add(rp);
    }
}
else
{
    // this role has no parent
    string rp = rolePath(r);
    if (!lrns.Contains(rp))
        lrns.Add(rp);
}

在显式角色具有父角色的情况下,代码段通过重复调用实体集服务的 MaterializeUpperRef 方法来查找其所有父角色,并将返回的父角色添加到用户拥有的角色列表中。

如这里所示,用于加载当前实体所依赖的实体的方法名称模式如下

"Materialize" + <属性名>

其中 <属性名> 是与当前实体所依赖的实体对应的属性的名称,这里是 UpperRef

获取角色中的用户

获取已配置的 applicationName 中指定角色中的用户列表。

public override string[] GetUsersInRole(string rolename)
{
    CallContext cctx = _cctx.CreateCopy();
    try
    {
        Role r = findRole(rolename);
        if (r == null)
            return new string[] { };
        RoleServiceProxy rsvc = new RoleServiceProxy();
        var ra = rsvc.LoadEntityHierarchyRecurs(cctx, r, 0, -1);
        //for a given role, the users in it also include the ones in all its 
        // child roles, recursively (see above), in addition to its own ...
        List<string> luns = new List<string>();
        _getUserInRole(cctx, ra, luns);
        return luns.ToArray();
    }
    finally
    {
    }
}

private void _getUserInRole(CallContext cctx, EntityAbs<Role> ra, 
                                                List<string> usersinrole)
{
    UserServiceProxy usvc = new UserServiceProxy();
    QueryExpresion qexpr = new QueryExpresion();
    qexpr.OrderTks = new List<QToken>(new QToken[] { 
            new QToken { TkName = "Username" } 
         });
    qexpr.FilterTks = new List<QToken>(new QToken[] {
            new QToken { TkName = "UsersInRole." },
            new QToken { TkName = "RoleID" },
            new QToken { TkName = "==" },
            new QToken { TkName = "" + ra.DataBehind.ID + "" }
        });
    var users = usvc.QueryDatabase(cctx, new UserSet(), qexpr);
    foreach (User u in users)
        usersinrole.Add(u.Username);
    if (ra.ChildEntities != null)
    {
        foreach (var c in ra.ChildEntities)
            _getUserInRole(cctx, c, usersinrole);
    }
}

此方法的逻辑与 GetRolesForUser 中的逻辑有些相反,即特定角色中的用户是明确拥有该角色的用户,以及明确属于该角色子角色树中的所有用户。

以下是其实现方式

RoleServiceProxy rsvc = new RoleServiceProxy();
var ra = rsvc.LoadEntityHierarchyRecurs(cctx, r, 0, -1);

所有分层集都具有额外的 API 方法来处理实体层次结构相关操作(有关其他方法,请参阅服务 API 文档)。方法 LoadEntityHierarchyRecurs 从一个节点开始加载树的一部分,具有给定的最大相对高度(相对于起始节点)和最大相对深度,并返回加载的子树的根。它接受四个参数。第二个参数是起始节点,第三个参数是最大相对高度,第四个参数是最大相对深度。如果为相对高度或深度提供值 -1,则加载相应方向上的所有可用节点。例如,对于以下图中所示的树,如果从红色节点开始并调用此方法,最大相对高度为 1,最大相对深度为 1,则加载的节点标记为深灰色。

Partial tree

图:此处红色节点为起始节点,深灰色节点为加载的树节点。

因此,上面的代码加载以该角色为本地根的角色子树,并调用递归方法 _getUserInRole 来累积所有在该角色子树中具有明确角色分配的用户。

配置文件提供程序

GetPropertyValues

在此处进行描述,因为服务 API 的其他方面也应在此处描述。

public override SettingsPropertyValueCollection GetPropertyValues(
                                                          SettingsContext context, 
                                                          SettingsPropertyCollection ppc)
{
    string username = (string)context["UserName"];
    bool isAuthenticated = (bool)context["IsAuthenticated"];
    // The serializeAs attribute is ignored in this provider implementation.
    SettingsPropertyValueCollection svc = new SettingsPropertyValueCollection();
    CallContext cctx = _cctx.CreateCopy();
    cctx.OverrideExisting = true;
    try
    {
        User u = isAuthenticated ? findUser(username) : null;
        UserProfileTypeServiceProxy uptsvc = new UserProfileTypeServiceProxy();
        UserProfileServiceProxy upsvc = new UserProfileServiceProxy();
        List<UserProfile> update = new List<UserProfile>();
        List<UserProfile> added = new List<UserProfile>();
        UserProfileSetConstraints cond = new UserProfileSetConstraints
        {
            ApplicationIDWrap = new ForeignKeyData<string> { KeyValue = app.ID },
            TypeIDWrap = null,
            UserIDWrap = new ForeignKeyData<string> { KeyValue = u.ID }
        };
        var profs = upsvc.ConstraintQuery(cctx, new UserProfileSet(), cond, null);
        foreach (SettingsProperty prop in ppc)
        {
            bool found = false;
            if (profs != null)
            {
                foreach (UserProfile p in profs)
                {
                    if (prop.Name == p.PropName)
                    {
                        SettingsPropertyValue pv = new SettingsPropertyValue(prop);
                        switch (prop.SerializeAs)
                        {
                            case SettingsSerializeAs.String:
                            case SettingsSerializeAs.Xml:
                                if (!p.IsStringValueLoaded)
                                {
                                    p.StringValue = upsvc.LoadEntityStringValue(cctx, p.ID);
                                    p.IsStringValueLoaded = true;
                                }
                                pv.SerializedValue = p.StringValue;
                                break;
                            case SettingsSerializeAs.Binary:
                                if (!p.IsBinaryValueLoaded)
                                {
                                    p.BinaryValue = upsvc.LoadEntityBinaryValue(cctx, p.ID);
                                    p.IsBinaryValueLoaded = true;
                                }
                                pv.SerializedValue = p.BinaryValue;
                                break;
                            default:
                                break;
                        }
                        svc.Add(pv);
                        update.Add(p);
                        p.LastAccessTime = DateTime.UtcNow;
                        p.IsLastAccessTimeModified = true;
                        found = true;
                        break;
                    }
                }
            }
            if (!found)
            {
                // do not support provider in this version ...
                string seras = prop.SerializeAs == SettingsSerializeAs.ProviderSpecific? 
                               SerializationMode.String.ToString() : 
                               prop.SerializeAs.ToString();
                var upts = uptsvc.LoadEntityByNature(cctx, 
                                        prop.PropertyType.FullName, null, seras);
                UserProfileType upt;
                if (upts != null && upts.Count > 0)
                    upt = upts[0];
                else
                {
                    upt = new UserProfileType();
                    upt.ClrType = prop.PropertyType.FullName;
                    upt.SerializeType = seras;
                    upt.SerializationProvider = null; // not handled now
                }
                UserProfile p = new UserProfile();
                p.PropName = prop.Name;
                p.ApplicationID = app.ID;
                p.UserID = u == null ? null : u.ID;
                p.TypeID = upt.ID;
                p.UserProfileTypeRef = upt;
                p.LastAccessTime = DateTime.UtcNow;
                p.LastUpdateTime = p.LastAccessTime;
                added.Add(p);
                SettingsPropertyValue pv = new SettingsPropertyValue(prop);
                switch (prop.SerializeAs)
                {
                    case SettingsSerializeAs.String:
                    case SettingsSerializeAs.Xml:
                        pv.SerializedValue = p.StringValue;
                        break;
                        case SettingsSerializeAs.Binary:
                            pv.SerializedValue = p.BinaryValue;
                            break;
                        default:
                            pv.SerializedValue = p.StringValue;
                            break;
                    }
                    svc.Add(pv);
                }
            }
            if (added.Count > 0)
                upsvc.AddOrUpdateEntities(cctx, new UserProfileSet(), added.ToArray());
            if (update.Count > 0)
                upsvc.EnqueueNewOrUpdateEntities(cctx, new UserProfileSet(), update.ToArray());
            return svc;
        }
        catch (Exception e)
        {
            throw e;
        }
        finally
        {
        }
    }</string>

此方法由应用程序调用,用于检索一组属性的值。要返回的属性集由参数 ppc 指定。该方法将此列表与已为用户注册的属性进行比较,未找到的属性将被添加

if (!found)
{
    string seras = prop.SerializeAs == SettingsSerializeAs.ProviderSpecific? 
                   SerializationMode.String.ToString() : 
                   prop.SerializeAs.ToString();
    // first check to see if the property type is registered
    var upts = uptsvc.LoadEntityByNature(cctx, 
                            prop.PropertyType.FullName, null, seras);
    UserProfileType upt;
    if (upts != null && upts.Count > 0)
        upt = upts[0];
    else
    {
        // no? create a new property type record
        upt = new UserProfileType();
        upt.ClrType = prop.PropertyType.FullName;
        upt.SerializeType = seras;
        upt.SerializationProvider = null; // not handled now
    }
    UserProfile p = new UserProfile();
    ... assign other properties
    p.UserProfileTypeRef = upt;
    added.Add(p);
    ... update the value of the property, changed or not ...
}

此代码段的有趣部分是 p.UserProfileTypeRef = upt 这行,其中 uptUserProfile 实体 p 所依赖的实体(参见数据模式图)。upt 被分配给 pUserProfileTypeRef 属性,以便在添加 p 时,它将自动添加到 UserProfileTypes 集中。这种添加给定实体所依赖的实体的模式可以应用于任何实体,并且可以重复应用于依赖图中的上层实体,直到达到根实体(即不依赖于其他实体类型的那些实体类型)。结合上述讨论的添加依赖实体的方式,原则上可以通过一次调用服务将整个实体图作为一个工作单元添加到数据源中。

应该提及的其他代码行是

if (added.Count > 0)
    upsvc.AddOrUpdateEntities(cctx, new UserProfileSet(), added.ToArray());
if (update.Count > 0)
    upsvc.EnqueueNewOrUpdateEntities(cctx, new UserProfileSet(), update.ToArray());

added 列表包含要添加到相应集合的 UserProfile 记录,它们通过调用我们已经知道的 AddOrUpdateEntities 方法添加。update 列表包含已存在于相应集合中的属性列表,它们之所以更新,是因为 UserProfile 实体有一个 LastAccessTime,每次读取属性时都必须更新。由于用户可能会多次调用 GetPropertyValues 方法,因此只有最后一次访问才真正重要,这些属性更新由 EnqueueNewOrUpdateEntities 方法处理。它的作用是将更新存储在内部队列中,并仅在将来的某个时间点将更新提交到相应的数据集。如果在等待提交时再次调用它,对于更新中的每个新实体,它将检查队列中是否已存在具有相同固有 ID 的实体等待提交。如果存在,现有实体更改将与新实体合并,然后新实体替换现有实体。如果不存在,它将被添加到队列中。通过调用此方法可以避免对后端数据源进行多次重复的更新调用。

此处不再详细讨论其他方法,因为关于如何使用服务 API 所需的大部分信息已经提及。

读者可以直接查阅源代码,尤其是测试项目中的源代码,借助 Visual Studio 的智能感知和客户端 API 文档,挖掘更详细的信息。

测试项目

在运行测试之前,需要进行一些配置。测试项目中有一个 app.config 文件,其中数据服务端点未完全初始化。参数 __servicedomain__ 应替换为托管数据服务的域名(如果不是 80 端口,则为端口)。

注意:不要针对多个代理(包括其他测试代理)可能同时访问的数据服务实例进行测试。之所以需要这样做,不是因为服务无法处理多个用户的 CRUD 操作,而是因为测试代码会不断重置(而不是锁定)数据源的状态,因此如果其他代理进行乱序重置,则其他代理和当前测试代理的结果是不可预测的。

所包含的测试项目解决方案既可用于测试提供程序,也可作为源代码,提供另一个视角来研究提供程序、其功能和数据服务。这是因为它们涵盖的细节远多于以下演示网站中包含的细节。

演示网站

演示网站的解决方案是针对 ASP.NET MVC 4 网站的。

它有四个选项卡。所有用户都可以查看“主页”和“关于”选项卡。“联系我们”选项卡可以由拥有 Administrators 角色的用户查看。“管理”选项卡可以由拥有 Administrators.System 角色的用户查看。

public class HomeController : BaseController
{
    ... other actions ...

    [Authorize(Roles="Administrators")]
    public ActionResult Contact()
    {
        ViewBag.Message = "Your contact page.";

        return View();
    }
    ... other actions ...
}

[Authorize]
public class AccountController : BaseController 
{
    ... other actions ...
    [HttpGet]
    [Authorize(Roles = "Administrators.System")]
    public ActionResult Admin()
    {
        return View();
    }
    ... other actions ...
}

数据服务有两个用户设置。使用其中一个凭据登录以检查基于角色的授权的效果。使用演示站点注册新用户。如果您愿意,可以使用数据服务管理器添加角色并分配角色。

如果您正在使用在线演示服务,添加到远程数据服务的新数据将无法保存,一旦服务重新加载,它们就会丢失。对于本地托管的演示服务,服务站点下的“App_Data\AspNetMember\Data”文件夹需要具有适当的权限才能保存数据。即,该文件夹应为用户 IIS_IUSRS 分配写入权限。

另请注意,服务站点上的“Scripts\DbViewModels\AspNetMember”子目录包含一份完整的 knockout.js 视图模型列表,您可以使用它们向演示网站或使用提供程序的其他网站添加管理内容。例如,您可以构建自己的更自定义的角色和角色分配界面。

使用自定义提供程序

自定义提供程序应配置为在 ASP.NET 网站上使用。

配置

首先,应该重新生成机器密钥值,不要使用演示 web.config 文件中的值。

要使用自定义提供程序,必须将以下部分添加到(或替换)网站的 web.config 文件中。

<system.web>
   ...
   other system.web settings 
   ...
   <membership defaultProvider="DefaultMembershipProvider">
      <providers>
         <clear/>
         <add name="DefaultMembershipProvider" 
             type="Archymeta.Web.Security.AspNetMembershipProvider, 
             AspNetUserServiceProvider, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
             passwordFormat="Hashed"
             enablePasswordRetrieval="false" 
             enablePasswordReset="true" 
             requiresQuestionAndAnswer="false" 
             requiresUniqueEmail="true" 
             maxInvalidPasswordAttempts="5" 
             minRequiredPasswordLength="6" 
             minRequiredNonalphanumericCharacters="0" 
             passwordAttemptWindow="10" 
             passwordStrengthRegularExpression=""
             writeExceptionsToEventLog="false"
             applicationName="demo" />
      </providers>
   </membership>
   <roleManager defaultProvider="DefaultRoleProvider" enabled="true">
      <providers>
         <clear/>
         <add name="DefaultRoleProvider" type="Archymeta.Web.Security.AspNetRoleProvider, 
             AspNetUserServiceProvider, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
             writeExceptionsToEventLog="false"
             applicationName="demo" />
      </providers>
   </roleManager>
   <profile defaultProvider="DefaultProfileProvider">
      <providers>
         <clear/>
         <add name="DefaultProfileProvider" 
              type="Archymeta.Web.Security.AspNetProfileProvider, 
              AspNetUserServiceProvider, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" 
              connectionStringName="DefaultConnection" 
              applicationName="demo" />
      </providers>
      <properties>
         <add name = "FirstName"/>
         <add name = "LastName"/>
         <group name = "SiteColors" >
            <add name = "BackGround"/>
            <add name = "SideBar"/>
            <add name = "ForeGroundText"/>
            <add name = "ForeGroundBorders"/>
         </group>
         <group name="Forums">
            <add name = "HasAvatar" type="bool" />
            <add name = "LastLogin" type="DateTime" />
            <add name = "TotalPosts" type="int" />
         </group>
      </properties> 
   </profile>
</system.web>

每个提供程序节点中的属性由相应提供程序的“Initialize”方法读取。它们可用于控制相应提供程序的行为。配置文件部分的 <properties> 节点定义了用户可以获取或设置的每个属性的名称、类型和序列化(以及其他元信息)。上述列表仅为示例,读者应设置自己的属性集来定义用户配置文件。请注意<roleManager/> 节节点应具有一个“enabled”属性,该属性必须显式设置为“true”才能调用角色提供程序,否则结果是不可预测的。

您也应该将网站设置为数据服务的客户端。以下是一些基本设置

<system.serviceModel>
   <bindings>
      <basicHttpBinding>
         <binding name="basicHttpBinding_DataService" 
                 allowCookies="true" maxBufferSize="6553600"
                 maxBufferPoolSize="5242880" 
                 maxReceivedMessageSize="6553600" >
            <security mode="None" />
         </binding>
      </basicHttpBinding>
   </bindings>
   <behaviors>
      <endpointBehaviors>
         <behavior name="ImpersonateEndpointBehavior">
            <clientCredentials>
               <windows allowedImpersonationLevel="Delegation" 
                              allowNtlm="true" />
            </clientCredentials>
         </behavior>
      </endpointBehaviors>
   </behaviors>
   <client>
      <endpoint name="HTTP" 
              address="http://_domain_/Services/DataService/AspNetMember/AspNetMemberDatabase2.svc" 
              binding="basicHttpBinding" 
              bindingConfiguration="basicHttpBinding_DataService" 
              contract="CryptoGateway.RDB.Data.AspNetMember.IAspNetMemberService2" />
      <endpoint name="HTTP" 
              address="http://_domain_/Services/DataService/AspNetMember/Application_Set2.svc" 
              binding="basicHttpBinding" 
              bindingConfiguration="basicHttpBinding_DataService" 
              contract="CryptoGateway.RDB.Data.AspNetMember.IApplication_Service2" />
      <endpoint name="HTTP" 
              address="http://_domain_/Services/DataService/AspNetMember/RoleSet2.svc" 
              binding="basicHttpBinding" 
              bindingConfiguration="basicHttpBinding_DataService" 
              contract="CryptoGateway.RDB.Data.AspNetMember.IRoleService2" />
      <endpoint name="HTTP" 
              address="http://_domain_/Services/DataService/AspNetMember/UserAppMemberSet2.svc" 
              binding="basicHttpBinding" 
              bindingConfiguration="basicHttpBinding_DataService" 
              contract="CryptoGateway.RDB.Data.AspNetMember.IUserAppMemberService2" />
      <endpoint name="HTTP" 
              address="http://_domain_/Services/DataService/AspNetMember/UserProfileSet2.svc" 
              binding="basicHttpBinding" 
              bindingConfiguration="basicHttpBinding_DataService" 
              contract="CryptoGateway.RDB.Data.AspNetMember.IUserProfileService2" />
      <endpoint name="HTTP" 
              address="http://_domain_/Services/DataService/AspNetMember/UserProfileTypeSet2.svc" 
              binding="basicHttpBinding" 
              bindingConfiguration="basicHttpBinding_DataService" 
              contract="CryptoGateway.RDB.Data.AspNetMember.IUserProfileTypeService2" />
      <endpoint name="HTTP" 
              address="http://_domain_/Services/DataService/AspNetMember/UserSet2.svc" 
              binding="basicHttpBinding" 
              bindingConfiguration="basicHttpBinding_DataService" 
              contract="CryptoGateway.RDB.Data.AspNetMember.IUserService2" />
      <endpoint name="HTTP" 
              address="http://_domain_/Services/DataService/AspNetMember/UsersInRoleSet2.svc" 
              binding="basicHttpBinding" 
              bindingConfiguration="basicHttpBinding_DataService" 
              contract="CryptoGateway.RDB.Data.AspNetMember.IUsersInRoleService2" />
   </client>
</system.serviceModel>

这里,每个端点节点中 address 属性内的 _domain_ 表示域名或 IP 地址(如果不是 80 端口,则加上端口号),应将其更改为指向托管数据服务的服务器的正确值。

Web.config 文件的 <connectionStrings> 部分包含一个 name="DefaultConnection" 的添加节点,它最初指向 ASP.NET 创建的会员数据库。如果您不再使用它,可以删除该节点,或者更改其内容以指向网站将要使用的其他数据库。

更改默认项目

开发人员很可能使用 Visual Studio 提供的模板创建 ASP.NET MVC 网站或 Web 应用程序。对于 ASP.NET MVC 4.0 之前的 Web 应用程序,上述更改足以将解决方案更改为使用当前的提供程序。

对于默认的 ASP.NET MVC 4.0 Web 应用程序,还有更多需要更改的地方。这是因为生成的解决方案中的默认会员提供程序是 SimpleMembership(参见此处),它不是派生自 MembershipProvider 基类,而是派生自一个新的 ExtendedMembershipProvider 类,该类包含对 OAuth 访问 API 的支持。生成的 AccountController 依赖于 WebMatrix.WebData 程序集中 WebSecurity 类的静态方法来调用提供程序 API,它们与派生自 MembershipProvider 基类的提供程序不兼容。

对于 ASP.NET MVC 4.0,生成的 AccountController 需要进行许多更改才能在此处使用。然而,一个简单的解决方案是创建一个 ASP.NET MVC 3.0 Web 应用程序,并将 AccountController 类的内容复制以覆盖 MVC 4.0 的内容。然后,您可以从项目中删除 Filters 文件夹下的 InitializeSimpleMembershipAttribute.cs 文件,因为当前提供程序不依赖 Entity Framework 来访问我们的数据服务。此外,还需要删除对 WebMatrix.Data.dllWebMatrix.WebData.dll 的项目引用,它们不再需要。

设置数据服务

将文件从会员数据服务包中提取到一个文件夹,为其配置一个网站(它是一个 ASP.NET MVC 4 Web 应用程序)。在您的系统中启用 WCF 的 HTTP 激活。基本上就是这样。

如果您需要持久化更改,至少服务站点下的“App_Data\AspNetMember\Data”子目录需要具有适当的权限。它应该允许用户 IIS_IUSRS 写入权限。

数据服务预加载了用于演示目的的示例数据集。服务本身具有一个 Web 前端,用户可以使用它添加、删除或查询数据。如果需要使用 Web 前端操作数据,请阅读此处的相应部分,获取详细说明。这些说明也适用于当前数据服务。

警告:这是用于评估目的的系统演示版本。它基于一种瞬态序列化方法。请勿将其用于长期数据持久化或支持生产系统。

关注点

Linux 下的服务

已做出一些努力,尝试使数据服务在 Mono(版本 3.0.3.1)(xsp + mono)下的 Linux 盒上运行。以下软件包是使用最新的 MonoDevelop(本身从 git 源代码编译)构建的。现在可以浏览数据服务站点,这意味着 Asp.Net MVC 4 部分对于服务来说似乎是正常的。感谢参与该项目的人们所做的出色工作。

他们似乎还有很多艰苦的工作要做,才能使 WCF 部分像 .Net WCF 那样工作。希望这里的发现能为他们提供信息,使 Mono 成为更好的服务托管平台。

我们的实验表明,Mono WCF 的 webHttpBinding(RESTful + json)部分不允许调用具有多个参数的服务方法。因此,当服务托管在 Linux 盒中时,数据源的大部分管理页面无法按预期使用。

Mono WCF 的 basicHttpBinding 部分工作得更好。但它似乎仍然不太完善。当我们的测试套件在 Linux 服务器上托管的服务上运行时,大约一半的测试会失败。详细调查显示,许多问题与数据序列化有关,即返回的一些对象图未按预期序列化,至少在使用 Windows .Net 客户端程序时是这样。例如,通过在服务端和客户端使用调试器进行比较,我们发现:1) 对于某些实体图,发送前服务站点上的子对象在客户端丢失了;2) 未使用 DataMember 属性标记的成员属性被序列化,导致返回的对象图大得多;等等。

Northwind 数据库已迁移

Microsoft Northwind 数据库已迁移到我们的内存数据库演示中,用于演示目的。此处是一个在线演示。

新型搜索方式

StackExchange.com 提供其数据的定期 XML 格式数据转储。从数据集中推断出关系数据模式,并为它们构建了一个只读数据服务。该服务目前连接到 PostgreSQL 数据库引擎,该引擎支持原生和统一全文搜索表达式,可与任意查询表达式中的其他元数据过滤相结合。感兴趣的读者可以访问 演示站点 A(包含大约 44 万个 Q/A),了解 serverfault.com,以及访问 演示站点 B(包含大约 5 万个 Q/A),了解 gis.stackexchange.com。服务 UI 允许用户以更准确、更灵活的方式查找、排序和研究数据。

基于服务的关系数据 + 关键词 + 动态分类搜索门户的演示门户已上线,请参见此处

历史

  1. 版本 1.0:首次发布。
  2. 版本 1.1:整体系统更新,支持异构数据库(具有相同数据模式)子系统之间的数据迁移。
  3. 版本 1.1.1:显著提高了数据导入和数据同步/迁移子系统的智能性和性能。进行了小的 API 更改。
  4. 版本 1.2:数据库中需要这些功能的表支持原生和统一全文索引和搜索。演示站点进行了整体升级(库、样式等),现在基于 foundation
  5. 版本 1.2.1:提供程序 bug 修复。向数据模型添加了数据注释。改进了文档。
  6. 版本 1.2.2:由于服务 JavaScript 中的一个错误,添加或更新项目时的不正确行为已得到纠正。添加了更多视图。只更改了服务包。
  7. 版本 1.2.5:系统整体优化、更新、 bug 修复、性能调优、API 新增和文档更新。
  8. 版本 1.2.6:系统整体更新、增强。
  9. 版本 1.3.0:累积系统更新,增强。添加了 .Net 4.0 版本之后的异步服务代理 API。添加了异步 API 文档。发布了 .Net 4.0 之后的 Asp.Net 异步会员存储
  10. 版本 1.5.0:数据服务现在运行在 .NET 4.5.1 和 ASP.NET MVC 5 下,改进了许多功能,并添加了新功能,例如支持 SignalR 或基于 WCF 的实体更改事件订阅端口等。
ASP.NET 基于服务的会员提供程序 - CodeProject - 代码之家
© . All rights reserved.