Реализация механизма разграничения прав доступа к админ-части

На своей практике веб-разработки я очень часто сталкивался с ситуациями, в которых заказчики ставили конкретную цель, а именно о разделении частей админки относительно доступности тем или иным пользователям. При этом разработка данного модуля велась в контексте расширяемой системы, а то есть с нефиксированым числом модулей, к которым организовуется доступ, ну и, соответственно, неограниченным числом пользователей системы.

Что ж, сама по себе данная тема довольно грузная, и требует определённого времени на анализ и постанувку задачи.

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

Давайте с самого начала обсудим архитектуру модульной системы на выбранной нами псевдо-системе.

Все модули представлены ввиде подключаемых к главному документу (индекс-файлу) вставок. Запрос модуля происходит из строки запроса 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;
}
?>

Вот, в целом и всё. Осталось лишь пробовать, экспериментировать, ошибатся и находить решение, что я всецело и оставляю вам.

Карпенко Кирилл, e-code.tnt43.com



Опубликовал admin
12 Авг, Воскресенье 2007г.



Программирование для чайников.