Исходные тексты:
http://www.ishodniki.ru/articles/dotnet/WEB_REMOTING_Updated.zip
В статье рассматривается пример решения задачи по аутентификации и
авторизации клиентов Web-сервера на сервере приложения, где под
Web-сервером понимается работающее на нем приложение ASP.NET, а под
сервером приложения - .NET-приложение. Взаимодействие осуществляется
через .NET Remoting (TCP/Binary).
Что есть интересного в рассматриваемом решении:
- Использование серверных и клиентских специализированных
канальных приемников.
- Организация сессий на стороне сервера приложения и стороне
Web-сервера
- Установление зависимости между клиентом и его сессиями на
Web-сервере и сервере приложения.
- Организация авторизации без добавления дополнительных аргументов
в методы сервера приложения.
- "Протаскивание" пользовательских данных через все методы в
потоке без добавления дополнительных аргументов в методы.
В статье не рассматриваются вопросы, связанные с защитой канала
передачи данных. О шифровании трафика можно прочитать тут:
http://msdn.microsoft.com/msdnmag/issues/03/06/netremoting/
Задача
Архитектура использования
На рисунке 1 изображена схема некоторой информационной системы (ИС)
.
ИС состоит из ядра - совокупности серверов приложений, выполняющих
бизнес-логику, и Web-интерфейса, расположенного на WEB сервере и
предоставляющего доступ к системе через Интернет. В приведенной
архитектуре и будет использоваться рассматриваемое решение.
Рисунок 1.
Требования
- Web-сервер не должен иметь прямого доступа к ИС.
- ИСдолжна определять права пользователей на выполнение того или
иного запроса (авторизованные и анонимные запросы).
- Права пользователей определяются их ролью в ИС.
Дополнительные ограничения
Все сервисы ИС реализованы на базе платформы 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