Сообщений 1 Оценка 100 Оценить |
Введение Задача Решение Архитектура решения ЗаключениеПротоколы взаимодействия Идея решения Развертывание Диаграмма классов Аутентификация Авторизация Поддержка сессий |
В статье рассматривается пример решения задачи по аутентификации и авторизации клиентов Web-сервера на сервере приложений, где под Web-сервером понимается работающее на нем приложение ASP.NET, а под сервером приложений – .NET-приложение. Взаимодействие осуществляется через .NET Remoting (TCP/Binary).
Что есть интересного в рассматриваемом решении:
В статье не рассматриваются вопросы, связанные с защитой канала передачи данных. О шифровании трафика можно прочитать тут: http://msdn.microsoft.com/msdnmag/issues/03/06/netremoting/
На рисунке 1 изображена схема некоторой информационной системы (ИС). ИС состоит из ядра – совокупности серверов приложений, выполняющих бизнес-логику, и Web-интерфейса, расположенного на WEB сервере и предоставляющего доступ к системе через Интернет. В приведенной архитектуре и будет использоваться рассматриваемое решение.
Рисунок 1.
Все сервисы ИС реализованы на базе платформы MS Windows (не ниже MS Windows 2000).
Серверы приложений системы расположены в пределах одной локальной сети.
В соответствии с требованиями разрабатываем архитектуру, представленную на рисунке 2
Рисунок 2.
Web-сервер – предоставляет доступ к ИС через Интернет посредством Web-интерфейса, который реализуется на технологии ASP.NET.
Сервер приложений – аутентифицирует пользователей, авторизует запросы пользователей, маршрутизирует запросы от Web-сервера к серверам ИС. Реализуются в виде .NET-приложения с возможностью удаленного вызова его методов.
База данных системы – хранит данные ИС.
Серверы приложений системы – совокупность сервисов, реализующих бизнес-логику ИС.
Firewall 1,2 – шлюзы, защищающие ИС от несанкционированного доступа.
На рисунке 3 изображена схема взаимодействия компонентов ИС и протоколы взаимодействия.
Рисунок 3
Интересующий нас участок цепи: Web-сервер – сервер приложений для Web. Мной выбран протокол взаимодействия .NET Remoting через TCP с бинарной сериализацией по причине высокой эффективности этого сочетания по сравнению с HTTP вместе с SOAP.
Идея решения состоит в реализации аутентификации на уровне канальных приемников (ChannelSink), встраиваемых в инфраструктуру канала Remoting на стороне клиента и сервера. Аутентификационная информация передается в заголовках запроса (TransportHeaders), результаты аутентификации передаются в заголовках ответа сервера. Авторизация выполняется с помощью декларативной проверки соответствия роли пользователя.
В случае успешной аутентификации на сервере приложений создается пользовательская сессия, в которой сохраняются пользовательские данные. Другая пользовательская сессия создается на Web-сервере, причем стандартный механизм сессий ASP.NET не используется, поэтому его можно отключить в web.config.
Сессии на сервере приложений и Web-сервере различны по содержанию, так как сервер приложений может хранить обязательные для каждого пользователя объекты, вполне возможно unmanaged (COM). Взаимосвязь между клиентом, Web-сервером и сервером приложений осуществляется по идентификатору сессии.
На рисунке 4 приведена диаграмма развертывания рассматриваемого решения.
Рисунок 4
Решение состоит из трех основных .NET-сборок, обеспечивающих процессы аутентификации, авторизации, поддержку сессий:
SecurityBase – сборка, содержащая общие для Web-сервера и сервера приложений типы и константы.
SecurityClient – сборка, содержащая типы для клиентской части схемы аутентификации и типы, обеспечивающие поддержку сессий на Web-сервере. Устанавливается на Web-сервер.
SecurityServer – сборка, содержащая типы для аутентификации и поддержки сессий на стороне сервера приложений.
Также в пример входит сборка BusinessFacade, содержащая типы, обеспечивающие интерфейс с сервером приложений. На Web-сервер устанавливается сокращенная версия этой сборки, в ней содержатся только сигнатуры методов, без содержания.
На сервере приложений устанавливается полная версия BusinessFacade.
На Web-сервере и сервере приложений настраивается конфигурация Remoting.
На Web-сервере конфигурация содержится в Web.config
<system.runtime.remoting> <application name="SHR"> <client> <wellknown type="RemotingExample.BusinessFacade.SomeSystem, BusinessFacade" url="tcp://localhost:8039/SHR/SomeSystem.rem"/> </client> <channels> <channel ref="tcp client"> <clientProviders> <formatter ref="binary" includeVersions="false"/> <provider type="RemotingExample.Security.ClientChannelSinkProvider, SecurityClient"/> </clientProviders> </channel> </channels> </application> </system.runtime.remoting> |
Не сервере приложений в ConsoleServer.exe.config:
<system.runtime.remoting> <application name="SHR"> <service> <wellknown mode="Singleton" type="RemotingExample.BusinessFacade.SomeSystem, BusinessFacade" objectUri="SomeSystem.rem" /> </service> <channels> <channel name="ServerCnannel" ref="tcp server" port="8039" > <serverProviders> <formatter ref="binary" includeVersions="false"/> <provider type="RemotingExample.Security.ServerChannelSinkProvider, SecurityServer"/> </serverProviders> </channel> </channels> </application> </system.runtime.remoting> |
Инициализация конфигурации Remoting на Web-сервере происходит в методе:
protected void Application_Start(Object sender, EventArgs e) { string configPath = System.IO.Path.Combine(Context.Server. MapPath(Context.Request.ApplicationPath ),"Web.config"); RemotingConfiguration.Configure(configPath); } |
Инициализация на сервере приложений:
RemotingConfiguration.Configure("ConsoleServer.exe.config");
|
На рисунке 5 приведена диаграмма используемых классов, в таблице 1 – краткое описание классов.
Рисунок 5.
Класс | Сборка | Описание |
---|---|---|
ServerSecurityContext | SecurityServer | Содержит пользовательские данные на стороне сервера приложений. |
ServerChannelSinkProvider | SecurityServer | Провайдер канального приемника. Помещает канальный приемник в цепочку серверных канальных приемников. |
ServerChannelSink | SecurityServer | Серверный канальный приемник. Аутентифицирует пользователей. Управляет состоянием сессии. |
SecurityContextContainer | SecurityBase | Контейнер для пользовательских сессий. |
ClientSecurityContext | SecurityClient | Содержит пользовательские данные на стороне Web-сервера. |
ClientChannelSinkProvider | SecurityClient | Провайдер канального приемника на стороне Web- сервера. |
ClientChannelSink | SecurityClient | Канальный приемник на стороне Web- сервера. |
ChannelSinkHeaders | SecurityBase | Содержит названия заголовков аутентификации. |
ISecurityContext | SecurityBase | Интерфейс для объектов, содержащих состояние сессии. |
На рисунке 6 изображен сценарий первичной аутентификации пользователя в ИС.
Рисунок 6.
Пользователь вводит логин и пароль в Web-форме. Обработчик отправки формы пытается выполнить аутентификацию:
// Создаем контекст для аутентификации. // Цель: привязать к текущему потоку выполнения аутентификационные данные, // чтобы иметь к ним доступ из клиентского канального приемника ClientSecurityContext context = new ClientSecurityContext(tbName.Text,tbPassword.Text); try { // Обращаемся к серверу приложений userData = (new RemotingExample.BusinessFacade.SomeSystem()). GetUserData(); } catch (System.Security.SecurityException ex) { //Аутентификация на сервере приложений прошла неудачноthis.lblMessage.Text = ex.Message; return; } //Аутентификация удалась//Создаем и записываем пользователю в Cookie билет аутентификации. SetAuthTiket(tbName.Text, context.SessionID); |
Но это только надводная часть айсберга, который называется аутентификацией. Все самое интересное происходит, когда начинают работать механизмы Remoting, а именно – клиентский и серверный канальные приемники.
Когда мы создаем контекст для аутентификации, мы готовим тем самым поле деятельности для клиентского канального приемника – ClientChannelSink, который и будет выполнять всю работу по аутентификации клиента на сервере приложений.
После вызова удаленного метода:
userData = (new RemotingExample.BusinessFacade.SomeSystem()).
GetUserData();
|
управление получает клиентский канальный применик ClientChannelSink, а именно его метод :
public void ProcessMessage(IMessage msg, ITransportHeaders requestHeaders, Stream requestStream, out ITransportHeaders responseHeaders, out Stream responseStream) //Вытаскиваем контекст запроса ClientSecurityContext context = ClientSecurityContext.Current; //Проверяем, аутентифицирован ли контекст switch (context.AuthState) { case AuthenticationStates.Authenticated: //Если аутентифицирован, то добавляем в заголовки запроса к серверу //приложений SID контекста requestHeaders[ChannelSinkHeaders.SID_HEADER] = context.SessionID; break; default : //Иначе добавляем логин и пароль requestHeaders[ChannelSinkHeaders.USER_NAME_HEADER] = context.Login; requestHeaders[ChannelSinkHeaders.PASSWORD_HEADER] = сontext.Password; break; } //Выполняем запрос на сервер приложений _nextSink.ProcessMessage(msg, requestHeaders, requestStream, out responseHeaders, out responseStream); AuthenticationStates serverAuth = AuthenticationStates.NotAuthenticated; //Получаем заголовок состояния аутентификации сервера приложенийstring serverAuthHeader = (string)responseHeaders[ChannelSinkHeaders.AUTH_STATE_HEADER]; //Анализируем полученный заголовокswitch (serverAuth) { //Контекст аутентифицирован на сервере приложенийcase AuthenticationStates.Authenticated: if (context.AuthState != AuthenticationStates.Authenticated) { //На Web-сервере контекст еще не аутентифицирован//Создаем Principal объект для контекстаstring roles = responseHeaders[ChannelSinkHeaders.ROLES_HEADER].ToString(); string[] rolesArr = roles.Split(newchar[]{','}); IIdentity identity=new GenericIdentity(ClientSecurityContext.Current.Login); IPrincipal userPrincipal = new GenericPrincipal(identity,rolesArr); //Аутентифицируем контекст context.SetAuthState(AuthenticationStates.Authenticated); context.SetPrincipal(userPrincipal); //Устанавливаем идентификатор сессии context.SetSessionID(responseHeaders[ChannelSinkHeaders.SID_HEADER]. ToString()); //Создаем сессию на Web-сервере SecurityContextContainer.GetInstance()[context.SessionID] = context; } break; } |
Во время выполнения запроса
_nextSink.ProcessMessage(msg, requestHeaders, requestStream, out responseHeaders, out responseStream); |
управление передается на сервер приложений, где в работу первым делом включается серверный канальный приемник ServerChannelSink, а именно, его метод
ServerProcessing ProcessMessage(IServerChannelSinkStack sinkStack,IMessage requestMsg, ITransportHeaders requestHeaders, Stream requestStream, out IMessage responseMsg, out ITransportHeaders responseHeaders, out Stream responseStream) //Получаем идентификатор сессии из заголовков запросаstring SID = (string)requestHeaders[ChannelSinkHeaders.SID_HEADER]; ServerSecurityContext context = null; if (SID == null) //Если SID отсутствует, пробуем аутентифицировать запрос { //Пробуем получить логин и пароль из заголовков запросаstring userName = (string)requestHeaders[ChannelSinkHeaders.USER_NAME_HEADER]; string password = (string)requestHeaders[ChannelSinkHeaders.PASSWORD_HEADER]; AuthenticationStates authResult = AuthenticationStates.NotAuthenticated; if ((userName != null) && (password != null)) { //Если логин и пароль найдены, выполняем аутентификациюstring roles; authResult = Authenticate(userName,password, out roles); switch (authResult) { case AuthenticationStates.Authenticated: //Аутентификация прошла успешно//Создаем серверный контекст для пользователя context = new ServerSecurityContext(userName,roles); context.SetAuthState(AuthenticationStates.Authenticated); //Создаем сессию на сервере приложений SecurityContextContainer.GetInstance()[context.SessionID]=context; break; default: //Аутентификация не удалась. thrownew System.Security.SecurityException("Authentication failed"); } } } //Если SID существует в заголовках запроса, то авторизируем запрос //по этому SIDelse { //Воостанавливаем сессию по ее идентификатору context = (ServerSecurityContext)SecurityContextContainer.GetInstance()[SID]; if (context == null) { thrownew System.Security.SecurityException("Authorization failed"); } else { //Ассоциируем текущий контекст с полученным по SID ServerSecurityContext.Current = context; } } System.Security.Principal.IPrincipal orginalPrincipal = Thread.CurrentPrincipal; if (ServerSecurityContext.Current != null) { //Ассоциируем Principal текущего потока с Principal объектом контекста Thread.CurrentPrincipal = ServerSecurityContext.Current.Principal; } sinkStack.Push(this, null); ServerProcessing processing; //Выполняем полученный запрос на сервере приложений processing = _nextSink.ProcessMessage(sinkStack, requestMsg, requestHeaders, requestStream ,out responseMsg, out responseHeaders, out responseStream); sinkStack.Pop(this); //Восстанавливаем Principal объект для потока Thread.CurrentPrincipal = orginalPrincipal; AuthenticationStates serverAuthState = AuthenticationStates.NotAuthenticated; if (ServerSecurityContext.Current != null) serverAuthState = context.AuthState; responseHeaders = new TransportHeaders(); switch (serverAuthState) { case AuthenticationStates.Authenticated: //Если аутентификация прошла успешно, //выставляем заголовки для отправки на Web-сервер responseHeaders[ChannelSinkHeaders.AUTH_STATE_HEADER] = AuthenticationStates.Authenticated; responseHeaders[ChannelSinkHeaders.SID_HEADER] = ServerSecurityContext.Current.SessionID; responseHeaders[ChannelSinkHeaders.ROLES_HEADER] = ServerSecurityContext.Current.Roles; break; default : responseHeaders[ChannelSinkHeaders.AUTH_STATE_HEADER]=serverAuthState; break; } //Очищаем текущий контекст ServerSecurityContext.Current = null; //Возвращаем управление и результаты запроса в клиентский канальный приемникreturn ServerProcessing.Complete; |
Теперь пользователь аутентифицирован и может работать с ИС. Для этого каждый его последующий запрос должен идентифицироваться на основе ранее проведенной аутентификации, то есть сначала Web-сервер, а потом и сервер приложений должны распознать пользователя и восстановить контекст его работы с ИС.
Сценарий процесса приведен на рисунке 7.
Рисунок 7.
Первым делом в запросе пользователя к Web-серверу ищется специализированное cookie – билет аутентификации (authTicket). Этот билет содержит некоторую информацию о пользователе и говорит Web-серверу о том, что пользователь уже аутентифицирован. Для активизации этой функциональности на Web-сервере необходимо включить Forms Authentication.
Идентификация пользователя происходит в методе AuthenticateRequest Web-сервера. Этот метод вызывается сервером в начале обработки каждого запроса.
//Получаем из Cookies билет аутентификации string cookieName = FormsAuthentication.FormsCookieName; HttpCookie authCookie = Context.Request.Cookies[cookieName]; System.Web.Security.FormsAuthenticationTicket authTicket = null; try { authTicket = System.Web.Security.FormsAuthentication.Decrypt(authCookie.Value); } catch(Exception) { return; } if (null == authTicket) { return; } //Получаем идентификатор сессии пользователя из билета аутентификацииstring sessionID = authTicket.UserData; ClientSecurityContext securityContext = null; //Восстанавливаем сессию пользователя по ее идентификатору securityContext = (ClientSecurityContext)SecurityContextContainer.GetInstance()[sessionID]; if (securityContext != null) { ClientSecurityContext.Current = securityContext; //Ассоциируем Principal объект с текущим потоком Context.User = securityContext.User; } else { System.Web.Security.FormsAuthentication.SignOut(); Response.Redirect("logout.aspx"); } |
Теперь пользователь аутентифицирован на стороне Web-сервера и может выполнять программы, реализующие логику Web-приложения. В процессе выполнения этих программ Web-сервер может обращаться к серверу приложений. Естественно, что и там запрос пользователя необходимо аутентифицировать. Для этого на сервер приложений передается SID, который извлечен из билета аутентификации Web-сервером. По SID происходит аутентификация и восстанавливается пользовательская сессия на сервере приложений.
Функциональность авторизации реализуется с помощью атрибута System.Security.Permissions.PrincipalPermissionAttribute, устанавливаемого перед соответствующими методами фасадного объекта (BusinessFacade):
[PrincipalPermissionAttribute(SecurityAction.Demand, Authenticated=true, Role = "Admin")] publicvoid DoAdminWork (string arg) { Console.WriteLine(DateTime.Now.ToString()+": Doing Admin work: " + arg); } |
Осуществляется с помощью объектов ServerSecurityContext, SecurityContextContainer, ClientSecurityContext на клиентской и серверной сторонах. Инициализация сессии происходит в методах AuthenticateRequest для Web-сервера и в ProcessMessage канального приемника для сервера приложений. Объекты ISecurityContext(ServerSecurityContext, ClientSecurityContext), содержащие состояние сессии, хранятся в коллекции SecurityContextContainer. Ключом к сессии является SID (идентификатор сессии). При инициализации сессия извлекается из коллекции(SecurityContextContainer) и с помощью статического метода Current ассоциируется с текущим потоком выполнения.
public static ClientSecurityContext Current { get { ClientSecurityContext currentContext = (ClientSecurityContext)System. Runtime.Remoting.Messaging.CallContext. GetData("ClientSecurityContext"); if (currentContext != null) { currentContext.lastActivity = DateTime.Now; } return currentContext; } set { if (value != null) { value.lastActivity = DateTime.Now; } System.Runtime.Remoting.Messaging. CallContext.SetData("ClientSecurityContext", value); } } |
После инициализации сессии ее состояние доступно в любом месте кода.
[PrincipalPermissionAttribute(SecurityAction.Demand, Authenticated=true)] publicstring GetUserData() { Console.WriteLine("GetUserData " + Security.ServerSecurityContext.Current.Login); } |
Главное – проставить для этого ссылки на SecurityBase и SecurityServer(SecurityClient).
Тестовое приложение WebCl (рисунок 8) демонстрирует возможности описанного решения. Это приложение, впрочем, как и все решение, прилагается к этой статье в виде проекта в формате Visual Studio .Net 2003.
Рисунок 8
Приведенный пример может быть расширен. Например, результатом аутентификации, помимо сообщения о ее успешности или неуспешности, может стать требование сменить пароль.
Можно организовать проверку – «один пользователь – одна сессия». Можно добавить шифрование трафика. Свойство Items объектов IsecurityContext может служить контейнером для сохранения различных объектов в сессии пользователя. Путем небольшой переработки клиентской части, это решение можно адаптировать для Windows Forms-приложений. В общем, поле для деятельности большое.
Если у кого возникнут вопросы, или идеи и замечания по улучшению описанного механизма, пишите sun_shef@msn.com.
Сообщений 1 Оценка 100 Оценить |