Сообщений 1    Оценка 100        Оценить  
Система Orphus

Аутентификация пользователей между Web-сервером и сервером приложений

Автор: Шеломанов Роман
Источник: RSDN Magazine #5-2003
Опубликовано: 11.04.2004
Исправлено: 10.12.2016
Версия текста: 1.0
Введение
Задача
Архитектура использования
Требования
Дополнительные ограничения
Решение
Архитектура решения
Протоколы взаимодействия
Идея решения
Развертывание
Диаграмма классов
Аутентификация
Авторизация
Поддержка сессий
Заключение

Демонстрационный проект

Введение

В статье рассматривается пример решения задачи по аутентификации и авторизации клиентов Web-сервера на сервере приложений, где под Web-сервером понимается работающее на нем приложение ASP.NET, а под сервером приложений – .NET-приложение. Взаимодействие осуществляется через .NET Remoting (TCP/Binary).

Что есть интересного в рассматриваемом решении:

В статье не рассматриваются вопросы, связанные с защитой канала передачи данных. О шифровании трафика можно прочитать тут: http://msdn.microsoft.com/msdnmag/issues/03/06/netremoting/

Задача

Архитектура использования

На рисунке 1 изображена схема некоторой информационной системы (ИС). ИС состоит из ядра – совокупности серверов приложений, выполняющих бизнес-логику, и Web-интерфейса, расположенного на WEB сервере и предоставляющего доступ к системе через Интернет. В приведенной архитектуре и будет использоваться рассматриваемое решение.


Рисунок 1.

Требования

  1. Web-сервер не должен иметь прямого доступа к ИС.
  2. ИСдолжна определять права пользователей на выполнение того или иного запроса (авторизованные и анонимные запросы).
  3. Права пользователей определяются их ролью в ИС.

Дополнительные ограничения

Все сервисы ИС реализованы на базе платформы 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 Интерфейс для объектов, содержащих состояние сессии.
Таблица 1.

Аутентификация

На рисунке 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.


Эта статья опубликована в журнале RSDN Magazine #5-2003. Информацию о журнале можно найти здесь
    Сообщений 1    Оценка 100        Оценить