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

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

Крайне важной задачей в любом бизнес-приложении является построение надежной системы аутентификации и авторизации.

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

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

Пользователи также могут иметь глобальную роль Администратора.

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

USE [MyApplication]

GO

SET ANSI_NULLS ON

GO

SET QUOTED_IDENTIFIER ON

GO

CREATE TABLE [dbo].[Users](

[ID] [int] IDENTITY(1,1) NOT NULL,

[Login] [nvarchar](50) NOT NULL,

[Password] [nvarchar](50) NOT NULL,

[Name] [nvarchar](50) NULL,

[IsAdmin] [bit] NOT NULL,

CONSTRAINT [PK_Users] PRIMARY KEY CLUSTERED

(

[ID] ASC

)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY],

CONSTRAINT [IX_Users] UNIQUE NONCLUSTERED

(

[Login] ASC

)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

) ON [PRIMARY]

GO

ALTER TABLE [dbo].[Users] ADD CONSTRAINT [DF_Users_IsAdmin] DEFAULT ((0)) FOR [IsAdmin]

GO

USE [MyApplication]

GO

SET ANSI_NULLS ON

GO

SET QUOTED_IDENTIFIER ON

GO

CREATE TABLE [dbo].[Projects](

[ID] [int] IDENTITY(1,1) NOT NULL,

[Name] [nvarchar](100) NOT NULL,

[BankName] [nvarchar](100) NULL,

[IsDeleted] [bit] NOT NULL,

[GUID] [uniqueidentifier] NOT NULL,

CONSTRAINT [PK_Projects] PRIMARY KEY CLUSTERED

(

[ID] ASC

)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

) ON [PRIMARY]

GO

ALTER TABLE [dbo].[Projects] ADD CONSTRAINT [DF_Projects_IsDeleted] DEFAULT ((0)) FOR [IsDeleted]

GO

ALTER TABLE [dbo].[Projects] ADD CONSTRAINT [DF_Projects_GUID] DEFAULT (newid()) FOR [GUID]

GO

USE [MyApplication]

GO

SET ANSI_NULLS ON

GO

SET QUOTED_IDENTIFIER ON

GO

CREATE TABLE [dbo].[ProjectRoles](

[ID] [int] IDENTITY(1,1) NOT NULL,

[Name] [nvarchar](50) NOT NULL,

CONSTRAINT [PK_ProjectRoles] PRIMARY KEY CLUSTERED

(

[ID] ASC

)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

) ON [PRIMARY]

GO

INSERT INTO [MyApplication].[dbo].[ProjectRoles] ([Name]) VALUES ('Manager')

GO

INSERT INTO [MyApplication].[dbo].[ProjectRoles] ([Name]) VALUES ('Editor')

GO

INSERT INTO [MyApplication].[dbo].[ProjectRoles] ([Name]) VALUES ('Viewer')

GO

------------------------------------------------------------

USE [MyApplication]

GO

SET ANSI_NULLS ON

GO

SET QUOTED_IDENTIFIER ON

GO

CREATE TABLE [dbo].[UserProjects](

[ID] [int] IDENTITY(1,1) NOT NULL,

[UserID] [int] NOT NULL,

[ProjectID] [int] NOT NULL,

CONSTRAINT [PK_UserProjects] PRIMARY KEY CLUSTERED

(

[ID] ASC

)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

) ON [PRIMARY]

GO

ALTER TABLE [dbo].[UserProjects] WITH CHECK ADD CONSTRAINT [FK_UserProjects_Projects] FOREIGN KEY([ProjectID])

REFERENCES [dbo].[Projects] ([ID])

GO

ALTER TABLE [dbo].[UserProjects] CHECK CONSTRAINT [FK_UserProjects_Projects]

GO

ALTER TABLE [dbo].[UserProjects] WITH CHECK ADD CONSTRAINT [FK_UserProjects_Users] FOREIGN KEY([UserID])

REFERENCES [dbo].[Users] ([ID])

GO

ALTER TABLE [dbo].[UserProjects] CHECK CONSTRAINT [FK_UserProjects_Users]

GO

Теперь можно перейти к системе аутентификации. Можно заметить, что проект Silverlight Business Application для Silverlight 4 сразу создает несколько файлов, которые связаны с системой аутентификации.

В папке Model хранится файл User.cs, в папке Model/Shared хранится файл user.shared.cs. А в папке Services находится AuthenticationService.cs.

Изначально файлы содержат мало интересного, но стоит их рассмотреть:

User.cs содержит описание класса User, который будет использоваться системой аутентификации, поля класса из этого файла могут быть использованы из Silverlight проекта.

Позже мы добавим сюда несколько полезных свойств.

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; }

}

}

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.cs содержит, пожалуй, самый важный класс – AuthenticationService. Это и есть сервер для аутентификации. Пока что в нем нет никаких методов, но, поскольку он наследуется от уже готового класса, какая-то работа в нем все равно производится. Это очевидно посколько уже сейчас можно зарегистрироваться и залогиниться в приложение.

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>

{

}

}

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

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

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 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);

}

// TODO:

// Consider constraining the results of your query method. If you need additional input you can

// add parameters to this method or create additional query methods with different names.

// To support paging you will need to add ordering to the 'UserProjects' query.

public IQueryable<UserProject> GetUserProjects()

{

return this.ObjectContext.UserProjects;

}

public void InsertUserProject(UserProject userProject)

{

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

{

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

}

else

{

this.ObjectContext.UserProjects.AddObject(userProject);

}

}

public void UpdateUserProject(UserProject currentUserProject)

{

this.ObjectContext.UserProjects.AttachAsModified(currentUserProject, this.ChangeSet.GetOriginal(currentUserProject));

}

public void DeleteUserProject(UserProject userProject)

{

if ((userProject.EntityState == EntityState.Detached))

{

this.ObjectContext.UserProjects.Attach(userProject);

}

this.ObjectContext.UserProjects.DeleteObject(userProject);

}

// TODO:

// Consider constraining the results of your query method. If you need additional input you can

// add parameters to this method or create additional query methods with different names.

// To support paging you will need to add ordering to the 'Projects' query.

public IQueryable<Project> GetProjects()

{

var user = this.ObjectContext.Users.FirstOrDefault(p => p.Login == this.ServiceContext.User.Identity.Name);

if (user == null)

{

Logger.Log.ErrorFormat("No user {0} found", this.ServiceContext.User.Identity.Name);

return null;

}

if (user.IsAdmin)

{

return this.ObjectContext.Projects.Where(p => !p.IsDeleted);

}

return from up in this.ObjectContext.UserProjects.Include("Project")

where up.UserID == user.ID && !up.Project.IsDeleted

select up.Project;

}

public IQueryable<Project> GetProjectsManagedByUser()

{

var user = this.ObjectContext.Users.FirstOrDefault(p => p.Login == this.ServiceContext.User.Identity.Name);

if (user == null)

{

Logger.Log.ErrorFormat("No user {0} found", this.ServiceContext.User.Identity.Name);

return null;

}

if (user.IsAdmin)

{

return this.ObjectContext.Projects.Where(p => !p.IsDeleted);

}

return from up in this.ObjectContext.UserProjects.Include("Project").Include("ProjectRole")

where up.UserID == user.ID && !up.Project.IsDeleted && up.ProjectRole.Name == "Manager"

select up.Project;

}

public void InsertProject(Project project)

{

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

{

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

}

else

{

this.ObjectContext.Projects.AddObject(project);

}

}

public void UpdateProject(Project currentProject)

{

this.ObjectContext.Projects.AttachAsModified(currentProject, this.ChangeSet.GetOriginal(currentProject));

}

public void DeleteProject(Project project)

{

if ((project.EntityState == EntityState.Detached))

{

this.ObjectContext.Projects.Attach(project);

}

project.IsDeleted = true;

this.ObjectContext.SaveChanges();

}

Мы добавили несколько интересных запросов помимо стандартных методов Get, Update, Insert, Delete. Также стоит обратить внимание на то, что уже сейчас мы возвращаем не все проекты, а только те, которые доступны пользователю.

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

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