::: PHP.com.ua - учимся вместе. ::: ::: PHP.com.ua - учимся вместе. :::



 
   - Помилка при установці
  - header
  - Я канєшно вибачаюся, ну от вирішив навчи...
  - Трудности с включенным safe mode, не вид...
  - Поможіть з установкою локальки
  - Помогите с функцией utf2win1251
  - Вывод окна предупреждения


Главная
Новости
Статьи
Шпаргалки
Файлы
О проекте
Форум
Футболки


FREEhost.com.ua - купил хостинг 10 у.е. на Begun в подарок.

iName.com.ua - регистрация доменных имен и хороший хостинг.

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

Designclub - Клуб дизайнеров Украины.

Регистрация доменов
Хостинг

 HowtoForge.ORG.UA - Это первый Украинский ресурс развития open source программного обеспечения


Путь: Статьи > Безопасность

Безопасность

Автор: - Yurik
Дата публикации - 11.05.2005
Просмотров: - 21011

Защищенная авторизация (сессии+javascript)


[p]Предпоcылки[/p]

Существенным требованием к средствам авторизации есть гибкость, которая не всегда достигается стандартными методам. Не всегда можно сконфигурировать сервер для защищенного SSL канала, а обычные средства авторизации (HTTP, сессии) не обеспечивают надежной защиты. Данный метод полностью системно независим и использует только PHP на стороне сервера и JavaScript на стороне клиента. Используется только стандартная функция MD5 (32-битное хеширование, RSA Message Digest Algorytm), JS имплементацию которой любезно предоставил Генри Торжман.
MD5 JavaScript

Скачайте себе md5.js который делает MD5 хеш из любой строки. Есть несколько версий его имплементации на JavaScript:
url=http://opensource.polytechnique.org/cvs/diogenes/md5.js?cvsroot=Public#rev1.1]http://opensource.polytechnique.org/cvs/diogenes/md5.js?cvsroot=Public#rev1.1/url]
url=http://pajhome.org.uk/crypt/md5/md5src.html]http://pajhome.org.uk/crypt/md5/md5src.html/url]
url=http://www.myersdaily.org/joseph/javascript/md5.js]http://www.myersdaily.org/joseph/javascript/md5.js/url]
url=http://www.onicos.com/staff/iz/amuse/javascript/expert/md5.txt]http://www.onicos.com/staff/iz/amuse/javascript/expert/md5.txt/url]

Данные имплементации не проверена на совместимость с non-ascii символами. Вероятнее всего кириллица не будет правильно хешироваться. Также скрипт о-очень медленно работает на броузере Opera 6. Также нужно проверять включена ли поддержка javascript вообще (об этом позже).

[p]Суть метода[/p]

Идея авторизации лежит в том, что клиент посылает логин и пароль через обычную форму, но в зашифрованном с помощью случайного ключа виде (challenge response). Без случайного ключа хеширование не было бы оправдано, а в нашем случае, если злоумышленник перехватит хеш, он не сможет его использовать, потому что сервер уже не знает того случайного ключа ($nonce), который посылался для хеширования. Для того, чтобы из перехваченных данных получить пароль, лучшего метода чем перебор не существует. При соответствующих требованиях к паролю усилия на расшифрование будут неоправданны.

Хотя метод достаточно защищенный, его надежность компрометируется самим механизмом сессий, который предусматривает передачу идентификатора сессии в открытом виде (через УРЛ или Куку). К тому же метод нету смысла использовать для защиты самой информации (информация все равно идет открітім каналом), а только для ограничения доступа к редактированию динамического контента
Реализация

Для авторизации необходимо создать 2 PHP скрипта. Первый генерирует случайную строку и посылает клиенту вместе с формой логина и пароля. Даная форма обрабатывается JS, который шифрует введенный пароль и отсылает на другой скрипт. Другой скрипт помнит этот ключ, получает зашифрованный пароль и логин, находит в своей базе (в нашем примере MySQL таблица) соответствующий логину пароль (точнее соответствующий ему MD5 хеш). Он той же функцией MD5, хеширует и сверяет с полученной строкой. Если все этапы прошли успешно на клиенте открывается идентификатор сессии, который дает ему право работать с защищенными скриптами.

Теперь к тому как это все работает

[b]login.php[/b]
[php]
<?php 
function make_seed() { 
    list(
$usec$sec) = explode (' 'microtime()); 
    return (float) 
$sec + ((float) $usec 100000); 

mt_srand(make_seed()); 
$nonce=mt_rand(110000000); #генерируем слуайное число 
$nonce=md5($nonce); #превращаем это число в случайную строку 
$_SESSION['nonce']=$nonce#запоминаем наш случайный ключ к следуйщей странице 
?> 

<html> 
<head> 
<script language="javascript" src=md5.js> 
</script> 
<script language="javascript"> 
function doSend(){ 
document.login.password.value=MD5(document.login.pass.value); 
document.login.password.value=MD5(document.login.password.value+document.login.nonce.value); 
document.login.pass.value=''; // NOT to send password as a plain text 
document.login.nonce.value='';// NOT to send random key 

</script> 
</head> 
<body> 
<form name="login" method="post" action="loginrespond.php" onSubmit="doSend()"> 
  <input type="text" name="login" value="<?=@$_COOKIE['lastlogged']?>"> 
  <input type="password" name="pass"> 
  <input type="submit" value="Log in"> 
  <input type="hidden" value="<?=$nonce?>" name="nonce"> 
  <input type="hidden" value="" name="password"> 
</form> 
</body> 
</html> 
[

/php]

Дальше необходимо обработать результат отсылки формы авторизации:

[b]loginrespond.php[/b]
[php]
<?php 
session_start
(); 
if (!isset(
$_SESSION['nonce']) || strlen($_SESSION['nonce'])!=32) die('Illegal challenge response. Possible hack'); 

#--------- Получение пароля из MySQL----------------- 
#- Здесь может быть любой способ получения пароля-- 
$host='localhost'$user='inet'$pass='po32jlkdjl3'$db='baza'
mysql_connect($host$user$pass); 
mysql_select_db($db); 
$sql="SELECT * FROM tblusers WHERE login='".mysql_escape_string(Trim(@$_POST['login'])); 
$sql.="' AND activate=1 AND MD5(CONCAT(pass, '".$_SESSION['nonce']."'))='";
$sql.=mysql_escape_string(@$_POST['password'])."'";
$result = @mysql_query($sql) or die('Query failure: '.mysql_error()); 
if (!
mysql_num_rows($result)) { 
die(
'Неправильный логин или пароль'); 
} else { 
$row=mysql_fetch_array($result); 
mysql_free_result($result); 

if (
$row['requireip']) { 
    
$sql='SELECT * FROM tblip WHERE ip='.ip2long($_SERVER['REMOTE_ADDR']); 
    
$result=$mysql_query($sql); 
    if (!
mysql_num_rows($result)) { 
     
$msg="<span class=hot>".$_SERVER['REMOTE_ADDR'].":</span><br>"
     
$msg.="Запрещена работа с этого IP"
     die(
$msg); 
    } 


if (isset(
$row['expires'])) { 
    if (
strtotime($row['expires'])<time()) die('Строк действия вашего доступа исчерпан'); 


// присваиваем переменной сессии уровень доступа для данного юзера 
$_SESSION['auth']=$row['authlevel']; 
$_SESSION['userid']=(int) $row['id']; 
$_SESSION['username']=$row['name']; 

/* PHPSESSID Anti-spoofing */ 
$_SESSION['REMOTE_ADDR']=$_SERVER['REMOTE_ADDR']; 
$_SESSION['HTTP_X_FORWARDED_FOR']=@$_SERVER['HTTP_X_FORWARDED_FOR']; 
$_SESSION['HTTP_USER_AGENT']=$_SERVER['HTTP_USER_AGENT']; 

// запоминаем в куку логин пользователя 
setcookie("lastlogged"$row['login'],time()+3600*24*30); 

header("Location: protected.php"); 
exit; 

?>[

/php]

В приведенном скрипте мы кроме простого сверения паролей проделали ряд полезных вещей

Проверка WHERE activate=1 дает возможность временно отключать юзерам доступ (activate=0)

Проверка поля expires (срок, когда доступ истекает). Если поле не NULL, то проверяется наступила ли уже указання дата

Если поле requireip установлено, то выполняется дополнительная привязка юзера к списку IP-адресов (в нашем случае дополнительная MySQL таблица)

Здесь можно задать общий список white list или привязывать конкретных юзеров к конкретным IP (по userid). Используйте эту проверку осторожно, например если вы постоянно работаете из некоторых IP и хотите вообще ограничить заход с других IP даже если пароль был скомпрометирован. Для других пользователей такую опцию можно предоставлять при регистрации, если они укажут например "Пускать только с этого IP"

[p]Примерная структура БД (для нашего примера)[/p]

Заметим что пароль в базе тоже лежит в md5. Нужно это чтобы люди имеющие доступ к БД не могли визуально смотреть чужие пароли (использовать md5 для авторизации не узнавая сам пароль они все равно смогут)

[code]CREATE TABLE tblusers (
id mediumint(8) unsigned NOT NULL auto_increment,
authlevel mediumint(8) unsigned NOT NULL,
name varchar(30) NOT NULL,
login varchar(15) NOT NULL,
pass varchar(32) NOT NULL,
registered datetime NOT NULL,
activate tinyint(1) unsigned NOT NULL,
requireip tinyint(1) unsigned NOT NULL,
expires datetime default NULL,
PRIMARY KEY (id),
UNIQUE KEY login (login,activate)
) TYPE=MyISAM;

# и структура таблицы tblip

CREATE TABLE tblip (
id smallint(5) unsigned NOT NULL auto_increment,
iduser smallint(5) unsigned NOT NULL,
ip int(11) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY ip (ip, iduser)
) TYPE=MyISAM
[/code]

[p]Защита страниц[/p]
Дальше создаем скрипт

[b]protect.php[/b]
[php]
<?php 
if (!isset($_SESSION['auth'])){ 
header("Location: login.php"); 
exit; 

if (
$_SESSION['auth']<$required_level){ 
die(
"Недостаточный уровень доступа"); 

?>[

/php]

и на нужных страницах включаем его:
[php]
<?php 
$required_level
=100
include(
'protect.php'); 
.... 
.... 
?>[

/php]

[p]Когда javascript отключен[/p]

Если у клиента отключен javascript, авторизацию все равно можно проводить. Только пароль тогда будет посылаться как есть, открытым текстом. Конечно в скрипт loginrespond.php нужно вставить соответствующую проверку и или прекращать работу с предложением включить JS или использовать пароль $_POST['pass'] из формы как есть.

[p]Выход (logout)[/p]

Два способа

[php]
<?php
session_destroy
(); 
// или
$_SESSION['auth']=NULL;
?>
[

/php]

Когда и как вызывать одну из этих конструкций - дело вашей фантазии
[p]Антихакер[/p]

Как было сказано, перехват PHPSESSID дает возможность работать от имени уже авторизованого пользователя. Перехватить его не так трудно, а в случае если session_save_path=/tmp вообще очень просто (`ls /tmp). Значительно утруднить задачу хакера можна когда в сессию добавить IP адрес клиента из какого была открыта сессия (можна еще и строку $USER_AGENT впихнуть):

[php]
<?php 
..... 
$_SESSION['auth']=$row['authlevel']; 
$_SESSION['userid']=$row['id']; 
$_SESSION['username']=$row['name']; 

$_SESSION['ip']=$_SERVER['REMOTE_ADDR']; 
$_SESSION['xip']=@$_SERVER['HTTP_X_FORWARDED_FOR']; 
$_SESSION['browser']=$_SERVER['USER_AGENT']; 
.... 
?> [

/php]

а потом на каждой странице (в security.php) проверять отличается ли текущая инфа от начальной и в случае чего делать session_destroy();

[php]
<?php 
$check
=Array( "ip" => "REMOTE_ADDR""xip" => "HTTP_X_FORWARDED_FOR""browser" => "HTTP_USER_AGENT"); 
foreach (
$check as $key=>$value) if (@$_SERVER[$value]!= $_SESSION[$key]) session_destroy(); 
?>[

/php]

[p]Многоуровневой доступ[/p]

Как видно из скрипта security.php, мы применили многоуровневую систему доступа по аналогу Mul-T-Lock. Каждый юзер имеет свой authlevel (уровень доступа), а на страницах мы ставим минимальный уровень для доступа. Эту проверку можно использовать на странице многократно, если она предполагает много действий с разным уровнем доступа.

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

[code]CREATE TABLE tblgrantlist (
idgrant smallint(5) unsigned NOT NULL auto_increment,
idgroup smallint(5) unsigned NOT NULL,
pravo varchar(12) NOT NULL,
descr varchar(100) NOT NULL,
sort smallint(6) NOT NULL,
PRIMARY KEY (idgrant),
UNIQUE KEY pravo (pravo)
) TYPE=MyISAM
[/code]

где
[b]pravo[/b] - короткое латинское название права, например viewThis, doThat
[b]descr[/b] - текстовое описание этого права (выводится юзеру когда он его не имеет)

Для того чтобы удобно было раздавать права, лучше их объединить в группы и потом раздавать целыми группами сходных действий (если прав десяток-два то можно и не группировать, но когда их за сотню и постоянно регистрируются пользователи, то перекликать сотню флажков становится накладно). Группировку сделаем по ключу idgroup с другой таблицей:

[code]CREATE TABLE tblgrantgroups (
idgroup smallint(5) unsigned NOT NULL auto_increment,
title char(50) NOT NULL,
sort smallint(6) NOT NULL,
PRIMARY KEY (idgroup)
) TYPE=MyISAM
[/code]

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

Наконец сама таблица где лежат права отдельно взятого пользователя:

[code]CREATE TABLE tblgrants (
id mediumint(8) unsigned NOT NULL auto_increment,
idgrant smallint(5) unsigned NOT NULL,
iduser mediumint(8) unsigned NOT NULL,
val tinyint(1) unsigned NOT NULL default '1',
PRIMARY KEY (id),
UNIQUE KEY pravo (idgrant, iduser)
) TYPE=MyISAM
[/code]

В программе будем проверять есть ли для текущего iduser право idgrant (если val=0 то значит это право отключено)

[p]Класс для обработки[/p]

Предположим у нас есть класс Auth который отвечает за авторизацию, который при успешной авторизации хранит атрибут iduser. Создадим класс Perms отвечающий за проверку прав доступа

[b]perms.php[/b]
[php]
<?php 
Class perms Extends Auth 
var 
$loadedgrants
function 
perms(){ 
  if (
$this->iduser>0) { 
    
$_sql="SELECT pravo FROM tblgrants, tblgrantlist"
    
$_sql.=" WHERE tblgrants.idgrant=tblgrantlist.idgrant"
    
$_sql.=" AND iduser=".$this->iduser." AND val>0"
    
$_result=$this->query($_sql__LINE____FILE__); 
    
$_tmp[]=NULL
    while (
$_row=mysql_fetch_row($_result)) $_tmp[]=$_row[0]; 
    
$this->loadedgrants=$_tmp
  } else { 
    
$this->loadedgrants=NULL
  } 
// конструктор класса просто загружает все права которые 
// есть у юзера в аттрибут-массив $loadedgrants 


function 
hasGrant($_checkg$_stop=false){ 
  if (
array_search($_checkg$this->loadedgrants)===false && 
      
array_search('su'$this->loadedgrants)===false
    if (
$_stop$this->noGrants($_checkg); 
    else return 
false
  else 
    return 
true
// метод проверяет наличие права, а если вызван со вторым параметром TRUE, 
// то оканчивает работу скрипта с выдачей сообщения 


function 
noGrants($_checkg){ 
  @
ob_end_clean(); 
  echo 
"<html><head><title>"
  echo 
"У Вас нету достаточно полномочий"
  echo 
"</title></head><body>"
  
$_sql="SELECT descr FROM tblgrantlist WHERE pravo='".$_checkg."'"
  
$_result=$this->query($_sql__LINE____FILE__); 
  if (
$_row=mysql_fetch_row($_result)) echo "<h3>Вы пробуете: "<u>".$_row[0]."</u>"</h3>"
  echo 
"<h4>Недостаточно прав для осуществления операции</h4>"
  echo 
"<input type=button value='< Назад' onClick='history.back()'>"
  exit; 


}
?> 
[

/php]

[p]Использование класса[/p]

[php]
<?php 
require_once('perms.php');
$p=new Perms

// проверка с остановом скрипта 
$p->hasGrant('viewOrders'True); 

// условная проверка 
if ($p->hasGrant('viewdetails')) echo "Additional info"

if (
$p->hasGrant('orderedit')) echo "<a href=edit.php>"
else echo 
"Edit blocked"

?>[

/php]



Обсудить в ФОРУМе - комментариев ()


Путь: Статьи > Безопасность

Если вы заметили орфографическую, стилистическую или другую ошибку на этой странице, просто выделите ошибку мышью и нажмите Ctrl+Enter.
Контакты Design by webFaction Ukrainian PHP Group 2004-2005