Завдання

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

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

*

*