Logo Море(!) аналитической информации!
IT-консалтинг Software Engineering Программирование СУБД Безопасность Internet Сети Операционные системы Hardware
Скидка до 20% на услуги дата-центра. Аренда серверной стойки. Colocation от 1U!

Миграция в облако #SotelCloud. Виртуальный сервер в облаке. Выбрать конфигурацию на сайте!

Виртуальная АТС для вашего бизнеса. Приветственные бонусы для новых клиентов!

Виртуальные VPS серверы в РФ и ЕС

Dedicated серверы в РФ и ЕС

По промокоду CITFORUM скидка 30% на заказ VPS\VDS

VPS/VDS серверы. 30 локаций на выбор

Серверы VPS/VDS с большим диском

Хорошие условия для реселлеров

4VPS.SU - VPS в 17-ти странах

2Gbit/s безлимит

Современное железо!

2004 г.

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

Шеломанов Роман
www.gotdotnet.ru

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

Исходные тексты:

http://www.gotdotnet.com/Community/UserSamples/Details.aspx?SampleGuid=DA54BDF3-B29C-41CB-9517-60705D156D18

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

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

  • Использование серверных и клиентских специализированных канальных приемников.
  • Организация сессий на стороне сервера приложения и стороне Web-сервера
  • Установление зависимости между клиентом и его сессиями на Web-сервере и сервере приложения.
  • Организация авторизации без добавления дополнительных аргументов в методы сервера приложения.
  • "Протаскивание" пользовательских данных через все методы в потоке без добавления дополнительных аргументов в методы.

В статье не рассматриваются вопросы, связанные с защитой канала передачи данных. О шифровании трафика можно прочитать тут: 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 - аутентифицирует пользователей, авторизует запросы пользователей, маршрутизирует запросы от 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.

Таблица 1.

Класс Сборка Описание
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(new char[]{','});
    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:
          //Аутентификация не удалась.   
          throw new System.Security.SecurityException("Authentication 
                        failed");
       }
     }
   }
   //Если SID существует в заголовках запроса, то авторизируем запрос 
   //по этому SID
   else
   {
     //Воостанавливаем сессию по ее идентификатору
     context = 
      (ServerSecurityContext)SecurityContextContainer.GetInstance()[SID];
     if (context == null)
     {
     throw new 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")]
public  void 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)]
public  string  GetUserData()
{
  Console.WriteLine("GetUserData " +  
      Security.ServerSecurityContext.Current.Login);
}

Главное - проставить для этого ссылки на SecurityBase и SecurityServer(SecurityClient).

Заключение

Тестовое приложение WebCl (рисунок 8) демонстрирует возможности описанного решения. Это приложение, впрочем, как и все решение, прилагается к этой статье в виде проекта в формате Visual Studio .Net 2003.

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

Можно организовать проверку - "один пользователь - одна сессия". Можно добавить шифрование трафика. Свойство Items объектов IsecurityContext может служить контейнером для сохранения различных объектов в сессии пользователя. Путем небольшой переработки клиентской части, это решение можно адаптировать для Windows Forms-приложений. В общем, поле для деятельности большое.

Так же можно добавить возможности для масштабирования, вынеся контейнер сессий во внешний сервис, по аналогии с ASP.NET State Service и сделав объекты сессий сериализуемыми.

Если у кого возникнут вопросы, или идеи и замечания по улучшению описанного механизма, пишите sun_shef@msn.com .

Бесплатный конструктор сайтов и Landing Page

Хостинг с DDoS защитой от 2.5$ + Бесплатный SSL и Домен

SSD VPS в Нидерландах под различные задачи от 2.6$

✅ Дешевый VPS-хостинг на AMD EPYC: 1vCore, 3GB DDR4, 15GB NVMe всего за €3,50!

🔥 Anti-DDoS защита 12 Тбит/с!

VPS в 21 локации

От 104 рублей в месяц

Безлимитный трафик. Защита от ДДоС.

🔥 VPS до 5.7 ГГц под любые задачи с AntiDDoS в 7 локациях

💸 Гифткод CITFORUM (250р на баланс) и попробуйте уже сейчас!

🛒 Скидка 15% на первый платеж (в течение 24ч)

Новости мира IT:

Архив новостей

IT-консалтинг Software Engineering Программирование СУБД Безопасность Internet Сети Операционные системы Hardware

Информация для рекламодателей PR-акции, размещение рекламы — adv@citforum.ru,
тел. +7 495 7861149
Пресс-релизы — pr@citforum.ru
Обратная связь
Информация для авторов
Rambler's Top100 TopList liveinternet.ru: показано число просмотров за 24 часа, посетителей за 24 часа и за сегодня This Web server launched on February 24, 1997
Copyright © 1997-2000 CIT, © 2001-2019 CIT Forum
Внимание! Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав. Подробнее...