Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Ясные печеньки

Anna | 29.05.2014 | нет комментариев
Здравствуй, Прогр! В свете информации, посвященной безопасности аккаунтов больших порталов, появившейся в последнее время, я решила немножко пересмотреть cookie авторизацию в своих планах. В первую очередь был допрошен с увлечением гугл на тему готовых решений. Ничего толкового не нашлось, правда может статься и так, что я не умею пользоваться поиском. Позже этого я решила посмотреть, что же вообще пишут про то, как верно жевать печенье. Моему изумлению не было границ, когда в стержневой массе оказались статьи из разряда «пагубные советы», и то что я читала больше 5-и лет назад.
Эта статья — попытка поправить сложившуюся обстановку.

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

Печенье мы будем беречь как в наилучших домах Филадельфии: в прекрасной жестяной коробочке. То есть при авторизации устанавливается сookie для исключительного каталога, хорошего от document root. В последующем эта cookie применяется только в том случае, когда сессия не установлена, либо данные (IP-адрес жертпользователя) не валидны. На странице проверки cookie не должно быть никакого динамического содержимого.

Сейчас давайте разглядим алгорифм подробнее.

Для начала нам потребуется SQL таблица со дальнейшей конструкцией:

CREATE TABLE `user_auth_cookies` (
 `key` char(32) NOT NULL,
 `user_id` int(10) unsigned NOT NULL,
 `logged_in` datetime NOT NULL,
 PRIMARY KEY (`key`),
 KEY `user_id` (`user_id`),
 KEY `logged_in` (`logged_in`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

кое-какие предустановки, и маленький класс User:

$db = new mysqli('localhost', 'test', '', 'test');
if ($db->connect_errno) die('Не удалось подключиться к MySQL: ('.$db->connect_errno.') '.$db->connect_error);
// думаю здесь все ясно

define('DOMAIN', ($_SERVER['HTTP_HOST'] !== 'localhost' ? $_SERVER['HTTP_HOST'] : false));
// Доменное имя сайта. Если localhost, то false (в отвратном случае куки не выставятся как нужно)
define('AUTO_AUTH_URL', '/auth/auto'); // ссылка на страницу механической авторизации
define('AUTH_URL', '/auth'); // ссылка на форму авторизации
define('AUTH_COOKIE_DURATION', 20); // временной промежуток в днях, на тот, что следует устанавливать куки
define('USE_HTTPS', false); // применять HTTPS для передачи кук (для радостных владельцев подписанных сертификатов)

class User {

	private $_id;

	public $isGuest = true;
	public $name = 'Гость';

	public function __construct() {
		GLOBAL $db;

		if (!isset($_SESSION['user']) || $_SESSION['user']['ip'] !== $_SERVER['REMOTE_ADDR'])
			return;

		$query = 'SELECT * FROM users WHERE `id` = '.(int)$_SESSION['user']['id'];
		if (($res = $db->query($query)) !== false && $res->num_rows) {

			$user = $res->fetch_assoc();

			$this->_id = $user['id'];
			$this->name = $user['name'];
			$this->isGuest = false;

			if (isset($_SESSION['last_request'])) {
				$_POST = $_SESSION['last_request']['data'];
				unset($_SESSION['last_request']);
			}

		} else {
			unset($_SESSION['user']);
		}
	}

	public function getId() {
		return $this->_id;
	}
}

Как видно, применяются две сессионные переменные, одна из них (user) непринужденно для авторизации, а вторая (last_request) для сохранения URI, с которого понадобился запрос авторизации, и параметров POST, Дабы не потерялись данные отправляемые формы, если таковые имелись.

Для авторизации применяется класс Auth, содержащий 4 статических способа.

class Auth {

	public static function loginRequired() {
		$_SESSION['last_request'] = array(
			'url' => $_SERVER['REQUEST_URI'],
			'data' => $_POST
		);

		header('Location: '.AUTO_AUTH_URL);
		die('Перенаправление...');
	}

	public static function login($login, $password, $remember = false) {
		GLOBAL $db;

		$query = "SELECT * FROM users WHERE `login` = '".$db->real_escape_string($login)."';";
		if (($res = $db->query($query)) === false || !$res->num_rows)
			return false;

		$user = $res->fetch_assoc();

		// это для примера, проверка может быть безусловно всякий
		if ($user['password'] !== md5($login.md5($password)))
			return false;

		if ($remember) {
			do {
				$key = md5(mcrypt_create_iv(30));
				$query = "SELECT COUNT(*) AS `cnt` FROM user_auth_cookies WHERE `key` = '".$key."';";

				$count = 0;
				if (($res = $db->query($query)) !== false && $res->num_rows) {
					$row = $res->fetch_assoc();
					$count = (int)$row['cnt'];
				} else
					die('Оплошность запроса к БД.');
			} while ($count > 0);

			$db->query("INSERT INTO user_auth_cookies VALUES ('".$key."', ".$user['id'].", NOW());");

			setcookie('key', $key, strtotime(' '.AUTH_COOKIE_DURATION.' days'), AUTO_AUTH_URL, DOMAIN, USE_HTTPS, true);
		}

		$_SESSION['user'] = array(
			'id' => $user['id'],
			'ip' => $_SERVER['REMOTE_ADDR'],
		);

		return true;
	}

	public static function loginByCookie() {
		GLOBAL $db;

		$location = AUTH_URL;

		if (isset($_COOKIE['key'])) {

			$query = "SELECT user_id FROM user_auth_cookies WHERE `key` = '".$db->real_escape_string($_COOKIE['key'])."';";

			if (($res = $db->query($query)) !== false && $res->num_rows) {

				$row = $res->fetch_assoc();

				$_SESSION['user'] = array(
					'id' => $row['user_id'],
					'ip' => $_SERVER['REMOTE_ADDR']
				);

				$location = '/';
				if (isset($_SESSION['last_request']))
					$location = $_SESSION['last_request']['url'];
			}
		}

		header('X-Frame-Options: DENY'); // охрана от встраивания в FRAME/IFRAME
		header('Location: '.$location);
		die('Перенаправление...');
	}

	public static function logout() {

		if (!isset($_SESSION['user']))
			return;

		if (mb_strlen($_SESSION['user']['key']) === 32)
			$db->query("DELETE FROM user_auth_cookies WHERE `key` = '".$db->real_escape_string($_SESSION['user']['key'])."';");

		setcookie('key', '', 0, AUTO_AUTH_URL, DOMAIN, USE_HTTPS, true);

		unset($_SESSION['user']);

		header('Location: /');
		die('Перенаправление...');
	}
}

Auth::loginRequired() вызывается, если пользователь не авторизован и переходит на страницу, требующую авторизацию. Способ сберегает нынешний URI и параметры POST запроса в сессионную переменную (сохранение POST данных необходимо, если пользователь писал длинный злой пост, и у него в данный момент сменился IP), и перенаправляет на страницу механической авторизации по cookie.
В контексте класса User:

……
$user = new User();
if ($user->isGuest)
	Auth::loginRequired();
……

Auth::login($login, $password, $remember = false) вызывается, если получена форма авторизации. Параметр $login неожиданно содержит полученный логин, $password не менее неожиданно — пароль, $remember – флаг отвечающий за установку куки.
Пример применения:

……
if (isset($_POST['login']) && Auth::login($_POST['login'], $_POST['password'], !!$_POST['remember_me'])) {

	$location = '/';
	if (isset($_SESSION['last_request']))
		$location = $_SESSION['last_request']['url'];

	header('Location: '.$location);
	die('Перенаправление...');
}
……

Auth::loginByCookie() вызывается на странице механической авторизации. Напомню, что во избежание неприятных обстановок на этой странице не должно быть никакого динамического итога, и не нужно ничего подгружать из других директорий, тем больше доменов. И вообще на директорию скрипта не нехорошо бы установить RewriteRule, перенаправляющее безусловно все запросы на данный скрипт. Возможен так:
.htaccess

<ifModule mod_rewrite.c>
 RewriteEngine On
 RewriteCond %{REQUEST_FILENAME} !-U
 RewriteRule ^.*$ index.php [L,QSA]
</ifModule> 

Auth::logout() вызывается для «разлогинивания» пользователя. Очищает куку и сессионную перемнную, удаляет за ненадобностью ключ из базы, и перенаправляет на основную.

Остался конечный штрих. Нужно периодический (по cron) очищать таблицу от устаревших ключей.

……
$db->query("DELETE FROM user_auth_cookies WHERE `logged_in` < DATE_SUB(NOW(), INTERVAL ".AUTH_COOKIE_DURATION." DAYS);");
……

Также дозволено добавить на сайт кнопку типа: «Разлогинить меня на всех устройствах», при нажатии на которую удаляются все ключи авторизации из таблицы user_auth_cookies по user_id. Это необходимо если пользователь, скажем, позабыл нажать на «Выход» на чужом компьютере, либо у него украли мобильное устройство, с которого он посещал ваш сайт.

……
if (isset($_POST['signout_all']) {
	$db->query("DELETE FROM user_auth_cookies WHERE `user_id` = ".$user->getId());
	Auth::logout();
}
……

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

На этом все, хочу вашему печенью оставаться неизменно свежим и хрустящим.

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB