Автор:
Dimon aka Manowar (
aspnetman@aspnetmania.com)
Источник:
www.aspnetmania.com
Почти все программисты, начинающие работать с ASP.NET, приходят в дикий восторг услышав фразу "авторизация на основе ролей". Но когда они знакомятся с этим поближе восторг обычно сменяется унынием и возмущением "неужели нельзя было сделать это более красивей и удобней - без заморачивания с файлом конфигурации и с более гибким управлением?". Но расстраиваться нет причин – не все так плохо, как кажется на второй взгляд :).
Для начала вспомним, что же нам предлагают создатели ASP.NET для управления доступом к ресурсам веб приложения пользователям, имеющим определенные роли (или говоря более привычным для администраторов языком – пользователям, входящим в определенные группы). А предлагают они следующее решение:
- Для аутентифицированного пользователя создать экземпляр класса GenericPrincipal, содержащий кроме всего прочего информацию о ролях этого пользователя.
- Определить в файле конфигурации web.config с помощью тегов <authorization> и <allow roles="..">/<deny roles=".."> права доступа пользователей с указанными ролями к указанным ресурсам.
Данный алгоритм имеет, к сожалению, один существенный недостаток – права доступа в нем определяются в файле web.config, имеющем весьма специфическую структуру и расположенном в папке веб приложения. И этот недостаток накладывает серьезные ограничения на внесение изменений в систему авторизации сайта.
Столкнувшись с данной проблемой, мне пришлось сразу же отбросить использование web.config для определения прав и обязанностей. Хотя бы потому, что реальным назначением этих самых прав доступа пользователей с определенными ролями должен был заниматься (и занимается до сих пор) человек, для которого XML вообще и web.config в частности сущности весьма далекие. Поэтому необходимо было создать подобное решение основываясь на следующих предпосылках:
- Вся информация должна храниться в БД.
- Должен быть понятный и удобный интерфейс для назначения прав доступа к файлам, для добавления/редактирования пользователей и ролей.
- В результате проверки прав доступа пользователь либо должен попасть на запрашиваемый ресурс, либо же получить сообщение о недостатке прав.
- Администратор должен иметь права на все.
Начал я естественно с БД. В результате получились 4 таблицы для хранения информации о пользователях, группах пользователей, страницах сайта и отношениях между пользователями и группами:
Небольшое пояснение в этой схеме нужно разве что лишь для поля GroupsList таблицы pages. В этом поле содержится список групп, разделенных запятыми, которым разрешен доступ к странице. Сделал я так ввиду того, что проверка на возможность доступа к странице происходит при каждом обращении к странице. И соответственно необходимо иметь список допустимых для страницы ролей в удобном виде.
Процесс создания административного интерфейса для управления этим всем я описывать не буду – этот вопрос достаточно тривиален. Перейдем сразу к рассмотрению вопроса авторизации пользователей с помощью ролей. Он состоит из двух частей – создания объекта GenericPrincipal, содержащего информацию о пользователе и группах, в которые входит этот пользователь, и, собственно, самой авторизации доступа пользователя к запрашиваемой странице.
Лучшее место для создания экземпляра класса GenericPrincipal – обработчик события AuthenticateRequest класса HttpApplication. Рассмотрим этот процесс в подробностях.
Первым делом нужно проверить аутентифицирован ли пользователь:
protected void Application_AuthenticateRequest(Object sender, EventArgs e)
{
if (Request.IsAuthenticated == true)
{
Следующим шагом будет получение списка ролей данного пользователя. Дабы не обременять базу слишком частыми обращениями для получения этого списка (все таки вызов данного обработчика события происходит при каждом обращении к страницам веб приложения) полученный из базы данных список ролей будет сохраняться в куках.
String[] roles;
if ((Request.Cookies["mff_roles"] == null) || (Request.Cookies["mff_roles"].Value == ""))
{
Куки со списком ролей еще не установлены – будем получать их из базы данных.
administrators admin = new administrators();
DataView dv = admin.MemberOf(Int32.Parse(User.Identity.Name));
Данный метод возвращает результат следующей SQL конструкции
select
name
from
groups inner join administrators_groups on
groups.group_uid = administrators_groups.group_uid
where
admin_uid = @admin_uid
т.е. список групп (ролей), в которые входит данный пользователь. На основании этого списка создается строка, содержащая роли пользователя, разделенные запятой.
String roleStr = "";
foreach (DataRowView drv in dv)
{
roleStr += String.Format("{0};", drv["name"]);
}
roleStr = roleStr.Remove(roleStr.Length - 1, 1);
После этого создается экземпляр класса FormsAuthenticationTicket, содержащий необходимую нам инофрмацию (в том числе и список ролей пользователя).
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
1,
Context.User.Identity.Name,
DateTime.Now,
DateTime.Now.AddHours(1),
false,
roleStr
);
roles = roleStr.Split(new Char[] {';'});
Затем он шифруется
String cookieStr = FormsAuthentication.Encrypt(ticket);
И записывается в куки.
Response.Cookies["mff_roles"].Value = cookieStr;
Response.Cookies["mff_roles"].Path = "/";
Response.Cookies["mff_roles"].Expires = DateTime.Now.AddHours(1);
}
else
{
Если же куки со списком ролей уже существуют, тогда они извлекаются и дешифруются
FormsAuthenticationTicket ticket =
FormsAuthentication.Decrypt(Context.Request.Cookies["mff_roles"].Value);
И сохраняются в массиве строк, необходимом для конструктора класса GenericPrincipal.
ArrayList userRoles = new ArrayList();
foreach (String role in ticket.UserData.Split( new char[] {';'} ))
{
userRoles.Add(role);
}
roles = (String[]) userRoles.ToArray(typeof(String));
}
И, наконец, создается экземпляр класса GenericPrincipal, содержащий всю необходимую информацию.
Context.User = new GenericPrincipal(Context.User.Identity, roles);
}
}
Теперь все готово к авторизации пользователя. Сам процесс авторизации проходит в обработчике события AuthorizeRequest все того же класса HttpApplication. Рассмотрим его подробнее:
protected void Application_AuthorizeRequest(Object sender, EventArgs e)
{
if(Request.IsAuthenticated)
{
Как и в предыдущем методе, здесь неплохо бы проверить аутентифицирован ли пользователь. После этого получаем полное имя файла (без ведущего слеша) и в случае, если запрашивается одна из специально указанных страниц, выходим из метода.
string pageName = Request.FilePath.Remove(0, 1);
if((pageName == "login.aspx") || (pageName == "logout.aspx") || (pageName == "error.aspx")
|| (pageName.ToLower() == "accessdenied.aspx"))
return;
Получаем список страниц из кеша. Если по какой-то причине объект, содержащий этот список, отсутствует в кеше (закончилось время хранения или объект был удален), получаем список страниц из базы и сохраняем его в кеш.
DataView pages = (DataView) Context.Cache["admin_pages"];
if(pages == null)
{
pages page = new pages();
pages = page.List();
Context.Cache.Insert("admin_pages", pages, null, DateTime.Now.AddHours(12), TimeSpan.Zero);
}
Находим интересующую нас cтраницу.
pages.RowFilter = "PageName = '" + pageName + "'";
if(pages.Count > 0)
{
Если эта страница найдена в списке страниц – проходимся по списку допустимых для этой страницы ролей и проверяем, не принадлежит ли пользователь одной из этих ролей. Если все ок – выходим из обработчика.
foreach(string role in pages[0]["GroupsList"].ToString().Split(new char[] {','}))
{
if(Context.User.IsInRole(role))
{
return;
}
}
Если пользователь не принадлежит ни одной из ролей – проверяем не является ли пользователь администратором (у меня для администраторов заведена предопреденная группа Administrators).
if(Context.User.IsInRole("Administrators"))
{
return;
}
}
else
В случае же, когда страница не найдена в списке страниц – к ней может обратиться только администратор – опять выполняем эту проверку.
if(Context.User.IsInRole("Administrators"))
return;
В этом месте обработчика мы окажемся только в случае, когда все предыдущие проверки не прошли. И означать это будет то, что у пользователя нет прав на доступ к запрашиваемой странице. Сообщим ему об этом перенаправив его на страницу, сообщающую, что Access Denied! :)
Context.RewritePath("/AccessDenied.aspx");
}
}
Вот и все. Осталось добавить Forms аутентификацию и запретить доступ ко всем файлам веб приложения вставив в web.config следующие строки:
<authentication mode="Forms">
<forms loginUrl="/login.aspx" name=".ADMINAUTH" timeout="45"/>
</authentication>
<authorization>
<deny users="?" />
</authorization>
занести данные о пользователя, группах и страницах сайта в базу данных и система готова к работе.
Созданная мной и описанная в данно статье система конечно же не претендует на место "лучшей из лучших", но моим заказчикам на тот момент хватило и такой функциональности :). Но при желании и потребности задача внесения дополнительной функциональности (например добавление права явного запрета или установка плав для отдельного пользователя) реализуется очень быстро.