Исходники.Ру - Программирование
Исходники
Статьи
Книги и учебники
Скрипты
Новости RSS
Магазин программиста

Главная » Статьи по программированию » .NET - ASP.NET »

Обсудить на форуме Обсудить на форуме

Создание системы авторизации, основанной на ролях, в ASP.NET приложении

Почти все программисты, начинающие работать с ASP.NET, приходят в дикий восторг услышав фразу "авторизация на основе ролей". Но когда они знакомятся с этим поближе восторг обычно сменяется унынием и возмущением "неужели нельзя было сделать это более красивей и удобней - без заморачивания с файлом конфигурации и с более гибким управлением?". Но расстраиваться нет причин – не все так плохо, как кажется на второй взгляд :).

Для начала вспомним, что же нам предлагают создатели ASP.NET для управления доступом к ресурсам веб приложения пользователям, имеющим определенные роли (или говоря более привычным для администраторов языком – пользователям, входящим в определенные группы). А предлагают они следующее решение:

  1. Для аутентифицированного пользователя создать экземпляр класса GenericPrincipal, содержащий кроме всего прочего информацию о ролях этого пользователя.
  2. Определить в файле конфигурации web.config с помощью тегов <authorization> и <allow roles="..">/<deny roles=".."> права доступа пользователей с указанными ролями к указанным ресурсам.

Данный алгоритм имеет, к сожалению, один существенный недостаток – права доступа в нем определяются в файле web.config, имеющем весьма специфическую структуру и расположенном в папке веб приложения. И этот недостаток накладывает серьезные ограничения на внесение изменений в систему авторизации сайта.

Столкнувшись с данной проблемой, мне пришлось сразу же отбросить использование web.config для определения прав и обязанностей. Хотя бы потому, что реальным назначением этих самых прав доступа пользователей с определенными ролями должен был заниматься (и занимается до сих пор) человек, для которого XML вообще и web.config в частности сущности весьма далекие. Поэтому необходимо было создать подобное решение основываясь на следующих предпосылках:

  1. Вся информация должна храниться в БД.
  2. Должен быть понятный и удобный интерфейс для назначения прав доступа к файлам, для добавления/редактирования пользователей и ролей.
  3. В результате проверки прав доступа пользователь либо должен попасть на запрашиваемый ресурс, либо же получить сообщение о недостатке прав.
  4. Администратор должен иметь права на все.

Начал я естественно с БД. В результате получились 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>

занести данные о пользователя, группах и страницах сайта в базу данных и система готова к работе.

Созданная мной и описанная в данно статье система конечно же не претендует на место "лучшей из лучших", но моим заказчикам на тот момент хватило и такой функциональности :). Но при желании и потребности задача внесения дополнительной функциональности (например добавление права явного запрета или установка плав для отдельного пользователя) реализуется очень быстро.


Может пригодится:


Автор: Manowar
Прочитано: 6948
Рейтинг:
Оценить: 1 2 3 4 5

Комментарии: (0)

Добавить комментарий
Ваше имя*:
Ваш email:
URL Вашего сайта:
Ваш комментарий*:
Код безопастности*:

Рассылка новостей
Рейтинги
© 2007, Программирование Исходники.Ру