разграничение прав доступа php
Создание ролей пользователей на сайте
Привет друзья. Хочу поговорить с вами о методе разграничения прав пользователей на сайте, а точнее о ролях пользователей.
Роль пользователя это совокупность прав и разрешений выданные той или иной роли описанной в информационной системе.
Так вот, на любом форуме вы встречали администраторов, модераторов, гостей и т.д. Все это и есть роли. Каждая из ролей отвечает своим задачам и имеет свои привилегии. В Web приложениях роли создаются программистом, он сам создает роли и описывает их права.
Самые распространённые роли это:
И так друзья, что мы сможем делать с помощью ролей на нашем сайте? А все что угодно. Например вы можете показывать какую-то часть контента не зарегистрированному пользователю. Если пользователь прошёл регистрацию и авторизовался в системе, то его привилегии увеличиваются в плане доступа к контенту и он уже может просматривать более подробный контент вашего ресурса (например фотогалерею, оставлять комментарии и т.д). Все зависит от вашей фантазии и того что хотите показать пользователю или на оборот спрятать от него.
И так, создадим таблицы в нашей базе данных. У меня это таблицы:
В таблице bez_reg в конце добавляем поле role, где собственно и будет привязка пользователя к роли.
Далее создаем таблицу bez_role, где напишим названия наших ролей:
Данный трех ролей нам будет достаточно, чтобы система ролей работала. Создаем третью таблицу bez_contnet, здесь будет храниться контент и роли пользователей которым можно просматривать данный контент.
С таблицами разобрались, теперь нужно нам сделать себя администраторами т.к. по идее при импорте базы данных у вас должна быть учетная запись по умолчанию с ролью администратор, но так как мы делаем все сами, то нам не лень залезть в базу и сделать себя админом )).
Регистрируемся через форму регистрации, активируем свой аккаунт изменив поле status = 1 и делаем себя администратором установив в поле role = 1.
Далее заполняете таблицу bez_contnet, произвольным текстом, при этом в поле role через запятую указываете те роли которым разрешен доступ на просмотр данного контента. У меня администратор может просматривать все статьи, модератор только первую статью, пользователь только вторую статью.
С созданием нужных таблиц разобрались, теперь нужно немного поправить скрипт авторизации. Для это переходим в папку ./scripts/auth/auth.php ищим запрос к базе данных
Заменяем на новый запрос к базе данных
Далее добавляем нужные сессионые переменные для работы с авторизированным пользователем.
Способы разграничения доступов к файлам при помощи php+mysql+apache
Задача по разграничению доступа к файлам, которые хранятся на диске довольно редка, но она может возникнуть при написании: online-магазина, который торгует файлами или файлового сервера вроде rapidshare.de. В данной статье я рассмотрю 3-и способа разграничения доступа при помощи php, mysql и специальных модулей веб сервера apache.
Способ #1: использование символьных ссылок
Это самый простой на мой взгляд способ, он не требует установки каких-то дополнительных модулей apache, но в то же время, в чистом виде, это наименее гибкий метод и работать он будет только на сервере под управлением *nix. Темнеменее это метод прекрасно подойдет для файлового хостинга.
Что нужно сделать:
1. Создадим две директории, в одной из которых будут храниться все файлы, доступ к которым нужно ограничить, а вторая пока будет пуста. Для примере я создал у себя директории: members и free;
3. Для примера создадим в директории members файл test.html. Если обратиться к этому файлу через http то получим Forbidden, т.е. пользователь даже имея прямую ссылку на этот файл не сможет его скачать;
Так же можно и нужно примешать к названию символьной ссылки какой-нибудь хеш и время, до которого она будет активна. Для удаления просроченных ссылок довольно просто можно написать CLI скрипт и выполнять его по крону раз в N минут.
Способ #2: использование модуля mod_auth_mysql
1. Скорее всего в базе данных вашего сайта уже есть таблица, в которой хранится список зарегистрированных пользователей сайта, в этом случае вам нужно будет добавить в эту таблицу поле в котором вы будете хранить md5 хеши паролей (если конечно вы их так уже не храните). Но т.к. у меня всего этого нет то я создам таблицу с нуля:
CREATE TABLE users (
id int(11) unsigned not null auto_increment primary key,
login CHAR(50) NOT NULL,
password CHAR(50) NOT NULL,
unique key login_idx (login)
)
Как видно из структуры таблицы поле login должно быть уникальным и не должно содержать значение NULL;
Способ #3: использование модуля mod_auth_cookie_mysql
Этот модуль по своей функциональности очень похож на модуль mod_auth_mysql. Но всеже этот модуль имеет 2-а существенных отличия:
1. Он может брать информацию для авторизации из cookie пользователя;
2. В cookie не фигурирует логин/пароль пользователя, что сводит на нет возможность их кражи.
Скачать модуль и ознакомиться с его документацией можно здесь.
# Активируем модуль mod_auth_cookie_mysql
AuthCookieSql on
# Обязательные параметры для подключения к базе данных
AuthCookieSql_DBhost
AuthCookieSql_DBuser
AuthCookieSql_DBpassword
AuthCookieSql_DBName
# Имя таблицы, в которой хранятся сессии пользователей. Обязательный параметр
AuthCookieSql_DBtable users_sessions
# Названия полей в таблице. Обязательные
AuthCookieSql_SessnameField cookie_name
AuthCookieSql_SessvalField cookie_value
AuthCookieSql_UsernameField login
# Имя cookie переменной, из которой будет взято значение для поиска в базе.
# Опциональный параметр, если он не указан то поиск совпадений в базе
# будет осуществляться для всех cookie пременных установленных на данный момент
AuthCookieSql_CookieName AuthorizationCookie
# Поле таблицы, в котором хранится время окончания действия cookie.
# Параметр опциональный, если не указан то время действия бесконечно.
AuthCookieSql_ExpiryField expire
# Поле таблицы, в котором хранится удаленный IP пользователя.
# Опциональный параметр, если он не указан то IP не проверяется.
CookieAuth_RemoteIPField ip
# Проверяем найдено ли совпадение в базе
require valid-user
3. После успешной авторизации пользователя на сайте сделаем следующие действия:
// Подготавливаем дату окончания действия cookie
$expire = time() + 60 * 60; // cookie будет действовать 1 час
После этих действий мы можем редиректить пользователя на любой файл, находящийся в каталоге members.
Вот собственно и все. На данный момент это все известные мне способы разграничения прав доступа к файлам под управлением веб сервера apache. Если кто-то знает другие — поделитесь ссылкой и я с удовольствием о них почитаю.
Система разделения прав доступа в веб-приложении
В этой статье мы пройдём с вами полный цикл от идеи, проектирования БД, написания PHP-Кода, и завершающей оптимизации. Постараюсь рассказать обо всем, как можно проще. Использовать для примеров буду PHP и Mysql. Заодно потренирую новичков :).
В этой статье я коснусь вопросов:
1. Идея ACL
2. Проектирование БД
3. Нормализация БД
4. Рефакторинг кода
5. Оптимизация рабочего кода
Статья является ответом на Бинарное распределение прав доступа в CMS. Пока автором пишется практическая часть, я хочу предоставить мой вариант, который я использую довольно давно.
То, что я сейчас расскажу, похоже на ACL.
Упрощенное описание идеи
Права доступа принадлежат ко всем объектам, к которым необходимо их применять.
Если рассматривать пример простой страницы новостей (которую мы с вами здесь напишем), то права доступа должны иметь:
1) Основная страница новостей — глобальные права доступа, означающие «создание новой новости», «модерирование новостей», «просмотр самой страницы».
2) Каждая новость — возможности «редактировать автором новости» или «не оставлять комментарии».
Система прав доступа состоит из:
Group — это набор имен, которые из себя представляют:
1) Права конкретного пользователя (например ‘User1’, ‘User2’. ). Например используется для личных сообщений, к которым этот пользователь имеет доступ или для допуска редактирования только его сообщений на сайте.
2) Группы приватных страниц (или групп пользователей), к которым необходимо дать права на определенные действия. (например, администраторами, супермодераторами и др.)
3) Дополнительные свойства. (Например, флаг — переключатель режимов)
Action — набор действий, которые пользователи с имеющимся
N — добавлять новую тему
D — удалять тему
E — редактировать тему
V — видеть тему
C — оставлять комментарий
B — удалять комментарий
± означает давать пользователю с такими правами или не давать (приоритетно) доступ к действию. Например: Users+VC, Users-C = Users+V.
Теперь рассмотрим пример прав доступа, для простого сайта новостей:
Объект MainNewsPage:
Users+VC, Moderator+NEDB, Admin+NEDB
Объект NewsMessage:
User1+ED (в принципе не нужно, если добавлять могут только модераторы)
Users-C (можно использовать, если нет желания оставлять комментарии)
Объект NewsComment:
User2+B (а здесь необходимо, так как комментарий может оставлять любой пользователь, но не все могут удалять их)
Упростим систему, для понимания идеи компьютером
Для начала, определим Базы данных, для работы с правами объектов.
Так как у нас получается список нескольких прав, то можно начать с такой БД:
RightsID — идентификатор списка прав.
Group — название группы.
Sign — знак группы.
Action — название действия.
Нормализация таблиц. Добавляем права пользователя.
У пользователя, который будет просматривать нашу страницу, должны иметься права, которыми он владеет. Исходя из них, мы сможем знать, имеет ли пользователь право на действие.
Например: User2, Users, Moderator.
Для этого определим таблицу прав:
RightsID — идентификатор списка прав пользователя.
Group — название группы, в которой пользователь состоит.
Пример:
ID | RightsID | Group |
---|---|---|
1 | 10 | User1 |
2 | 10 | Users |
3 | 10 | Moderator |
Теперь приведем обе наши таблицы к нормальной форме. (wiki)
В результате ID ключи отсеются, и мы получим 3 таблицы:
rights_action — права объекта
RightsID: integer (pk) — идентификатор списка прав.
GroupID: integer (pk) — название группы.
Sign: tinyint (1) — знак группы.
Action: enum (pk) — название действия.
rights_group — права пользователя
RightsID: integer (pk) — идентификатор списка прав пользователя.
GroupID: integer (pk) — идентификатор группы, в которой пользователь состоит.
rights_names — названия групп
GroupID: integer (pk) — идентификатор группы.
name — название группы.
Primary key ‘ID’ мы заменили на другие ключи, состоящие в некоторых случаях из нескольких полей таблицы.
Знак группы теперь 0 (+) или 1 (-), потому что так нам будет проще к ним обращаться.
Идентификатор GroupID прямиком указывает на название в rights_names.
На самом деле таблица rights_names является в нашем случае аппендиксом, который не будет использоваться для выявления прав на необходимое действие. Эта таблица теперь служит лишь для «Очеловечивания» результатов.
Пример, что у нас получилось:
rights_name | |||
---|---|---|---|
GroupID | name | ||
10 | Users | ||
11 | Moderator | ||
12 | Admin | ||
1001 | User1 | ||
1002 | User2 | ||
1003 | User3 | ||
rights_group | |||
RightsID | GroupID | ||
1 | 1001 | ||
1 | 10 | ||
1 | 11 | ||
rights_action | |||
RightsID | GroupID | Sign | Action |
100 | 10 | 0 | message_view |
100 | 10 | 0 | comment_create |
100 | 11 | 0 | message_create |
100 | 11 | 0 | message_edit |
100 | 11 | 0 | message_delete |
100 | 11 | 0 | comment_delete |
100 | 12 | 0 | message_create |
100 | 12 | 0 | message_edit |
100 | 12 | 0 | message_delete |
100 | 12 | 0 | comment_delete |
101 | 1001 | 0 | message_edit |
101 | 1001 | 0 | message_delete |
101 | 10 | 1 | comment_create |
Стало менее наглядно — для человека. Компьютеру, который оперирует числами, стало намного проще обращаться с таблицами.
Теперь мы можем добавить права любому объекту в таблице на любое действие. Действия теперь записываются в таблицу в виде ENUM (поле ‘action’), что упрощает понимание и разработку проектов. Само действие как string и может называться, как угодно.
`rights_group` должна быть привязана к пользователям и говорит о тех правах, которыми пользователь обладает.
`rights_action` должна быть привязана к объектам и говорит, с каким правами, какие действия пользователь может выполнять.
Например (для нашего сайта новостей):
news_page (параметры основной страница с новостями) | ||||
---|---|---|---|---|
PageID | RightsID | Name | ||
1 | 100 | Страница новостей | ||
news_message (сообщения на странице новостей) | ||||
MsgID | PageID | RightsID | Header | Message |
1 | 1 | 101 | Ура, мы на главной. | Но это только начало, дальше, когда мы ближе подберемся к администрации хабра. |
2 | 1 | 101 | Новости последней недели | Не смотря на наше стройное шествие по главной, похоже планы обломались. |
Разработка библиотеки работы с правами доступа
А сейчас мы посмотрим, что необходимо, чтобы эти таблицы собрать воедино и произвести необходимую нам выборку результатов.
Алгоритм наших действий при проверке возможности действия:
1) Берем из БД выборку прав по необходимому объекту (объектам). (100: Users+VC, Moderator+NEDB, Admin+NEDB)
2) Выбираем необходимые нам действия (action). (V: Users+V)
3) Сравниваем права доступа пользователя и нашей выборки. (Users, User1 Users+)
4) Если результатов нет, тогда возвращаем false.
5) Если результаты есть, но состоят из минусов, возвращаем false. Иначе возвращаем true.
Ещё один момент, который стоит заметить: права доступа от parent (страницы новостей) переходят к child (в данном случае, к сообщению). То есть, если указать на странице +’message_view’, то все сообщения автоматически будут с такими правами (чтения). Это обстоятельство мы будем использовать и проверять в пункте 1 нашего алгоритма.
Пункт 2.
С выбором необходимого нам действия, тоже всё просто:
SELECT * FROM `rights_action` WHERE `action`= ‘message_view’
Пункт 3.
А вот здесь придется объединить несколько SELECTов. Сначала выбираем права доступа пользователя, а затем сравниваем их с необходимыми нам. Если объединить эти действия в одно, получится:
SELECT * FROM `rights_action` WHERE `GroupID` IN ( SELECT `GroupID` FROM `rights_group` WHERE `RightsID` = 1)
Пункт 1-3.
Теперь всё вместе одним сложным запросом:
SELECT * FROM `rights_action` WHERE `RightsID` IN (100, 101) AND `action`= ‘message_view’ AND `GroupID` IN ( SELECT `GroupID` FROM `rights_group` WHERE `RightsID` = 1)
Пункт 1-5.
Вставим это в PHP реализацию и заодно добавим проверку:
if (!$result)
return false ;
Это лишь начало, первый шаг.
Создаём класс работы с правами доступа
Что нам необходимо?
Разработаем пример работы с нашим классом.
1) Во-первых, необходимо указать права пользователя, с правами которого нам надо работать. А сам класс необходимо привязать к конкретному пользователю.
2) Добавление в класс child-прав объектов с дополнением свойств.
3) Проверка доступа по различным действиям (action).
Замечания и мысли:
Права пользователя можно указать при создании класса в __construct.
При добавлении новых свойств, чтобы не терялись свойства в классе, необходимо будет делать новый класс (клонировать старый с добавлением свойств).
Давайте попробуем это всё реализовать:
$UserRights = new Rights($CurrentUser->rightsID);
Рассмотрим добавление права объекта для проверки:
Напомню, что портить объект нам не нужно, потому что мы будем к нему (parent) добавлять права от разных сообщений (child).
Теперь перепишем нашу функцию check, введя её в класс, и посмотрим что получилось:
if (!$result)
return false ;
То, что мы сейчас с вами проделали (переделали функцию в класс, сделав её удобнее в использовании и универсальнее) называется Рефакторинг. (wiki)
Сейчас этот класс можно использовать вот так:
//Создаём права пользователя
$UserRights = new Rights($CurrentUser->rightsID);
//Проверяем, может ли пользователь просматривать страницу?
if ($PageRights->check( ‘messages_view’ )) <
//Да, может. Но что делать с сообщениями?
//И проверяем на читаемость
if ($MsgRights->check( ‘messages_view’ )) <
//И если оно читается, проверяем можем ли мы редактировать сообщения?
if ($MsgRights->check( ‘messages_edit’ ))
$msg->editable_flag = 1;
//А удалять сообщения?
if ($MsgRights->check( ‘messages_delete’ ))
$msg->delete_flag = 1;
где $CurrentUser — структура пользователя, который смотрит страницу.
$MainPage — структура страницы, который смотрит пользователь.
$MainPage->Messages — массив сообщений, которые выводятся на странице.
Структуры, предварительно, были считаны из БД.
Оптимизация
Качеством и функциональностью библиотеки мы с вами довольны, но возникает вопрос производительности.
Первое, что бросается в глаза — это при каждой проверке с новым ‘action’, происходит не хилый SQL запрос. Давайте попробуем это исправить.
Для начала посмотрим, что от запроса к запросу не меняется и оптимизируем это.
function __construct($grp) <
$result=mysql_query( «SELECT `group_rights`.groupID FROM `group_rights` WHERE `group_rights`.rightsID=$grp» );
Уже стало легче, но все равно происходит поиск по всей БД каждый запрос. Как нам от этого можно избавиться? Вероятнее всего, создать предварительный результат, зависящий только от action — потому что выборка `RightsID` и `GroupID` остается неизменной.
Когда добавляется группа объектов, считываем все результаты из БД в массив, который будет зависеть лишь от значений ‘action’.
SELECT * FROM `rights_action` WHERE `RightsID` IN (. ) AND `GroupID` IN (. )
Далее, уже перебором по каждому ‘action’ в массиве, ищем необходимый элемент. При этом запросов в БД больше нет — до следующего объекта с новыми правами.
В результате оптимизации, наш класс будет выглядеть вот так:
if ($tmp) <
return (array_search(0,$tmp)!==FALSE);
>
return false ;
>
function __construct($grp) <
$result=mysql_query( «SELECT `group_rights`.groupID FROM `group_rights` WHERE `group_rights`.rightsID=$grp» );
Можно ли ещё быстрее?
Да, можно.
1) Если учитывать, например пустые группы прав у сообщений (child), которые не поменяют нашу уже используемую временную таблицу. В этом случае мы можем использовать её, не создавая заново. А для проверки, нам необходимо добавить лишь ещё один SELECT count(*) FROM `action_rights` WHERE `GroupID` =…, который пройдётся по индексу и вернет результат.
2) Правильно расставить индексы в таблицах `action_rights` и `group_rights`.
Тут я не уверен. Эксперты меня надеюсь, поправят. Лично сделал PK — ‘rightsID’, ‘action’, ‘groupID’, INDEX — ‘groupID’, ‘rightsID’
4) Использовать кеш. Но это уже другая история 🙂
Работающий пример
Я думаю, что уже достаточно на сегодня кода. Вот как это работает:
работающий пример — извиняюсь за не наглядность.
test.php (рабочий пример) — здесь используются мои библиотеки, которые работают с SQL БД, не удивляйтесь. Уверен, что разберетесь.
rights.php — наша библиотека.
Расширяемость
Любые новые действия, которые вы будете использовать в вашем проекте добавляются в ‘action’ ENUM.
Если вы не хотите быть привязанным к конкретным действиям и добавлять их в реальном времени, то стоит заменить ‘action’ ENUM на integer и создать ещё одну таблицу соответствий actionID с action_name. (как мы сделали это с названиями Групп)
Реализация механизма разграничения прав доступа к админ-части
На своей практике веб-разработки я очень часто сталкивался с ситуациями, в которых заказчики ставили конкретную цель, а именно о разделении частей админки относительно доступности тем или иным пользователям. При этом разработка данного модуля велась в контексте расширяемой системы, а то есть с нефиксированым числом модулей, к которым организовуется доступ, ну и, соответственно, неограниченным числом пользователей системы.
Что ж, сама по себе данная тема довольно грузная, и требует определённого времени на анализ и постанувку задачи.
В контексте данной статьи, мы будем вести разработку в контексте некоторой абстрактной информационной системы, со своей инфраструктурой и архитектурой, при этом данная система предоставляет пользователю возможность расширять функционал, а то есть устанавливать новые модули, и соответственно устанавливать права доступа к ним тому либо иному пользователю, зарегистрированному в качестве администратора системы.
Давайте с самого начала обсудим архитектуру модульной системы на выбранной нами псевдо-системе.
Все модули представлены ввиде подключаемых к главному документу (индекс-файлу) вставок. Запрос модуля происходит из строки запроса QUERY_STRING, и название подключаемого модуля передаётся в качестве аргумента act. В некотором месте индекса файла происходит изъятие и обработка данного параметра. После, если у пользователя достаточно прав для доступа к модулю в контексте чтения, происходит проверка существования указанного в строке запроса модуля, и если таковой существует, то происходит его подключение к индекс файлу.
Для воплощения данного механизма мы будет проверять значение переменной строки запроса `do`, которая обрабатывается в самом модуле и носит информацию о том, к какому разделу модуля необходимо предоставить доступ пользовалю.
Значение do буду фиксированными, данная переменная будет принимать следующие значения:
В целом, этот список можно увеличить, при этом всё зависит лишь только от масштабов проекта и его потребностей в функционале.
Теперь непосредственно о модулях. Кроме физического существования некоторого модуля в контексте файловой системы проекта, модуль так же должен быть добавлен в особую таблицу БД, которая будет содержать информацию о всех существующих модулях в системе. Добавление и изменение данных данной таблицы, обычно, производится непосредственно в контексте модулей, а то есть во время их инсталяции в системе. Однако это уже углубление в принципы посмотроения расширяемых систем, о чём мы как-то в другой раз поговорим, и посему, мы ограничимся ручным обновлением и добавлением данных о модулях.
Кроме модулей у нас будут ещё две таблицы, а именно таблица в которой будут хранится данные относительно профилей прав доступа и таблица с информацией о пользователях непосредственно.
Что ж, давайте рассмотрим эту особую структуру. Она будет следующей: [ module_indefier : [0 | 1]+ \: [0 | 1]+ \;] *
То есть идёт список из пар: имя модуля «:» права чтения «,» права записи «;». При этом данная метка обновляется в момент внесения изменений о правах доступа пользователя к системе. Если в системе появляется информация о модуле, который не вошёл в данную метку, то стоит просто произвести процедуру редактирования, и данные сохранятся автоматически.
Теперь же нам осталось рассмотреть структуру всего одной таблицы БД, и мы сможем принятся за реализацию алгоритмической части, а именно таблицы с информацией о пользователях системы, ведь назначение им прав доступа и является нашей главной задачей.
Я не буду добавлять ничего лишнего в неё, но лишь то, что будет использоватся в контексте темы данной статьи. Таблица пользователей будет содержать следующие поля: идентифицатор пользователя (числовой счётчик), логин, пароль (хеш оригинального пароля), профиль безопасности пользователя (идетификатор группы пользователя, относительно прав в системе), и всё. Мне кажется этой информации нам с вами вполне хватит, для реализации поставленной задачи, а уже все остальные надстройки я предоставляю возможность сделать самим.
Итак, структуру мы обсудили, и, надеюсь, у всех сложилось уже некоторое представление о том, как мы будем реализовывать поставленную в теме статьи задачу. Сейчас я приведу вспомогательный SQL-код таблиц, описанных выше, после чего сразу же перейду к воплощению алгоритма проверки прав доступа пользователя, а так же создания и изменения профилей доступа. После каждого отдельного модуля мы подробно обсудим все вопросы, которые могут возникнуть у читателей.
Далее описан класс для внедрения функций проверки прав доступа пользователей к модулям системы.
Данный класс внедряет функции, предназначенные для воплещения алгоритмического задания, описанного выше. Сейчас мы обсудим каждую функцию отдельно.
Функция secure::getUserId()
Функция secure::getUserSecurityAccess($id)
На выходе данная функция возвращает идентификатор профиля безопасности текущего пользователя в системе.
Функция secure::checkUserPermission($module,$act))
Производится запрос к БД, относительно прав пользователя на произведение действий чтения/записи в контексте переданного в качестве параметра модуля.
Процедура авторизации будет выглядеть ввиде внесения личных данных пользователя (логин и пароль) в специальную форму, после отправки которой произойдёт обработка данных, переданных пользователем, по-методу функции checkAuthData(), и, в случае корректности данных, будет произведено сохранение данных о пользователе ввиде куки записи на период установленный пользователем, либо в отсутствии заданного значение на период по-умолчанию.
Я не привожу форму для отправки, так как это не часть теории программирования, указав лишь идентификаторы полей.
Вот, в целом и всё. Осталось лишь пробовать, экспериментировать, ошибатся и находить решение, что я всецело и оставляю вам.
С уважением Карпенко Кирилл, глава IT-отдела ИНПП.