Оценка 100 Оценить ![]() ![]() ![]() ![]() ![]() ![]()
|
| Введение Задача Решение Архитектура решения ЗаключениеПротоколы взаимодействия Идея решения Развертывание Диаграмма классов Аутентификация Авторизация Поддержка сессий | ![]() |
В статье рассматривается пример решения задачи по аутентификации и авторизации клиентов Web-сервера на сервере приложений, где под Web-сервером понимается работающее на нем приложение ASP.NET, а под сервером приложений – .NET-приложение. Взаимодействие осуществляется через .NET Remoting (TCP/Binary).
Что есть интересного в рассматриваемом решении:
В статье не рассматриваются вопросы, связанные с защитой канала передачи данных. О шифровании трафика можно прочитать тут: http://msdn.microsoft.com/msdnmag/issues/03/06/netremoting/
На рисунке 1 изображена схема некоторой информационной системы (ИС). ИС состоит из ядра – совокупности серверов приложений, выполняющих бизнес-логику, и Web-интерфейса, расположенного на WEB сервере и предоставляющего доступ к системе через Интернет. В приведенной архитектуре и будет использоваться рассматриваемое решение.

Рисунок 1.
Все сервисы ИС реализованы на базе платформы 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 | Интерфейс для объектов, содержащих состояние сессии. |
На рисунке 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.
Оценка 100 Оценить ![]() ![]() ![]() ![]() ![]() ![]()
|