|
| ||||||||||||||||||||||||||||||
| ||||||||||||||||||||||||||||||
|
2004 г. Аутентификация и авторизация пользователей между Web-сервером и сервером приложения в .NETШеломанов Роман В статье рассматривается пример решения задачи по аутентификации и авторизации клиентов Web-сервера на сервере приложения, где под Web-сервером понимается работающее на нем приложение ASP.NET, а под сервером приложения – .NET-приложение. Взаимодействие осуществляется через .NET Remoting (TCP/Binary). Исходные тексты:В статье рассматривается пример решения задачи по аутентификации и авторизации клиентов 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 - аутентифицирует пользователей, авторизует запросы пользователей, маршрутизирует запросы от 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.
АутентификацияНа рисунке 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 . |
|
CITForum © 1997–2025