пятница, января 12, 2007

Declarative and Imperative Security в .Net web сервисах

Жуткая вообще тема, конечно. Вот человек спрашивает «как мне защитить web сервис», и что такое Thread.CurrentPrincipal, и что такое declarative и imperative security. И никто ему толком не ответил. А я не зарегистрирован на SQL.ru и регистрироваться мне там лень. Да и вообще, не там он вопрос свой задал. Спросил бы на gotdotnet.ru или на rsdn.ru – ему бы быстренько ответили. А тема, вообще то хорошая. Жизненная тема.
Продвинутым читателям все это будет скучно и не интересно. Так и не читайте дальше. Но тем, кто приступает к решению подобных задач, я надеюсь, этот материал будет полезен.
Начнем, наверное, с того, что когда мы обращаемся к web сервису, как клиент, нам совершенно нет нужды обращаться к контексту безопасности текущего потока, который как раз и представлен статическим Thread.CurrentPrincipal. На клиенте у нас есть прокси класс web сервиса, а у него замечательное свойство Credentials. Вот его мы и используем для того, чтобы передать web сервису информацию о вызывающем его пользователе. Сделать это надо до первого вызова web метода сервиса, т.е. сразу после создания его экземпляра. Тут есть два варианта. Либо мы передаем информацию о текущем пользователе, и делается это вот так:


// создаем экземпляр прокси класса сервиса на клиенте
Echo echo = new Echo();
// передаем сервису удостоверения текущего пользователя
echo.Credentials = System.Net.CredentialCache.DefaultCredentials;


Либо передаем сервису удостоверения другого пользователя (учетной записи) которые храним где-либо в надежном месте:


// создаем экземпляр прокси класса сервиса на клиенте
Echo echo = new Echo();
// передаем сервису удостоверения пользователя "domain\user"
echo.Credentials = new System.Net.NetworkCredential("user","password","domain");


Вот собственно и все, что нам надо сделать на клиенте. Теперь перенесемся на сервер.
Чтобы исключить неавторизованный доступ к web сервису нам достаточно указать это в конфигурационном файле web.config


<authorization>
<deny users="?" />
</authorization>


Теперь все пользователи, которые не прошли аутентификацию будут завернуты IIS-ом с ошибкой 401 – Unauthorized еще на подступах к нашему web сервису. Сама аутентификация может осуществляться разными способами, в том числе теми, что вы напишите сами. ASP.Net хорошо расширяема в этом аспекте (читаем MSDN здесь).
Итак, те запросы которые добрались таки до нашего web сервиса уже аутентифицированы (ужасное слово). Это важно понимать. Что это означает на практике? Все мы знаем, что каждый запрос у IIS обрабатывается в отдельном потоке. Следовательно, каждый вызов web метода нашего web сервиса тоже обрабатывается в отдельном потоке, и контекст безопасности этого потока (представленный статическим Thread.CurrentPrincipal) содержит данные о пользователе вызвавшем этот метод с клиента. (Справедливости ради, надо отметить, что Thread.CurrentPrincipal может не содержать никаких данных о пользователе, если мы разрешили анонимный доступ к web сервису.)
И тут наступает время поговорить о декларативном и императивном (declarative and Imperative) стиле обеспечения безопасности в .Net. Главное, что стоит знать об этом, состоит в том, что оба стиля, декларативный и императивный, используют одни и те же механизмы, основанные на контексте безопасности потока (Thread.CurrentPrincipal). Декларативный стиль состоит в том, что мы при помощи специальных атрибутов помечаем код, и таким образом предписываем CLR выполнять те или иные проверки безопасности при любом доступе к этому коду. Звучит сложно, но выглядит все это просто:


[WebMethod]
[PrincipalPermission(SecurityAction.Demand, Role="Home\\Supervisor")]
public string Ask(string question)
{
string user = "User ";
return user + " asked: " + question;
}


Атрибут PrincipalPermission указывает, что перед исполнением данного метода CLR необходимо проверить (SecurityAction.Demand) входит ли тот, кто вызвал этот метод в роль «Supervisor» на компьютере (или в домене) «Home». Если данное условие не выполняется (пользователь не входит в данную роль), то при попытке вызова мы получим вот такое исключение


System.Web.Services.Protocols.SoapException: System.Web.Services.Protocols.SoapException: Server was unable to process request. ---> System.Security.SecurityException: Request for principal permission failed. at System.Security.Permissions.PrincipalPermission.Demand() at System.Security.PermissionSet.Demand() at TargetService.Echo.Ask(String question)


Императивный стиль обеспечения безопасности означает, что весь необходимый код по проверке прав доступа мы пишем сами. Например, вот так:


[WebMethod]
public string Ask(string question)
{
PrincipalPermission perm = new PrincipalPermission(
Thread.CurrentPrincipal.Identity.Name,
RoleName);
perm.Demand();

string user = "User ";
return user + " asked: " + question;
}


У нас добавилось две строчки кода. Сначала мы создаем экземпляр PrincipalPermission, передавая ему имя пользователя из текущего контекста безопасности и имя роли. А затем вызываем метод Demand(), который проверяет, принадлежит ли пользователь заданной роли. В случае метод Demand() выбрасывает исключение вот такого вида:


System.Web.Services.Protocols.SoapException: System.Web.Services.Protocols.SoapException: Server was unable to process request. ---> System.Security.SecurityException: Request for principal permission failed. at System.Security.Permissions.PrincipalPermission.Demand() at TargetService.Echo.Ask(String question)


Итак, получен результат аналогичный предыдущему.
Настоящий программист должен быть достаточно ленивым, и у него в голове сразу возникает мысль «зачем писать лишний код, если есть декларативный метод обеспечения безопасности»? Ответ прост – декларативный метод не обеспечивает достаточной гибкости. В нашем примере имя роли, заданное в атрибуте – это константа, и, следовательно, оно не может быть изменено после компиляции. Во втором примере, имя роли задается в переменной, а она может быть, к примеру, проинициализирована из конфигурационного файла, или получена из другого источника.
Кстати, императивный подход вовсе не исчерпывается использованием встроенных классов, реализующих IPermission (таких, как PrincipalPermission). Проверку ролевого доступа мы можем выполнить и по-другому. Например, так:


[WebMethod]
public string Ask(string question)
{
if(!Thread.CurrentPrincipal.IsInRole(RoleName))
throw new SecurityException("Request for principal permission failed");

string user = "User ";
return user + " asked: " + question;
}


Этот код выглядит еще проще, чем предыдущий, но результат получаем аналогичный. Если текущий пользователь не принадлежит заданной роли, получаем исключение:


System.Web.Services.Protocols.SoapException: System.Web.Services.Protocols.SoapException: Server was unable to process request. ---> System.Security.SecurityException: Request for principal permission failed at TargetService.Echo.Ask(String question)


И, наконец, вот на этом месте, как мне кажется, наступает самое подходящее время, разобраться с тем, откуда все таки берутся все эти роли, и как Principal или PrincipalPermission узнает о том, что текущий пользователь входит в данную роль.
Код, который отвечает за это (определение принадлежности пользователя к роли) содержится в методе CurrentPrincipal.IsInRole(string). Если мы не предпринимали никаких дополнительных действий, то наверняка наш Thread.CurrentPrincipal на поверку окажется экземпляром класса WindowsPrincipal. Это следствие того, что мы использовали режим Windows Integrated Aithentication IIS. Сам WindowsPrincipal представляет учетную запись пользователя Windows (локальную или доменную) а метод WindowsPrincipal.IsInRole(string) определяет принадлежность пользователя группе (доменной или локальной).
Ну а если мы хотим для авторизации использовать не Windows пользователей, а своих, данные о которых храним в БД приложения? Или, как вариант, пользователи у нас будут Windows, а вот роли мы определяем свои, и правила принадлежности пользователя к роли у нас тоже свои и при этом очень хитрые? Обе проблемы решаемы, но для этого нам придется писать код.
Когда нас не устраивает WindowsPrincipal, есть класс GenericPrincipal и мы можем создать его экземпляр и присвоить HttpContext.Current.User в обработчике события Application_AuthenticateRequest, что в Global.asax. Ну а если нас не устраивает и GenericPrincipal, мы можем написать свою реализацию интерфейса IPrincipal и даже расширить ее. Потребоваться это может, например, в случае, когда нас не устраивает метод IsInRole(string). Может случиться такое, что для определения принадлежности к роли понадобится передавать дополнительные параметры. Тогда мы делаем свою реализацию IPrincipal и расширяем ее дополнительными методами (не интерфейс, конечно, а класс). Вообще то, все это замечательно, с картинкам, с примерами кода, расписано в MSDN. Мне подробней все равно не написать, да и смысла нет.
Вот собственно то, что вам надо знать для того, чтобы начать понимать, как обеспечивается безопасность ASP.NET web сервисов. Теперь вы можете заняться этой темой плотнее, используя MSDN, и попытаться построить действительно безопасный сервис (или сайт – механизмы безопасности для web сервисов и web страниц ASP.NET общие). Есть еще одна тема, которая плотно связана с безопасностью - это имперсонация и делегирование в ASP.NET. О них я тоже недавно писал, и кому интересно, могут посмотреть по ссылке

1 комментарий:

Leonid Slobodchikov комментирует...

Немного offtopic: А чем вы форматируете код опубликованый на блоге?