Крайне важной задачей в любом бизнес-приложении является построение надежной системы аутентификации и авторизации.
Сейчас мы рассмотрим, как мне кажется, достаточно жизненный сценарий, в котором информация обо всех пользователях хранится в базе данных.
Также в базе будет храниться информация о связях каждого пользователя с определенным проектом. Причем пользовать может иметь одну из заранее определенных ролей в каждом из проектов.
Пользователи также могут иметь глобальную роль Администратора.
Запросы для создания нужной структуры базы данных могут выглядеть так:
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. Также стоит обратить внимание на то, что уже сейчас мы возвращаем не все проекты, а только те, которые доступны пользователю.
Комментариев нет:
Отправить комментарий