ASP.NET 自定义成员资格提供程序 Part.1(以 XML 存储为例,实现底层数据存储)
ASP.NET 提供了成员资格 API 和角色 API,这些 API 为你提供了一个用户和角色管理框架。但通过实现自定义的成员资格和角色提供程序来交换和 SQL Server 一起工作的默认实现,这样无需修改 Web 程序就可以交换用来存储用户和角色信息的底层存储。
正是由于这种分层的抽象设计,一个自定义提供程序总是基于 ASP.NET 成员资格和角色框架所引入的这个分层模型中的最低层。在这个基本架构中,成员资格和角色服务彼此是独立的,它们有各自的基类。
为什么要创建自定义的成员资格提供程序?
- 你可能想使用一个现存的用户和角色数据库,它们和 ASP.NET 标准的设计模式不同
- 你可能想使用一个别的数据库,比如 Oracle、MySql 等
- 你可能想使用一个特别的数据存储,比如 XML 文件、Web 服务或者 LDAP 目录
- 你可能想实现额外的验证逻辑,某些政府网站验证需要:用户名、密码、订阅 ID 等
如果你只想存储默认实现方案之外的一些信息,建议你不要自己实现自定义提供程序。因为成员资格 API 赋予了对存储中唯一确认用户的主键的访问权限,添加你自己的数据库表来存储额外的信息是个好的选择,并通过用户的唯一主键将存储在你的数据表里面的信息和成员资格提供程序存储的实际用户联系起来。
在程序中,你可以通过 MembershipUser 类的 ProviderUserKey 属性来访问用户唯一的主键。
创建自定义提供程序的基本步骤
- 设计并创建底层的数据存储机制
- 创建工具类访问底层的数据存储
- 创建 MembershipUser 的派生类
- 创建 RoleProvider 的派生类
- 创建测试程序,并配置这个自定义提供程序进行测试
实现自定义提供程序非常简单,只是需要一些时间,因为要实现许多方法和属性。本系列文章将创建一个自定义的成员资格和角色提供程序,它使用 XML 文件作为底层的数据存储(XML 文件对于高度可扩展的程序来讲不是一个很好的解决方案)。
1. 自定义提供程序的总体设计
需要仔细考虑解决方案的总体设计。你的目的是底层的功能尽量简单,这样可以将精力集中在实际的成员资格和角色提供程序的实现上面。具体到 XML,加载和保存数据到 XML 文件的最简单方式就是 XML 序列化。只需要调用一个函数就可以将一个完整的对象存储在文件中,调用另一个就可以读取它。
XmlSerializer serializer = new XmlSerializer(typeof(List<SimpleUser>));
using (XmlTextReader reader = new XmlTextReader(fileName))
{
users = ((List<SimpleUser>)serializer.Deserialize(reader));
}
记住,必须在创建序列化器的实例时告诉 XmlSerializer 要序列化和反序列化的类型。(引入 System.Xml 和 System.Xml.Serialization 会更方便)
像 MembershipUser 这样的类不允许你访问某些信息(比如密码),你不能直接使用 XML 序列化它们。XML 序列化需要所有的属性和成员,它们都应以公共属性或者成员来存储。
因此,需要创建自己的工具类代表用户和角色,以便用于后台的数据存储,它们只是简单的依赖于现存的 Membership 类。这个工具类将包含一些内部用户和 MembershipUser 类之间的映射逻辑。
下图是这个自定义提供程序解决方案的总体设计:
SimpleUser 和 SimpleRole 使得 XML 序列化成为了可能,但需要一些逻辑映射支持 MembershipUser。
UserStore 和 RoleStore 都是工具类,用来访问 XML 文件。它们加载和保存 XML 文件,并能进行一些搜索信息的基本功能。
XmlMembershipProvider 继承了 MembershipProvider 的基本功能,而 XmlRoleProvider 继承了 RoleProvider 的基本功能。
2. 设计并实现自定义存储
完成总体设计后,可以考虑底层的数据存储了。在这个例子中,数据存储包含用户和角色这 2 个 XML 文件。使用 XML 序列化作为首要的读写机制。因此,需要一些类以公共字段或者属性的形式持有将存储在 XML 文件里的数据。
public class SimpleUser
{
public Guid UserKey = Guid.Empty;
public string UserName = "";
public string Password = "";
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 System.Collections.Specialized.StringCollection AssignedUsers
= new System.Collections.Specialized.StringCollection();
}
使用 GUID 作为 ProviderUserKey 属性的值,以此来唯一确认数据存储中的用户(主键)。对于角色,需要存储一个名称以及和用户的联系。为了简单起见,每个角色会包含一个用户名(它们是字符串)数组,通过它来和角色关联。
另外一个设计细节就是如何访问数据存储。对于每一个数据存储类(UserStore、RoleStore),都只需要在内存中保留一个实例以便节省资源,避免频繁加载 XML 文件。这可以通过单例(Singleton)模式实现。这种模式保证处理过程中一个类只有一个实例。
public class UserStore
{
private string fileName;
private List<SimpleUser> users;
private XmlSerializer serializer;
private static Dictionary<string, UserStore> registeredStores;
private UserStore(string fileName)
{
this.fileName = fileName;
users = new List<SimpleUser>();
serializer = new XmlSerializer(typeof(List<SimpleUser>));
LoadStore(fileName);
}
public static UserStore GetStore(string fileName)
{
// Create the registered store if it does not exist yet
if (registeredStores == null)
{
registeredStores = new Dictionary<string, UserStore>();
}
// Now return the appropriate store for the filename passed in
if (!registeredStores.ContainsKey(fileName))
{
registeredStores.Add(fileName, new UserStore(fileName));
}
return registeredStores[fileName];
}
......
}
仔细观察这个类的成员。因为构造函数私有,实例无法在类外面创建,因此只能调用 GetStore() 来获取实例。这个场合下单例模式的实现比较特殊,它根据文件名创建单独的实例,对于提供程序所处理的每一个文件来讲,都会创建一个 UserStore 类的一个实例。如果在同一过程中有多个使用该提供程序的 Web 应用程序在运行,你要确保为不同的文件名创建不同的实例。因此,这个类并非为一个单独的实例管理一个静态变量,相反,它拥有一个包含了所有实例的字典,每一项对应一个文件名。
使用 XML 序列化来从存储中保存和加载数据,这 2 个函数就非常的简单:
private void LoadStore(string fileName)
{
try
{
if (System.IO.File.Exists(fileName))
{
using (XmlTextReader reader = new XmlTextReader(fileName))
{
users = serializer.Deserialize(reader) as List<SimpleUser>;
}
}
}
catch (Exception ex)
{
throw new Exception(string.Format("Unable to load file {0}", fileName), ex);
}
}
private void SaveStore(string fileName)
{
try
{
if (System.IO.File.Exists(fileName))
{
System.IO.File.Delete(fileName);
}
using (XmlTextWriter write = new XmlTextWriter(fileName,System.Text.Encoding.UTF8))
{
serializer.Serialize(write, users);
}
}
catch (Exception ex)
{
throw new Exception(string.Format("Unable to save file {0}", fileName), ex);
}
}
最后,这个类提供一些公共方法来从 users 集合里查询信息:
public List<SimpleUser> Users
{
get { return users; }
}
public void Save()
{
SaveStore(fileName);
}
public SimpleUser GetUserByName(string name)
{
return users.Find(p => p.UserName == name);
}
public SimpleUser GetUserByEmail(string email)
{
return users.Find(p => p.Email == email);
}
public SimpleUser GetUserByKey(Guid key)
{
return users.Find(p => p.UserKey.CompareTo(key) == 0);
}
UserStore 只包含了用于保存用户信息的实现。为此,还需要实现 RoleStore 类,这 2 个类非常类似:
public class RoleStore
{
XmlSerializer serializer;
private string fileName;
List<SimpleRole> roles;
private static Dictionary<string, RoleStore> registeredStores;
private RoleStore(string fileName)
{
this.fileName = fileName;
roles = new List<SimpleRole>();
serializer = new XmlSerializer(typeof(List<SimpleRole>));
LoadStore(fileName);
}
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 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("Unable to load file {0}", fileName), ex);
}
}
private void SaveStore(string fileName)
{
try
{
if (System.IO.File.Exists(fileName))
{
System.IO.File.Delete(fileName);
}
using (XmlTextWriter write = new XmlTextWriter(fileName, Encoding.UTF8))
{
serializer.Serialize(write, roles);
}
}
catch (Exception ex)
{
throw new Exception(string.Format("Unable to save file {0}", fileName), ex);
}
}
public List<SimpleRole> Roles
{
get { return this.roles; }
}
public void Save()
{
SaveStore(fileName);
}
public List<SimpleRole> GetRolesForUser(string userName)
{
return roles.FindAll(p => p.AssignedUsers.Contains(userName));
}
public SimpleRole GetRole(string roleName)
{
return roles.Find(p => p.RoleName.Equals(roleName,
StringComparison.OrdinalIgnoreCase));
}
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("Role with name {0} does not exist!", roleName));
}
}
}
这个存储类和 UserStore 类十分相似,只是查询功能略有不同。UserStore 通过 Email、唯一的 ID、用户名查找用户;而 RoleStore 类通过角色名查找所有用户或查询一个指定用户的所有角色。
另外,注意 GetRole() 方法,使用了字符串函数 Equals() 通过传递参数 StringComparison.OrdinalIgnoreCase 比较角色名(不区分大小写)。