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

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

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

Для этого переопределим несколько методов в класса AuthenticationService.

Нам нужно переопределить методы ValidateUser и GetAuthenticatedUser. ValidateUser должен вернуть true, если пользователь ввел правильные login и пароль.

GetAuthenticatedUser должен вернуть объект типа User (тот, который создан автоматически).

AuthenticationService.cs

namespace MyApplication.Web

{

using System.Security.Authentication;

using System.ServiceModel.DomainServices.Hosting;

using System.ServiceModel.DomainServices.Server;

using System.ServiceModel.DomainServices.Server.ApplicationServices;

using System.Threading;

using System.Security.Principal;

using HLUser = MyApplication.BLL.User;

using MyApplication.BLL;

using MyApplication.Web.Services;

/// <summary>

/// RIA Services DomainService responsible for authenticating users when

/// they try to log on to the application.

///

/// Most of the functionality is already provided by the base class

/// AuthenticationBase

/// </summary>

[EnableClientAccess]

public class AuthenticationService : AuthenticationBase<User>

{

protected override bool ValidateUser(string username, string password)

{

return false;

}

protected override User GetAuthenticatedUser(IPrincipal pricipal)

{

return null;

}

}

}

Пока мы не может вернуть ничего разумного. Расширим наши классы User. Добавим несколько новых свойств, которые нам пригодятся в дальнейшем.

User.cs

namespace MyApplication.Web

{

using System.Runtime.Serialization;

using System.ServiceModel.DomainServices.Server.ApplicationServices;

using System.Collections.Generic;

/// <summary>

/// Class containing information about the authenticated user.

/// </summary>

public partial class User : UserBase

{

//// NOTE: Profile properties can be added for use in Silverlight application.

//// To enable profiles, edit the appropriate section of web.config file.

////

//// public string MyProfileProperty { get; set; }

/// <summary>

/// Gets and sets the friendly name of the user.

/// </summary>

public string FriendlyName { get; set; }

public bool IsAdmin { get; internal set; }

public bool HasProjects { get; internal set; }

public bool HasManagedProjects { get; internal set; }

public int ID { get; set; }

public List<int> ManagedProjects { get; set; }

}

}

User.cs

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

namespace MyApplication.BLL

{

public partial class User

{

public static bool Authenticate(string userName, string password)

{

using (var ctx = new MyApplicationEntities())

{

var user = ctx.Users.Where(p => p.Login == userName).FirstOrDefault();

if (user != null && Hasher.VerifyMd5Hash(password, user.Password))

{

return true;

}

}

return false;

}

public static bool IsUserExist(string userName)

{

using (var ctx = new MyApplicationEntities())

{

var user = ctx.Users.Where(p => p.Login == userName).FirstOrDefault();

if (user != null)

{

return true;

}

}

return false;

}

public static string GetUserName(string userName)

{

using (var ctx = new MyApplicationEntities())

{

var user = ctx.Users.Where(p => p.Login == userName).FirstOrDefault();

if (user != null)

{

return user.Name;

}

}

return null;

}

public static User GetUserByLogin(string userName)

{

using (var ctx = new MyApplicationEntities())

{

var user = ctx.Users.Where(p => p.Login == userName).FirstOrDefault();

if (user != null)

{

return user;

}

}

return null;

}

public int GetProjectsCount()

{

using (var ctx = new MyApplicationEntities())

{

return ctx.UserProjects.Where(p => p.UserID == ID).Count();

}

}

public int GetManagedProjectsCount()

{

using (var ctx = new MyApplicationEntities())

{

return ctx.UserProjects.Include("ProjectRole").Where(p => p.UserID == ID && p.ProjectRole.Name == "Manager").Count();

}

}

public List<int> GetManagedProjects()

{

using (var ctx = new MyApplicationEntities())

{

return (from pu in ctx.UserProjects.Include("ProjectRole")

where pu.UserID == ID && pu.ProjectRole.Name == "Manager"

select pu.ProjectID).ToList();

}

}

public static string GetProjectRole(int userId, int projectId)

{

using (var ctx = new MyApplicationEntities())

{

return (from up in ctx.UserProjects.Include("ProjectRole")

where up.UserID == userId && up.ProjectID == projectId

select up.ProjectRole.Name).FirstOrDefault();

}

}

}

}

User.shared.cs

using MyApplication.BLL;

namespace MyApplication.Web

{

/// <summary>

/// Partial class extending the User type that adds shared properties and methods

/// that will be available both to the server app and the client app

/// </summary>

public partial class User

{

/// <summary>

/// Returns the user display name, which by default is its Friendly Name,

/// and if that is not set, its User Name

/// </summary>

public string DisplayName

{

get

{

if (!string.IsNullOrEmpty(this.FriendlyName))

{

return this.FriendlyName;

}

else

{

return this.Name;

}

}

}

}

}

Теперь, когда нужные свойства добавлены можно переходить к реализации класса AuthenticationService. Стоит обратить внимание на то, что сейчас мы можем не заполнять поля класса User в методе GetAuthenticatedUser. С другой стороны, нужно понимать, что, поскольку Silverlight – клиентская технология, нам придется подгружать все недостающие данные асинхронно во время работы приложения.

Я не хочу этого делать, так что я заполню все поля сразу. Из-за этого появится следующая проблема – данные в полях User будут действительны до момента нового вызова GetAuthenticatedUser, то есть до момента нового входа пользователя в систему.

AuthenticationService.cs

namespace MyApplication.Web

{

using System.Security.Authentication;

using System.ServiceModel.DomainServices.Hosting;

using System.ServiceModel.DomainServices.Server;

using System.ServiceModel.DomainServices.Server.ApplicationServices;

using System.Threading;

using System.Security.Principal;

using HLUser = MyApplication.BLL.User;

using MyApplication.BLL;

using MyApplication.Web.Services;

/// <summary>

/// RIA Services DomainService responsible for authenticating users when

/// they try to log on to the application.

///

/// Most of the functionality is already provided by the base class

/// AuthenticationBase

/// </summary>

[EnableClientAccess]

public class AuthenticationService : AuthenticationBase<User>

{

protected override bool ValidateUser(string username, string password)

{

return HLUser.Authenticate(username, password);

}

protected override User GetAuthenticatedUser(IPrincipal pricipal)

{

User user = null;

if (HLUser.IsUserExist(pricipal.Identity.Name))

{

user = new User();

var dbUser = HLUser.GetUserByLogin(pricipal.Identity.Name);

if (dbUser != null)

{

user.Name = dbUser.Name;

user.IsAdmin = dbUser.IsAdmin;

user.HasProjects = dbUser.GetProjectsCount() > 0;

user.HasManagedProjects = dbUser.GetManagedProjectsCount() > 0;

user.ID = dbUser.ID;

user.ManagedProjects = dbUser.GetManagedProjects();

}

}

return user;

}

}

}

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

Для этого изменим страницу LoginForm следующим образом:

<StackPanel

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

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

xmlns:local="clr-namespace:MyApplication.Controls"

xmlns:login="clr-namespace:MyApplication.LoginUI"

x:Class="MyApplication.LoginUI.LoginForm"

KeyDown="LoginForm_KeyDown"

xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

mc:Ignorable="d"

d:DataContext="{d:DesignInstance Type=login:LoginInfo}">

<Grid>

<Grid.RowDefinitions>

<RowDefinition Height="Auto"/>

<RowDefinition/>

</Grid.RowDefinitions>

<local:BusyIndicator x:Name="busyIndicator" BusyContent="{Binding Path=ApplicationStrings.BusyIndicatorLoggingIn, Source={StaticResource ResourceWrapper}}"

IsBusy="{Binding IsLoggingIn}">

<StackPanel Orientation="Vertical">

<local:CustomDataForm x:Name="loginForm"

Padding="10,0,10,0"

CurrentItem="{Binding}"

IsEnabled="{Binding IsLoggingIn, Converter={StaticResource NotOperatorValueConverter}}"

AutoEdit="True" CommandButtonsVisibility="None" HeaderVisibility="Collapsed"

AutoGeneratingField="LoginForm_AutoGeneratingField"

Style="{StaticResource LoginDataFormStyle}" />

</StackPanel>

</local:BusyIndicator>

<StackPanel Grid.Row="1" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,0,10,0">

<Button x:Name="loginButton" Content="{Binding Path=ApplicationStrings.OKButton, Source={StaticResource ResourceWrapper}}" Click="LoginButton_Click" Style="{StaticResource RegisterLoginButtonStyle}" IsEnabled="{Binding Path=CanLogIn}" />

<Button x:Name="loginCancel" Content="{Binding Path=ApplicationStrings.CancelButton, Source={StaticResource ResourceWrapper}}" Click="CancelButton_Click" Style="{StaticResource RegisterLoginButtonStyle}" />

</StackPanel>

<!--<StackPanel Grid.Row="1" Grid.Column="0" Style="{StaticResource RegisterLoginLinkPanelStyle}">

<TextBlock Text="{Binding Path=ApplicationStrings.NotRegisteredYetLabel, Source={StaticResource ResourceWrapper}}" Style="{StaticResource CommentStyle}"/>

<HyperlinkButton x:Name="registerNow" Content="{Binding Path=ApplicationStrings.RegisterNowButton, Source={StaticResource ResourceWrapper}}" Click="RegisterNow_Click" IsEnabled="{Binding IsLoggingIn, Converter={StaticResource NotOperatorValueConverter}}" />

</StackPanel>-->

</Grid>

</StackPanel>

Теперь мы можем использовать все поля, которые были описаны в классе User в нашем Silverlight приложении.

Также имеет смысл подписаться на события LoggedIn и LoggedOut на странице Main, чтобы можно было упралять пользовательским интерфейсом.

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

MainPage.xaml.cs

namespace MyApplication

{

using System.Windows;

using System.Windows.Controls;

using System.Windows.Navigation;

using MyApplication.LoginUI;

using System;

/// <summary>

/// <see cref="UserControl"/> class providing the main UI for the application.

/// </summary>

public partial class MainPage : UserControl

{

/// <summary>

/// Creates a new <see cref="MainPage"/> instance.

/// </summary>

public MainPage()

{

InitializeComponent();

this.loginContainer.Child = new LoginStatus();

WebContext.Current.Authentication.LoggedIn += new System.EventHandler<System.ServiceModel.DomainServices.Client.ApplicationServices.AuthenticationEventArgs>(Authentication_LoggedIn);

WebContext.Current.Authentication.LoggedOut += new System.EventHandler<System.ServiceModel.DomainServices.Client.ApplicationServices.AuthenticationEventArgs>(Authentication_LoggedOut);

}

void Authentication_LoggedOut(object sender, System.ServiceModel.DomainServices.Client.ApplicationServices.AuthenticationEventArgs e)

{

// скрыть станицы, недоступные для незарегистрированных пользователей

this.ContentFrame.Navigate(new Uri("/Home", UriKind.Relative));

}

void Authentication_LoggedIn(object sender, System.ServiceModel.DomainServices.Client.ApplicationServices.AuthenticationEventArgs e)

{

if (WebContext.Current.User.IsAdmin)

{

// показывать больше страниц

}

}

/// <summary>

/// After the Frame navigates, ensure the <see cref="HyperlinkButton"/> representing the current page is selected

/// </summary>

private void ContentFrame_Navigated(object sender, NavigationEventArgs e)

{

foreach (UIElement child in LinksStackPanel.Children)

{

HyperlinkButton hb = child as HyperlinkButton;

if (hb != null && hb.NavigateUri != null)

{

if (hb.NavigateUri.ToString().Equals(e.Uri.ToString()))

{

VisualStateManager.GoToState(hb, "ActiveLink", true);

}

else

{

VisualStateManager.GoToState(hb, "InactiveLink", true);

}

}

}

}

/// <summary>

/// If an error occurs during navigation, show an error window

/// </summary>

private void ContentFrame_NavigationFailed(object sender, NavigationFailedEventArgs e)

{

e.Handled = true;

ErrorWindow.CreateNew(e.Exception);

}

}

}

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

2 комментария:

  1. Здравствуйте!
    Начал разбираться с аутентификацией в Silverlight по вашим статьям.
    По умолчанию бизнес приложение SL создается со структурой из 2-х проектов MyApplication и MyApplication.Web. Не пойму что такое MyApplication.BLL, и что там содержится, можите объяснить подробней?

    ОтветитьУдалить
    Ответы
    1. При создании Silverlight проекта вы можете выбрать метод для размещения приложения Silverlight. Есть два метода: 1) Использовать новый отдельный веб-сайт ASP.NET или проект веб-приложения ASP.NET для размещения приложения Silverlight 2)Не использовать новый отдельный веб-сайт ASP.NET. Вместо этого будет создана тестовая страница HTML для размещения данного приложения.
      Если вы выберите первый способ, то у вас создадутся два проекта. Первый проект - это Silverlight проект, который компилируется в .xap файл, а второй проект это и будет ASP сайт, на страничке которого будет запускаться ваш xap файл. ASP проект как раз будет компилироваться в MyApplication.BLL, это сборка вашего сайта, где находятся скомпилированные страницы сайта.

      Удалить