使用 MEF、MVVM 和 WCF RIA 服务构建 Silverlight 4 应用程序示例 - 第 3 部分
本系列文章的第 3 部分,介绍使用 MEF、MVVM Light 和 WCF RIA 服务创建 Silverlight 业务应用程序。在本文中,我们将讨论在示例应用程序中如何实现自定义身份验证、重置密码和用户维护。
- 从 第 1 部分 下载源文件和设置包
文章系列
本文是关于使用 MEF、MVVM Light 和 WCF RIA 服务开发 Silverlight 业务应用程序的系列文章的最后一部分。
- 第 1 部分 - 简介、安装和一般应用程序设计主题
- 第 2 部分 - MVVM Light 主题
- 第 3 部分 - 自定义身份验证、重置密码和用户维护
目录
- 引言
- User、LoginUser 和 PasswordResetUser
- AuthenticationService
- PasswordResetService
- AuthenticationModel 和 PasswordResetModel
- 我的个人资料屏幕
- 用户维护屏幕
- 进一步工作
- 参考文献
- 历史
引言
在最后这部分,我们将讨论在示例应用程序中如何实现自定义身份验证、重置密码和用户维护。首先,让我们重申一下我们将要讨论的主要功能
- 有两种用户帐户类型:管理员用户帐户和普通用户帐户。
- 只有管理员用户可以通过用户维护屏幕添加、删除或更新用户。
- 普通用户无法访问用户维护屏幕,只能更新自己的个人资料。
- 帐户添加或更新后,用户首次登录时将提示重置密码和安全答案。
- 如果用户忘记密码,可以使用重置密码屏幕根据安全答案创建新密码。
- 如果用户同时忘记了密码和安全答案,则只有管理员用户可以重置密码。
User、LoginUser 和 PasswordResetUser
User
、LoginUser
和 PasswordResetUser
是在项目 IssueVision.Data.Web 中定义的三个类。User
类是 IssueVision
实体模型中的一个 EntityObject
类。由于 User
类被定义为部分类,我们可以添加一些新属性,如下所示:
/// <summary>
/// User class exposes the following data members to the client:
/// Name, FirstName, LastName, Email, Password, NewPassword,
/// PasswordQuestion, PasswordAnswer, UserType, IsUserMaintenance
/// and ProfileResetFlag
/// </summary>
[MetadataTypeAttribute(typeof(User.UserMetadata))]
public partial class User
{
internal class UserMetadata
{
// Metadata classes are not meant to be instantiated.
protected UserMetadata()
{
}
[Display(Name = "UserNameLabel", ResourceType = typeof(IssueVisionResources))]
[Required(ErrorMessageResourceName = "ValidationErrorRequiredField",
ErrorMessageResourceType = typeof(ErrorResources))]
[RegularExpression("^[a-zA-Z0-9_]*$",
ErrorMessageResourceName = "ValidationErrorInvalidUserName",
ErrorMessageResourceType = typeof(ErrorResources))]
public string Name { get; set; }
[CustomValidation(typeof(UserRules), "IsValidEmail")]
public string Email { get; set; }
[Exclude]
public string PasswordAnswerHash { get; set; }
[Exclude]
public string PasswordAnswerSalt { get; set; }
[Exclude]
public string PasswordHash { get; set; }
[Exclude]
public string PasswordSalt { get; set; }
[Exclude]
public Byte ProfileReset { get; set; }
}
[DataMember]
[Display(Name = "PasswordLabel", ResourceType = typeof(IssueVisionResources))]
[Required(ErrorMessageResourceName = "ValidationErrorRequiredField",
ErrorMessageResourceType = typeof(ErrorResources))]
[RegularExpression("^.*[^a-zA-Z0-9].*$",
ErrorMessageResourceName = "ValidationErrorBadPasswordStrength",
ErrorMessageResourceType = typeof(ErrorResources))]
[StringLength(50, MinimumLength = 12,
ErrorMessageResourceName = "ValidationErrorBadPasswordLength",
ErrorMessageResourceType = typeof(ErrorResources))]
public string Password { get; set; }
[DataMember]
[Display(Name = "NewPasswordLabel", ResourceType = typeof(IssueVisionResources))]
[Required(ErrorMessageResourceName = "ValidationErrorRequiredField",
ErrorMessageResourceType = typeof(ErrorResources))]
[RegularExpression("^.*[^a-zA-Z0-9].*$",
ErrorMessageResourceName = "ValidationErrorBadPasswordStrength",
ErrorMessageResourceType = typeof(ErrorResources))]
[StringLength(50, MinimumLength = 12,
ErrorMessageResourceName = "ValidationErrorBadPasswordLength",
ErrorMessageResourceType = typeof(ErrorResources))]
public string NewPassword { get; set; }
[DataMember]
[Display(Name = "SecurityAnswerLabel",
ResourceType = typeof(IssueVisionResources))]
[Required(ErrorMessageResourceName = "ValidationErrorRequiredField",
ErrorMessageResourceType = typeof(ErrorResources))]
public string PasswordAnswer { get; set; }
[DataMember]
public bool IsUserMaintenance { get; set; }
[DataMember]
public bool ProfileResetFlag
{
get
{
return this.ProfileReset != (byte)0;
}
}
}
从上面的代码中,您可以看到我们通过 UserMetadata
类添加了一些属性。具体来说,我们排除了 PasswordAnswerHash
、PasswordAnswerSalt
、PasswordHash
、PasswordSalt
和 ProfileReset
属性在客户端自动生成。此外,我们还添加了新的属性 Password
、NewPassword
、PasswordAnswer
以及一个只读属性 ProfileResetFlag
。这些更改确保任何密码哈希和密码盐值仅保留在服务器端,并且永远不会通过网络传输。
User
类被 MyProfile
和 UserMaintenance
屏幕使用,我们稍后将讨论该主题。现在,让我们检查 LoginUser
和 PasswordResetUser
类。
LoginUser
类是 User
类的子类,并实现了 IUser
接口。它在 AuthenticationService
类中使用。以下是其定义:
/// <summary>
/// LoginUser class derives from User class and implements IUser interface,
/// it only exposes the following three data members to the client:
/// Name, Password, ProfileResetFlag, and Roles
/// </summary>
[DataContractAttribute(IsReference = true)]
[MetadataTypeAttribute(typeof(LoginUser.LoginUserMetadata))]
public sealed class LoginUser : User, IUser
{
internal sealed class LoginUserMetadata : UserMetadata
{
// Metadata classes are not meant to be instantiated.
private LoginUserMetadata()
{
}
[Key]
[Display(Name = "UserNameLabel", ResourceType = typeof(IssueVisionResources))]
[Required(ErrorMessageResourceName = "ValidationErrorRequiredField",
ErrorMessageResourceType = typeof(ErrorResources))]
[RegularExpression("^[a-zA-Z0-9_]*$",
ErrorMessageResourceName = "ValidationErrorInvalidUserName",
ErrorMessageResourceType = typeof(ErrorResources))]
public new string Name { get; set; }
[Exclude]
public new string Email { get; set; }
[Exclude]
public string FirstName { get; set; }
[Exclude]
public string LastName { get; set; }
[Exclude]
public string NewPassword { get; set; }
[Exclude]
public string PasswordQuestion { get; set; }
[Exclude]
public string PasswordAnswer { get; set; }
[Exclude]
public string UserType { get; set; }
[Exclude]
public bool IsUserMaintenance { get; set; }
}
[DataMember]
public IEnumerable<string> Roles
{
get
{
switch (UserType)
{
case "A":
return new List<string> {
IssueVisionServiceConstant.UserTypeUser,
IssueVisionServiceConstant.UserTypeAdmin };
case "U":
return new List<string> { "User" };
default:
return new List<string>();
}
}
set
{
if (value.Contains(IssueVisionServiceConstant.UserTypeAdmin))
{
// Admin User
UserType = "A";
}
else if (value.Contains(IssueVisionServiceConstant.UserTypeUser))
{
// Normal User
UserType = "U";
}
else
UserType = String.Empty;
}
}
}
与 User
类一样,我们从 LoginUser
类中排除了所有属性在客户端自动生成,除了四个属性:Name
、Roles
、Password
和 ProfileResetFlag
。前两个是 IUser
接口必需的,最后一个属性 ProfileResetFlag
用于确定在管理员用户新创建或最近更新帐户后是否需要要求用户重置个人资料。
接下来,让我们看一下 PasswordResetUser
类。此类也是 User
的子类,并在 PasswordResetService
类中使用。它只公开四个属性:Name
、NewPassword
、PasswordQuestion
和 PasswordAnswer
,定义如下:
/// <summary>
/// PasswordRestUser derives from User class and
/// only exposes the following four data members to the client:
/// Name, NewPassword, PasswordQuestion, and PasswordAnswer
/// </summary>
[DataContractAttribute(IsReference = true)]
[MetadataTypeAttribute(typeof(PasswordResetUser.PasswordResetUserMetadata))]
public sealed class PasswordResetUser : User
{
internal sealed class PasswordResetUserMetadata : UserMetadata
{
// Metadata classes are not meant to be instantiated.
private PasswordResetUserMetadata()
{
}
[Key]
[Display(Name = "UserNameLabel", ResourceType = typeof(IssueVisionResources))]
[Required(ErrorMessageResourceName = "ValidationErrorRequiredField",
ErrorMessageResourceType = typeof(ErrorResources))]
[RegularExpression("^[a-zA-Z0-9_]*$",
ErrorMessageResourceName = "ValidationErrorInvalidUserName",
ErrorMessageResourceType = typeof(ErrorResources))]
public new string Name { get; set; }
[DataMember]
[Display(Name = "SecurityQuestionLabel",
ResourceType = typeof(IssueVisionResources))]
public string PasswordQuestion { get; set; }
[Exclude]
public new string Email { get; set; }
[Exclude]
public string FirstName { get; set; }
[Exclude]
public string LastName { get; set; }
[Exclude]
public string Password { get; set; }
[Exclude]
public string UserType { get; set; }
[Exclude]
public bool IsUserMaintenance { get; set; }
[Exclude]
public bool ProfileResetFlag { get; set; }
}
}
现在我们知道了 User
、LoginUser
和 PasswordResetUser
类的定义方式,就可以看到它们如何在 AuthenticationService
和 PasswordResetService
类中使用。
AuthenticationService
AuthenticationService
是一个 DomainService
类,它实现了 IAuthentication<LoginUser>
接口,并且是提供自定义身份验证的类。以下是 login()
主函数是如何实现的:
/// <summary>
/// Validate and login
/// </summary>
public LoginUser Login(string userName, string password,
bool isPersistent, string customData)
{
try
{
string userData;
if (ValidateUser(userName, password, out userData))
{
// if IsPersistent is true, will keep logged in for up to a week
// (or until you logout)
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
/* version */ 1,
userName,
DateTime.Now, DateTime.Now.AddDays(7),
isPersistent,
userData,
FormsAuthentication.FormsCookiePath);
string encryptedTicket = FormsAuthentication.Encrypt(ticket);
HttpCookie authCookie = new HttpCookie(
FormsAuthentication.FormsCookieName, encryptedTicket);
if (ticket.IsPersistent)
{
authCookie.Expires = ticket.Expiration;
}
HttpContextBase httpContext =
(HttpContextBase)ServiceContext.GetService(typeof(HttpContextBase));
httpContext.Response.Cookies.Add(authCookie);
return GetUserByName(userName);
}
return DefaultUser;
}
catch (Exception ex)
{
Exception actualException = ex;
while (actualException.InnerException != null)
{
actualException = actualException.InnerException;
}
throw actualException;
}
/// <summary>
/// Validate user with password
/// </summary>
/// <param name="username"></param>
/// <param name="password"></param>
/// <param name="userData"></param>
/// <returns></returns>
private bool ValidateUser(string username, string password,
out string userData)
{
userData = null;
LoginUser foundUser = GetUserByName(username);
if (foundUser != null)
{
// generate password hash
string passwordHash =
HashHelper.ComputeSaltedHash(password, foundUser.PasswordSalt);
if (string.Equals(passwordHash, foundUser.PasswordHash,
StringComparison.Ordinal))
{
userData = foundUser.UserType;
return true;
}
return false;
}
return false;
}
Login()
函数调用一个私有函数 ValidateUser()
,ValidateUser()
将根据用户提供的密码和数据库中保存的密码盐生成一个哈希值。如果哈希值与数据库中存储的值匹配,则用户已通过身份验证。
PasswordResetService
同样,PasswordResetService
也是一个 DomainService
类。它只有两个函数。第一个函数 GetUserByName()
接受用户名作为唯一参数,如果数据库中存在该用户名,则返回一个有效的 PasswordResetUser
对象。登录屏幕调用此函数来查找安全问题,然后再切换到重置密码屏幕。
第二个函数是 UpdateUser()
。此函数从客户端接收一个 PasswordResetUser
对象,并检查安全问题和答案是否与数据库中存储的相符。如果相符,新密码将作为密码盐和密码哈希对存储在数据库中。
/// <summary>
/// Update user information to the database
/// User information can only be updated if the user
/// question/answer matches.
/// </summary>
[Update]
public void UpdateUser(PasswordResetUser passwordResetUser)
{
// Search user from database by name
User foundUser = ObjectContext.Users.FirstOrDefault(
u => u.Name == passwordResetUser.Name);
if (foundUser != null)
{
// generate password answer hash
string passwordAnswerHash = HashHelper.ComputeSaltedHash(
passwordResetUser.PasswordAnswer, foundUser.PasswordAnswerSalt);
if ((string.Equals(passwordResetUser.PasswordQuestion,
foundUser.PasswordQuestion, StringComparison.Ordinal)) &&
(string.Equals(passwordAnswerHash, foundUser.PasswordAnswerHash,
StringComparison.Ordinal)))
{
// Password answer matches, so save the new user password
// Re-generate password hash and password salt
foundUser.PasswordSalt = HashHelper.CreateRandomSalt();
foundUser.PasswordHash = HashHelper.ComputeSaltedHash(
passwordResetUser.NewPassword, foundUser.PasswordSalt);
// re-generate passwordAnswer hash and passwordAnswer salt
foundUser.PasswordAnswerSalt = HashHelper.CreateRandomSalt();
foundUser.PasswordAnswerHash =
HashHelper.ComputeSaltedHash(passwordResetUser.PasswordAnswer,
foundUser.PasswordAnswerSalt);
}
else
throw new UnauthorizedAccessException(
ErrorResources.PasswordQuestionDoesNotMatch);
}
else
throw new UnauthorizedAccessException(ErrorResources.NoUserFound);
}
至此,我们完成了对自定义身份验证和重置密码的服务器端数据访问层逻辑的检查。接下来我们将切换到客户端。
AuthenticationModel 和 PasswordResetModel
从客户端来看,LoginForm.xaml 屏幕在运行时绑定到其 ViewModel 类 LoginFormViewModel
,而 ViewModel 类持有一个 AuthenticationModel
和 PasswordResetModel
对象的引用,我们现在将对此进行讨论。
AuthenticationModel
类基于下面的 IAuthenticationModel
接口
public interface IAuthenticationModel : INotifyPropertyChanged
{
void LoadUserAsync();
event EventHandler<LoadUserOperationEventArgs> LoadUserComplete;
void LoginAsync(LoginParameters loginParameters);
event EventHandler<LoginOperationEventArgs> LoginComplete;
void LogoutAsync();
event EventHandler<LogoutOperationEventArgs> LogoutComplete;
IPrincipal User { get; }
Boolean IsBusy { get; }
Boolean IsLoadingUser { get; }
Boolean IsLoggingIn { get; }
Boolean IsLoggingOut { get; }
Boolean IsSavingUser { get; }
event EventHandler<AuthenticationEventArgs> AuthenticationChanged;
}
以下是其主函数 LoginAsync()
的实现:
/// <summary>
/// Authenticate a user with user name and password
/// </summary>
/// <param name="loginParameters"></param>
public void LoginAsync(LoginParameters loginParameters)
{
AuthService.Login(loginParameters, LoginOperation_Completed, null);
}
LoginAsync()
中的 Login()
函数最终将调用我们上面讨论的 AuthenticationService
类中的服务器端 Login()
函数。
同样,PasswordResetModel
基于 IPasswordResetModel
接口。
public interface IPasswordResetModel : INotifyPropertyChanged
{
void GetUserByNameAsync(string name);
event EventHandler<EntityResultsArgs<PasswordResetUser>> GetUserComplete;
void SaveUserAsync();
event EventHandler<ResultsArgs> SaveUserComplete;
void RejectChanges();
Boolean IsBusy { get; }
}
当 ViewModel 类 LoginFormViewModel
需要在切换到重置密码屏幕之前找出正确安全问题时,它会调用 GetUserByNameAsync()
函数。SaveUserAsync()
在 ResetPasswordCommand
内部使用,它最终会调用 PasswordResetService
类中的服务器端 UpdateUser()
来验证并保存新密码(如果安全问题和答案都与数据库中的匹配)。
至此,我们关于自定义身份验证和重置密码逻辑的讨论结束了。接下来,让我们看看用户维护是如何进行的。
我的个人资料屏幕
如上所述,我的个人资料屏幕使用 User
类。此屏幕绑定到 ViewModel 类 MyProfileViewModel
,该类通过 IssueVisionService
类中的两个服务器端函数 GetCurrentUser()
和 UpdateUser()
来检索和更新用户信息。
另外,在管理员用户更新或添加帐户后的首次成功登录期间,我的个人资料屏幕将显示,而不是主页。
实现此功能的实际逻辑位于 ViewModel 类 MainPageViewModel
中,如下所示:
private void _authenticationModel_AuthenticationChanged(object sender,
AuthenticationEventArgs e)
{
IsLoggedIn = e.User.Identity.IsAuthenticated;
IsLoggedOut = !(e.User.Identity.IsAuthenticated);
IsAdmin = e.User.IsInRole(IssueVisionServiceConstant.UserTypeAdmin);
if (e.User.Identity.IsAuthenticated)
{
WelcomeText = "Welcome " + e.User.Identity.Name;
// if ProfileResetFlag is set
// ask the user to reset profile first
if (e.User is LoginUser)
{
if (((LoginUser)e.User).ProfileResetFlag)
{
// open the MyProfile screen
AppMessages.ChangeScreenMessage.Send(ViewTypes.MyProfileView);
CurrentScreenText = ViewTypes.MyProfileView;
}
else
{
// otherwise, open the home screen
AppMessages.ChangeScreenMessage.Send(ViewTypes.HomeView);
CurrentScreenText = ViewTypes.HomeView;
}
}
}
else
WelcomeText = string.Empty;
}
用户维护屏幕
最后,我们将讨论用户维护屏幕。此屏幕仅对管理员用户可用。它绑定到 ViewModel 类 UserMaintenanceViewModel
,并最终通过服务器端的 IssueVisionService
类中的函数 GetUsers()
、InsertUser()
、UpdateUser()
和 DeleteUser()
来检索和更新用户信息。让我们看看 InsertUser()
函数是如何实现的:
public void InsertUser(User user)
{
// check for insert user permission
if (CheckUserInsertPermission(user) && user.IsUserMaintenance)
{
// validate whether the user already exists
User foundUser = ObjectContext.Users.Where(
n => n.Name == user.Name).FirstOrDefault();
if (foundUser != null)
throw new ValidationException(ErrorResources.CannotInsertDuplicateUser);
// Re-generate password hash and password salt
user.PasswordSalt = HashHelper.CreateRandomSalt();
user.PasswordHash = HashHelper.ComputeSaltedHash(
user.NewPassword, user.PasswordSalt);
// set a valid PasswordQuestion
SecurityQuestion securityQuestion =
ObjectContext.SecurityQuestions.FirstOrDefault();
if (securityQuestion != null)
user.PasswordQuestion = securityQuestion.PasswordQuestion;
// set PasswordAnswer that no body knows
user.PasswordAnswerSalt = HashHelper.CreateRandomSalt();
user.PasswordAnswerHash = HashHelper.CreateRandomSalt();
// requires the user to reset profile
user.ProfileReset = 1;
if ((user.EntityState != EntityState.Detached))
{
ObjectContext.ObjectStateManager.ChangeObjectState(
user, EntityState.Added);
}
else
{
ObjectContext.Users.AddObject(user);
}
}
else
throw new ValidationException(ErrorResources.NoPermissionToInsertUser);
}
从上面的代码可以看出,在首次创建新用户时,实际上没有设置安全答案。这也是用户在首次登录时被提醒重置个人资料的原因之一。
后续工作
我们的讨论到此结束。当然,还需要进一步的工作来改进这个示例应用程序。一个明显(且是故意的)遗漏是单元测试项目。此外,添加日志记录机制将有助于跟踪任何潜在问题。
希望您觉得本系列文章有用,请在下方评分和/或留下反馈。谢谢!
参考文献
- 使用 Microsoft Silverlight 4 构建业务应用程序
- 使用 RIA Services、MEF 和 MVVM 构建 Silverlight 4 应用程序架构
- MVVM Light Toolkit
- Silverlight Toolkit
- Silverlight 菜单
历史
- 2010 年 5 月 - 初始发布
- 2010 年 7 月 - 基于反馈的次要更新
- 2010 年 11 月 - 更新以支持 VS2010 Express Edition
- 2011 年 2 月 - 更新以修复包括内存泄漏问题在内的多个 bug
- 2011 年 7 月 - 更新以修复多个 bug