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.
Требования
- 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 .