понедельник, 30 августа 2010 г.

Немного об аутентификации в Silverlight 4 - Часть 4

Теперь нам нужно правильно реализовать логику работы DataForm для пользователей. Можно выдвинуть следующие требования.

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

· Поля с паролем и подтверждением пароля должны быть пустыми пока администратор сам не заполнит их.

· (Соответственно пароль не должен обнуляться, когда он не заполнен, а изменялись только другие поля)

· Логин пользователя должен быть уникальным.

Сейчас код нашей страницы пользователей выглядит примерно так:

Users.xaml

<navigation:Page

x:Class="MyApplication.Users"

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

xmlns:navigation="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"

mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480"

Style="{StaticResource PageStyle}" xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk" xmlns:riaControls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.DomainServices" xmlns:my="clr-namespace:MyApplication.Web.Services" xmlns:my1="clr-namespace:MyApplication.BLL" xmlns:my2="clr-namespace:MyApplication.Controls">

<UserControl.Resources>

Описание стиля для DataGrid (в предыдущей статье)

</UserControl.Resources>

<Grid x:Name="LayoutRoot">

<ScrollViewer x:Name="PageScrollViewer" Style="{StaticResource PageScrollViewerStyle}">

<StackPanel x:Name="ContentStackPanel" Style="{StaticResource ContentStackPanelStyle}">

<TextBlock x:Name="HeaderText" Style="{StaticResource HeaderTextStyle}"

Text="{Binding Path=ApplicationStrings.UsersPageTitle, Source={StaticResource ResourceWrapper}}"/>

<StackPanel>

<my2:BusyIndicator IsBusy="{Binding ElementName=userDomainDataSource, Path=IsBusy}">

<sdk:DataGrid AutoGenerateColumns="False" ItemsSource="{Binding ElementName=userDomainDataSource, Path=Data}" Name="userDataGrid" RowDetailsVisibilityMode="VisibleWhenSelected" LoadingRow="userDataGrid_LoadingRow" IsReadOnly="True">

<sdk:DataGrid.Columns>

<sdk:DataGridTemplateColumn Width="80" HeaderStyle="{StaticResource DataGridColumnHeaderStyle}">

<sdk:DataGridTemplateColumn.CellTemplate>

<DataTemplate>

<CheckBox x:Name="chk"

HorizontalAlignment="Center"

HorizontalContentAlignment="Center"

VerticalAlignment="Center"></CheckBox>

</DataTemplate>

</sdk:DataGridTemplateColumn.CellTemplate>

</sdk:DataGridTemplateColumn>

<sdk:DataGridTextColumn x:Name="loginColumn" Binding="{Binding Path=Login}" Header="Login" Width="SizeToHeader" />

<sdk:DataGridTextColumn x:Name="nameColumn" Binding="{Binding Path=Name}" Header="Name" Width="SizeToHeader" />

</sdk:DataGrid.Columns>

</sdk:DataGrid>

</my2:BusyIndicator>

<Button Content="Remove selected" Height="23" Name="btnRemove" Width="120" HorizontalAlignment="Left" Margin="10,10,0,0" Click="btnRemove_Click" />

<my2:CustomDataForm x:Name="userDataForm" Margin="0,10,0,0" CurrentItem="{Binding ElementName=userDataGrid, Path=SelectedItem, Mode=TwoWay}"

CommandButtonsVisibility="All" AutoEdit="False" AutoCommit="True" ItemsSource="{Binding ElementName=userDomainDataSource, Path=Data}" EditEnded="userDataForm_EditEnded"/>

</StackPanel>

<riaControls:DomainDataSource AutoLoad="True" d:DesignData="{d:DesignInstance my1:User, CreateList=true}" Height="0" LoadedData="userDomainDataSource_LoadedData" Name="userDomainDataSource" QueryName="GetUsersQuery" Width="0" >

<riaControls:DomainDataSource.DomainContext>

<my:MyApplicationDomainContext />

</riaControls:DomainDataSource.DomainContext>

</riaControls:DomainDataSource>

</StackPanel>

</ScrollViewer>

</Grid>

</navigation:Page>

Модифицируем наш класс с метаданными:

Как уже упомяналось, поле с паролем не должно быть заполнено. Для этого нужно пометить его аттрибутом Exclude.

[Exclude]

public string Password { get; set; }

Нам понадобится дополнительное поле с подтверждением пароля, с другой стороны оно никак не должно быть связано с полями базы данных.

Также отметим сейчас, что мы как обычно будем сохранять в базу данных не просто текст пароля, а его хэш-функцию. Получается, что нам нужно дополнительное поле NewPassword, котороые мы позже будем использовать для сохранения пароля в базу данных.

Добавим новые поля наш класс User

[DataMember]

[CustomValidation(typeof(CompareValidator), "ValidatePassword")]

[Display(Order = 3, Name = "Password Confirmation")]

public string PasswordConfirmation { get; set; }

[DataMember]

[Display(Order = 2, Name = "New Password")]

public string NewPassword { get; set; }

Тут мы добавили новый аттрибут CustomValidation, он нужен нам для сравнения паролей.

CompareValidator.shared.cs

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.ComponentModel.DataAnnotations;

namespace MyApplication.BLL

{

public class CompareValidator

{

public static ValidationResult ValidatePassword(string password, ValidationContext context)

{

var user = context.ObjectInstance as User;

if (string.IsNullOrWhiteSpace(password) && string.IsNullOrWhiteSpace(user.NewPassword))

return null;

if (user.NewPassword == password)

return null;

return new ValidationResult("Passwords do not match", new string[] { "PasswordConfirmation" });

}

}

}

Также нам нужен метод для проверки логина пользователя. Напомню, что мы хотели сделать логины уникальными в рамках всего приложения.

На клиентской стороне нам не известны пароли всех остальных пользователей, так что нужно сделать запрос к доменному сервису.

Следующий метод осуществляет асинхронную валидацию логина поьзователя:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.ComponentModel.DataAnnotations;

#if SILVERLIGHT

using System.ServiceModel.DomainServices.Client;

using MyApplication.Web.Services;

#endif

namespace MyApplication.BLL

{

public class CompareValidator

{

public static ValidationResult IsLoginAvailable(string login, ValidationContext context)

{

var user = context.ObjectInstance as User;

#if SILVERLIGHT

MyApplicationDomainContext ctx = new MyApplicationDomainContext();

InvokeOperation<bool> availability = ctx.IsLoginAvailable(login, user.ID);

availability.Completed += (s, e) =>

{

if (!availability.HasError && !availability.Value)

{

user.ValidationErrors.Add(new ValidationResult("Username already taken", new string[] { context.MemberName }));

}

};

#endif

return ValidationResult.Success;

}

public static ValidationResult ValidatePassword(string password, ValidationContext context)

{

var user = context.ObjectInstance as User;

if (string.IsNullOrWhiteSpace(password) && string.IsNullOrWhiteSpace(user.NewPassword))

return null;

if (user.NewPassword == password)

return null;

return new ValidationResult("Passwords do not match", new string[] { "PasswordConfirmation" });

}

}

}

Также нам нужно добавить новый метод в сервис:

public IQueryable<User> GetUsers()

{

return this.ObjectContext.Users;

}

public User GetUserById(int id)

{

return this.ObjectContext.Users.FirstOrDefault(p => p.ID == id);

}

public User GetUserByLogin(string login)

{

return this.ObjectContext.Users.FirstOrDefault(p => p.Login == login);

}

public bool IsLoginAvailable(string login, int id)

{

var user = this.ObjectContext.Users.Where(p => p.Login == login && p.ID != id).FirstOrDefault();

return user == null;

}

public void InsertUser(User user)

{

user.Password = Hasher.GetMd5Hash(user.NewPassword);

if ((user.EntityState != EntityState.Detached))

{

this.ObjectContext.ObjectStateManager.ChangeObjectState(user, EntityState.Added);

}

else

{

this.ObjectContext.Users.AddObject(user);

}

}

public void UpdateUser(User currentUser)

{

if (!string.IsNullOrWhiteSpace(currentUser.NewPassword))

{

currentUser.Password = Hasher.GetMd5Hash(currentUser.NewPassword);

}

this.ObjectContext.Users.AttachAsModified(currentUser, this.ChangeSet.GetOriginal(currentUser));

}

public void DeleteUser(User user)

{

if ((user.EntityState == EntityState.Detached))

{

this.ObjectContext.Users.Attach(user);

}

this.ObjectContext.Users.DeleteObject(user);

}

Также нужно обратить внимание на метод UpdateUser. Он использует поле NewPassword для определения того, что ввел пользователь в поле с паролем.

Нужно проверить, не пустое ли это поле, а если это не так, то нужно получить хэш-функцию от этого текста и сохранить ее в базу данных.

public void UpdateUser(User currentUser)

{

if (!string.IsNullOrWhiteSpace(currentUser.NewPassword))

{

currentUser.Password = Hasher.GetMd5Hash(currentUser.NewPassword);

}

this.ObjectContext.Users.AttachAsModified(currentUser, this.ChangeSet.GetOriginal(currentUser));

}

Класс Hasher у меня выглядит вот так:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Security.Cryptography;

namespace MyApplication.BLL

{

public class Hasher

{

public static string GetMd5Hash(string input)

{

MD5 md5Hasher = MD5.Create();

byte[] data = md5Hasher.ComputeHash(Encoding.Default.GetBytes(input));

StringBuilder sBuilder = new StringBuilder();

for (int i = 0; i < data.Length; i++)

{

sBuilder.Append(data[i].ToString("x2"));

}

return sBuilder.ToString();

}

public static bool VerifyMd5Hash(string input, string hash)

{

string hashOfInput = GetMd5Hash(input);

StringComparer comparer = StringComparer.OrdinalIgnoreCase;

if (comparer.Compare(hashOfInput, hash) == 0)

{

return true;

}

else

{

return false;

}

}

}

}

Теперь полный код метаданных для пользователя будет выглядеть так:

// The MetadataTypeAttribute identifies UserMetadata as the class

// that carries additional metadata for the User class.

[MetadataTypeAttribute(typeof(User.UserMetadata))]

public partial class User

{

// This class allows you to attach custom attributes to properties

// of the User class.

//

// For example, the following marks the Xyz property as a

// required property and specifies the format for valid values:

// [Required]

// [RegularExpression("[A-Z][A-Za-z0-9]*")]

// [StringLength(32)]

// public string Xyz { get; set; }

internal sealed class UserMetadata

{

// Metadata classes are not meant to be instantiated.

private UserMetadata()

{

}

[Display(AutoGenerateField = false)]

public int ID { get; set; }

[Display(AutoGenerateField = false)]

public EntityCollection<Loan> Loans { get; set; }

[Display(Order = 1)]

[CustomValidation(typeof(CompareValidator), "IsLoginAvailable")]

public string Login { get; set; }

[Display(Order = 0)]

public string Name { get; set; }

[Exclude]

public string Password { get; set; }

[Display(AutoGenerateField = false)]

public EntityCollection<UserProject> UserProjects { get; set; }

}

[DataMember]

[CustomValidation(typeof(CompareValidator), "ValidatePassword")]

[Display(Order = 3, Name = "Password Confirmation")]

public string PasswordConfirmation { get; set; }

[DataMember]

[Display(Order = 2, Name = "New Password")]

public string NewPassword { get; set; }

[Update]

public void Update(User user)

{

}

}

После этих модификаций страница пользователей должна работать правильно, то есть в соответствии с описанными выше требованиями.

Комментариев нет:

Отправить комментарий