Завдання, ASP, Програмування, статті

Архітектура використання

На малюнку 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

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*