Реализация механизма разграничения прав доступа к админ-части
На своей практике веб-разработки я очень часто сталкивался с ситуациями, в которых заказчики ставили конкретную цель, а именно о разделении частей админки относительно доступности тем или иным пользователям. При этом разработка данного модуля велась в контексте расширяемой системы, а то есть с нефиксированым числом модулей, к которым организовуется доступ, ну и, соответственно, неограниченным числом пользователей системы.
Что ж, сама по себе данная тема довольно грузная, и требует определённого времени на анализ и постанувку задачи.
В контексте данной статьи, мы будем вести разработку в контексте некоторой абстрактной информационной системы, со своей инфраструктурой и архитектурой, при этом данная система предоставляет пользователю возможность расширять функционал, а то есть устанавливать новые модули, и соответственно устанавливать права доступа к ним тому либо иному пользователю, зарегистрированному в качестве администратора системы.
Давайте с самого начала обсудим архитектуру модульной системы на выбранной нами псевдо-системе.
Все модули представлены ввиде подключаемых к главному документу (индекс-файлу) вставок. Запрос модуля происходит из строки запроса QUERY_STRING, и название подключаемого модуля передаётся в качестве аргумента act. В некотором месте индекса файла происходит изъятие и обработка данного параметра. После, если у пользователя достаточно прав для доступа к модулю в контексте чтения, происходит проверка существования указанного в строке запроса модуля, и если таковой существует, то происходит его подключение к индекс файлу.
Я не просто так упомянул о "контексте чтения", так как наша системе предполагает существование двух контекстов работы с системой, а именно - чтение и запись. При этом под чтением предполагается непосредственный доступ к модулю и к тем его частям, которые не предполагают внесение изменений в структуру данных в БД. Под записью же предполагается непосредственное внесение изменений в информацию, хранимую в базе данных.
Для воплощения данного механизма мы будет проверять значение переменной строки запроса `do`, которая обрабатывается в самом модуле и носит информацию о том, к какому разделу модуля необходимо предоставить доступ пользовалю.
Значение do буду фиксированными, данная переменная будет принимать следующие значения:
- main - главная часть модуля (доступно в контексте чтения)
- config - раздел настройки модуля (доступно в контексте записи)
- create - произвести некоторые действия, по добавлению информации в БД (доступно в контексте записи)
- delete - доступ к разделу, предоставляющему возможности удалить некоторую информацию, в контексте данного модуля (доступно в контексте записи)
- edit - доступ к редактированию информации в контексте модуля (доступно в контексте записи)
В целом, этот список можно увеличить, при этом всё зависит лишь только от масштабов проекта и его потребностей в функционале.
Теперь непосредственно о модулях. Кроме физического существования некоторого модуля в контексте файловой системы проекта, модуль так же должен быть добавлен в особую таблицу БД, которая будет содержать информацию о всех существующих модулях в системе. Добавление и изменение данных данной таблицы, обычно, производится непосредственно в контексте модулей, а то есть во время их инсталяции в системе. Однако это уже углубление в принципы посмотроения расширяемых систем, о чём мы как-то в другой раз поговорим, и посему, мы ограничимся ручным обновлением и добавлением данных о модулях.
Так, запись о модуле системы будет содержать следующую информацию: английский идентификатор названия модуля, который будет идентичен значению переменной среды GET - act (относительно него будет производится непосредственно запрос модуля), русский идентификатор модуля, который будет использоватся в списке модулей.
Кроме модулей у нас будут ещё две таблицы, а именно таблица в которой будут хранится данные относительно профилей прав доступа и таблица с информацией о пользователях непосредственно.
Таблица профилей безопасности будет состоять всего из трёх полей - идентификатор профиля (числовое значение идентификатора записи), текстый идентификатор модуля (предназначенный для пользователей), а так же особым образом сформированная текстовая метка, содержащая информацию о правах пользователя, в контексте каждого из модулей.
Что ж, давайте рассмотрим эту особую структуру. Она будет следующей: [ module_indefier : [0 | 1]+ \: [0 | 1]+ \;] *
То есть идёт список из пар: имя модуля ":" права чтения "," права записи ";". При этом данная метка обновляется в момент внесения изменений о правах доступа пользователя к системе. Если в системе появляется информация о модуле, который не вошёл в данную метку, то стоит просто произвести процедуру редактирования, и данные сохранятся автоматически.
Теперь же нам осталось рассмотреть структуру всего одной таблицы БД, и мы сможем принятся за реализацию алгоритмической части, а именно таблицы с информацией о пользователях системы, ведь назначение им прав доступа и является нашей главной задачей.
Я не буду добавлять ничего лишнего в неё, но лишь то, что будет использоватся в контексте темы данной статьи. Таблица пользователей будет содержать следующие поля: идентифицатор пользователя (числовой счётчик), логин, пароль (хеш оригинального пароля), профиль безопасности пользователя (идетификатор группы пользователя, относительно прав в системе), и всё. Мне кажется этой информации нам с вами вполне хватит, для реализации поставленной задачи, а уже все остальные надстройки я предоставляю возможность сделать самим.
Итак, структуру мы обсудили, и, надеюсь, у всех сложилось уже некоторое представление о том, как мы будем реализовывать поставленную в теме статьи задачу. Сейчас я приведу вспомогательный SQL-код таблиц, описанных выше, после чего сразу же перейду к воплощению алгоритма проверки прав доступа пользователя, а так же создания и изменения профилей доступа. После каждого отдельного модуля мы подробно обсудим все вопросы, которые могут возникнуть у читателей.
Таблица `modules`:
CREATE TABLE `modules` ( `id` bigint(20) NOT NULL auto_increment, `indefier` text collate utf8_unicode_ci NOT NULL, `title` text collate utf8_unicode_ci NOT NULL, PRIMARY KEY (`id`) ) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
Таблица `secure_groups`:
CREATE TABLE `secure_groups` ( `id` bigint(20) NOT NULL auto_increment, `title` text collate utf8_unicode_ci NOT NULL, `perms` text collate utf8_unicode_ci NOT NULL, PRIMARY KEY (`id`) ) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci ;
Таблица `users`
CREATE TABLE `users` ( `id` bigint(20) NOT NULL auto_increment, `login` text collate utf8_unicode_ci NOT NULL, `passwd` text collate utf8_unicode_ci NOT NULL, `groupId` int(1) NOT NULL default '0', PRIMARY KEY (`id`) ) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci ;
Далее описан класс для внедрения функций проверки прав доступа пользователей к модулям системы.
<?php class security extends engine{ var $temp=array(); function getUserId(){ $hash=explode('::',$_COOKIE['site_hash']); $id=$hash[0]; return $id; } function getUserSecurityAccess($id){ if(is_numeric($id)){ $id=rawurlencode($id); $conn_id=mysql_connect('host','user','passwd'); mysql_select_db('database'); $q=mysql_query("SELECT groupid FROM `users` WHERE id='".$id."'",$conn_id); if($q){ if(mysql_numrows($q)!=0){ $result=@mysql_fetch_assoc($q); return $result['group_id']; }else{ return -1; } }else{ return -1; } mysql_close($conn_id); }else{ return -1; } } function checkUserPermission($module,$act){ #return true; $this->temp=array(); $this->temp['_result']=0; $this->temp['_uid']=explode('::',$_COOKIE['site_hash']); $this->temp['_uid']=$this->temp['_uid'][0]; $this->temp['_gid']=$this->getUserSecurityAccess($this->temp['_uid']); $this->temp['_conn_id']=mysql_connect('host','user','passwd'); mysql_select_db('database'); $this->temp['_q1']=mysql_query('SELECT perms' .'FROM `secure_groups`' .'WHERE id='.$this->temp['_gid']); $this->temp['_access_stamp']=mysql_fetch_assoc($this->temp['_q1']); $this->temp['_access_stamp']=$this->temp['_access_stamp']['perms']; $this->temp['_access_stamp']=explode(';',$this->temp['_access_stamp']); $this->temp['_access_stamp']=array_slice($this->temp['_access_stamp'],0,-1); foreach($this->temp['_access_stamp'] as $this->temp['v']){ $this->temp['_mod_access']=explode(':',$this->temp['v']); $this->temp['_mod_indefier']=$this->temp['_mod_access'][0]; if($this->temp['_mod_indefier']==$module){ $this->temp['_perms']=explode(',',$this->temp['_mod_access'][1]); switch($act){ case 'r': $this->temp['_result']=($this->temp['_perms'][0]==1)? 1:0; break; case 'w': $this->temp['_result']=($this->temp['_perms'][1]==1)? 1:0; break; } break; } } mysql_close($conn_id); return $this->temp['_result']; } } ?>
Данный класс внедряет функции, предназначенные для воплещения алгоритмического задания, описанного выше. Сейчас мы обсудим каждую функцию отдельно.
Функция secure::getUserId()
Используя данную функцию, мы подразумеваем, что во время авторизации пользователя в системе в переменной среде $_COOKIE была установлена переменная `site_hash`, состоящая из идентификатора пользователя в системе и хеша для проверки аутентичности его в системе. Функция просто изымает значение идентификатора, возращая его значение на выходе.
Функция secure::getUserSecurityAccess($id)
На выходе данная функция возвращает идентификатор профиля безопасности текущего пользователя в системе.
Функция secure::checkUserPermission($module,$act))
Производится запрос к БД, относительно прав пользователя на произведение действий чтения/записи в контексте переданного в качестве параметра модуля.
Осталось лишь описать процедуру формирования переменной в среде $_COOKIE, и тему статьи можно будет считать расскрытой.
Процедура авторизации будет выглядеть ввиде внесения личных данных пользователя (логин и пароль) в специальную форму, после отправки которой произойдёт обработка данных, переданных пользователем, по-методу функции checkAuthData(), и, в случае корректности данных, будет произведено сохранение данных о пользователе ввиде куки записи на период установленный пользователем, либо в отсутствии заданного значение на период по-умолчанию.
Для проверки аутентичности данных хранимых в переменной среде $_COOKIE, мы будем использовать функцию EatCookie(), которая будет производить валидацию данных, возвращая булевый результат проверки (истина - ложь).
Я не привожу форму для отправки, так как это не часть теории программирования, указав лишь идентификаторы полей.
- `ulogin` - логин пользователя
- `upasswd` - пароль пользователя
- `stime` - время сессии, устанавливаемое пользователем (от 1 до 5 часов)
- `auth` - имя кнопки отправки
<?php function checkAuthData($info){ $login=rawurlencode($info['ulogin']); $passwd=rawurlencode($info['upasswd']); $stime=(rawurlencode(trim($info['stime']))=='')?1:rawurlencode($info['stime']); $err_code=0; $result=false; if(trim($login)==''){ $err_code=1; } if(trim($passwd)==''){ $err_code=1; } if($err_code){ $result=false; }else{ $conn_id=mysql_connect('host','user','passwd'); mysql_select_db('database'); $q=mysql_query("SELECT * FROM `users` WHERE passwd='".md5($passwd)."' AND login='".$login."'",$conn_id); if($q){ if(@myql_numrows($q)!=0){ $row=mysql_fetch_assoc($q); if($row['status']=='on'){ $hash=$row['id'].'::'.md5(md5($login). md5($passwd)); setcookie('site_hash',$hash,$stime*3600*60); $result=true; }else{ $result=false; } }else{ $result=false; } }else{ $result=false; } mysql_close($conn_id); } return $result; } function EatCookie(){ $result=false; $hash=isset($_COOKIE['site_hash'])?explode('::',$_COOKIE['site_hash']):($result=false); $id=$hash[0]; $hash=$hash[1]; $conn_id=mysql_connect('host','user','passwd'); mysql_select_db('database'); if(!is_numeric($id)){ $result=false; }else{ $q=mysql_query("SELECT * FROM `users` WHERE id='".$id."'",$conn_id); if(mysql_numrows($q)==0){ $result=false; }else{ $row=mysql_fetch_assoc($q); $newHash=md5(md5($row['login']).md5($row['passwd'])); if($newHash==$hash){ $result=true; }else{ $result=false; } } } return $result; } ?>
Вот, в целом и всё. Осталось лишь пробовать, экспериментировать, ошибатся и находить решение, что я всецело и оставляю вам.
Надеюсь, что мы скоро встретимся, а для тех кто имеет ко мне вопрос в отношении статьи, да и не только - писать на LoRd1990@gmail.com, либо на mag@tnt43.com.
Кто хочет почитать остальные мои статьи - прошу на http://e-code.tnt43.com.
С уважением Карпенко Кирилл, глава IT-отдела ИНПП.
Оставить комментарий
Комментарии
WHERE id='".$id."'",$conn_id);
1. Отсутствие форматирования кода.
2. Хранение ID пользователя в куки и последующая выборка из БД по этому номеру (меняем номер на 1 и в 90% получаем права админа)
3. Хроническая лень (остались куски старого кода в комментах)
4. Не совсем понял момент..
if(is_numeric($id)){
$id=rawurlencode($id);
//Тут что-то ещё
}
Это наверное прикол такой, сначала проверить является ли цифрой, а потом преобразовывать rawurlencode()
if(trim($login)==''){
$err_code=1;
}
if(trim($passwd)==''){
$err_code=1;
}
а не проще
$isError = false;
if(trim($login) == '' || trim($passwd) == '') {
$isError = true;
}
Мне например так понятней)
Короче в отстой...
PS Я не огромный любитель опускать, но когда вижу подобное и под этим подпись
"глава IT-отдела ИНПП."
у меня всё внутри переворачивает. Ибо нефиг выёживаться!!!!