Здравствуйте. У меня есть вопрос по работе с сессиями. Я реализовал класс для работы с сессиями, но точно не могу определить его адекватность и корректность. PHP: <?php namespace Core; class Session { /** * @var string Название сесии */ private $name; /** * @var array Cookie сессии */ private $cookie; /** * @var int Время жизни сессии */ private $timeToLive; /** * Session constructor * @see https://php.ru/manual/session.configuration.html Настройка во время выполнения * @see https://php.ru/manual/function.session-set-cookie-params.html PHP session_set_cookie_params * @param int $time_to_live Время жизни сессии (в минутах) * @param string $name * @param array $cookie */ public function __construct($time_to_live = 30, $name = "application.session", $cookie = []) { $this->timeToLive = $time_to_live; // Изменяется имя сеанса (по умолчанию) на указанное (если есть) имя для конкретного приложения $this->name = $name; $this->cookie = $cookie; // session.cookie_path определяет устанавливаемый путь в сессионной cookie // session.cookie_domain определяет устанавливаемый домен в сессионной cookie $this->cookie += [ 'lifetime' => 0, 'path' => ini_get('session.cookie_path'), 'domain' => ini_get('session.cookie_domain'), 'secure' => isset($_SERVER['HTTPS']), 'httponly' => true ]; /* * Указывается, что сеансы должны передаваться только с помощью файлов cookie, * исключая возможность отправки идентификатора сеанса в качестве параметра «GET». * Установка параметров cookie идентификатора сеанса. Эти параметры могут быть переопределены при инициализации * обработчика сеанса, однако рекомендуется использовать значения по умолчанию, разрешающие отправку * только по HTTPS (если имеется) и ограниченный доступ HTTP (без доступа к сценарию на стороне клиента). */ // Определяет, будет ли модуль использовать cookies для хранения идентификатора сессии на стороне клиента ini_set('session.use_cookies', 1); // Определяет, будет ли модуль использовать только cookies для хранения идентификатора сессии на стороне клиента ini_set('session.use_only_cookies', 1); session_set_cookie_params( $this->cookie['lifetime'], $this->cookie['path'], $this->cookie['domain'], $this->cookie['secure'], $this->cookie['httponly'] ); } public function __get($name) { switch ($name) { case 'isActive': return $this->getActive(); case 'id': return $this->getId(); case 'name': return isset($this->name) ? $this->name : $this->getName(); case 'isValid': return $this->isValid(); } } public function __set($name, $value) { switch ($name) { case 'id': $this->setId($value); break; case 'name': $this->setName($value); break; case 'timeToLive': $this->timeToLive = $value * 60; break; } } /** * Получение статуса активности сессии * @see https://secure.php.net/manual/en/function.session-status.php PHP session_status * @return bool Статус активности сессии */ private function getActive() { return session_status() === PHP_SESSION_ACTIVE; } /** * Получение идентификатора текущей сессии. * Метод является оберткой для реализации стандартного метода. * @see https://secure.php.net/manual/ru/function.session-id.php PHP session_id * @return string */ private function getId() { return session_id(); } /** * Получение имени сессии * Метод является оберткой для реализации стандартного метода * @see https://php.ru/manual/function.session-name.html PHP session_name * @return string|null */ private function getName() { return $this->isActive ? session_name() : null; } private function isValid() { return !$this->isExpired() && $this->isFingerprint(); } /** * Проверка срока действия сессии * @return bool */ private function isExpired() { $activity = isset($_SESSION['_last_activity']) ? $_SESSION['_last_activity'] : false; if ($activity && ((time() - $activity) > $this->timeToLive)) { return true; } $_SESSION['_last_activity'] = time(); return false; } /** * Проверка клиента * @return bool */ private function isFingerprint() { $hash = sha1($_SERVER['HTTP_USER_AGENT'] . (ip2long($_SERVER['REMOTE_ADDR']) & ip2long('255.255.0.0'))); if (isset($_SESSION['_fingerprint'])) { return $_SESSION['_fingerprint'] === $hash; } $_SESSION['_fingerprint'] = $hash; return true; } /** * Назначение идентификатора текущей сессии. * Метод является оберткой для реализации стандартного метода. * @see https://secure.php.net/manual/ru/function.session-id.php PHP session_id * @param string $id Идентификатор сессии для текущей сессии */ private function setId($id) { session_id($id); } /** * Установка имени сессии * Метод является оберткой для реализации стандартного метода * @see https://php.ru/manual/function.session-name.html PHP session_name * @param $name */ public function setName($name) { if ($this->isActive) { session_name($name); } } /** * Инициализация сессии */ public function open() { // Бездействие, если сессия была инициализирована ранее if ($this->isActive) { return; } session_start(); // Проверка на корректность инициализированнйо сессии if (!$this->isActive) { // TODO: Вывод исключения } } /** * Уничтожение сессии, включая все атрибуты. Метод имеет эффект только при наличии активной сессии. * @see https://php.ru/manual/function.setcookie.html PHP setcookie */ public function destroy() { if ($this->isActive) { $this->deleteAll(); setcookie( $this->name, time() - 42000, $this->cookie['path'], $this->cookie['domain'], $this->cookie['secure'], $this->cookie['httponly'] ); session_destroy(); } } /** * Удаление всех значений сессии * Метод является оберткой для реализации стандартного метода * @see https://php.ru/manual/function.session-unset.html PHP session_unset */ public function deleteAll() { if ($this->isActive) { session_unset(); } } /** * Обновление текущего ID на новый. Метод имеет эффект только при наличии активной сессии. * @see https://secure.php.net/session_regenerate_id PHP session_regenerate_id * @param bool $delete_old_session */ public function refresh($delete_old_session = true) { if ($this->isActive) { session_regenerate_id($delete_old_session); } } /** * Получение значение сессии по ключу. * @param string $key Ключ, по которому необходимо получить значения * @return null|mixed Значение сессии по ключу */ public function get($key) { if ($this->isActive) { return isset($_SESSION[$key]) ? $_SESSION[$key] : null; } return null; } /** * Добавление или установка значений в сессию по ключу * @param string $key Ключ, в который необходимо добавить значения * @param string $value Значение добавления */ public function set($key, $value) { if ($this->isActive) { $_SESSION[$key] = $value; } } /** * Удаление значения сессии по ключу * @param string $key Ключ, по которому необходимо удалить значения */ public function delete($key) { if ($this->isActive && isset($_SESSION[$key])) { unset($_SESSION[$key]); } } /** * Проверка наличия ключа у сессии * @param string $key Ключ, в который необходимо найти * @return bool */ public function hasKey($key) { return ($this->isActive && isset($_SESSION[$key])); } } Использование такое: PHP: $session = new Session(); $session->open(); // If AFK more than access - logout if (!$session->isValid) { $session->destroy(); } ...
В чем же безопасность работы заключается? Так же метод hasKey() можно было бы использовать внутри методов get() и delete().
@mepihindeveloper продемонстрируй вектор атаки, от которого, по твоему мнению, защищает этот класс. Мне непонятно. Буду рад узнать что-то новое, тем более, что ты поместил тему в раздел Профи. Наверное ты что-то такое знаешь...
Отошел от компа, а кто то подойдет после 42000 11.6ч и не воспользуется твоей сессий если я все правильно понял.
Почитал код, нормально в принципе. Правда смена IP у пользователя это не такое уж редкое явление. Фингерпринт можно подшаманить под свои вкусы, в принципе. Я бы IP убрал, а добавил ещё пару заголовков запроса.
IP даже банки не чекают. Кто-то например полтора часа до работы едет, так IP десять раз сменится. --- Добавлено --- HTTPS в принципе сделал куки безопасными, даже фингерпринт это экстра, хотя имеет смысл при атаке непосредственно на устройство пользователя
Просто загляни в инспектор браузера, вкладка Network. Возьми любой запрос и посмотри его Request headers. Как минимум, всё, что начинается на "Accept" можно включить в отпечаток. --- Добавлено --- Про фингерпринтинг и идентификацию клиента много пишут. Это выйдет за рамки твоей текущей задачи Я вообще не уверен надо ли с т.з. ООП включать уловки по идентификации клиента в класс Сессия.
Реализовал систему и обнаружил, что заголовок http_accept изменчив. Получилось, что сначала он запрашивает картинку (фавикон), а потом страницы и эти 2 заголовка разные. Есть ли возможность как-то настроить эту вещь? Ну, скажем, какое-то условие, которое игнорировало бы http_accept, если обращение к картинкам. То есть, создавать и работать с сессиями только если запросы к страницам.
Ну вот, для меня это выглядит как ещё один аргумент в пользу того, что класс сессии не должен отвечать за такие вещи. Он находится на более низком уровне чем контроль доступа. Если ты знаком с Laravel, то знаешь, что каждому маршруту (или группе маршрутов) можно сопоставить middleware. Таким образом можно контролировать доступ к разделу /admin/, например, но ничего не предпринимать при доступе к другим адресам. Так вот, логично поместить контроль за фингерпринтом в этот самый мидлвар. А не в класс сессии. Мидлвар решает надо ли открывать сессию, надо ли идентифицировать пользователя и как это делать.
А мне, если честно, не понятно, зачем мне печатать так много кода для того, чтобы начать сессию? Мне кажется, должно быть достаточно одной строки $session = new Session(); Всё остальное можно уже внутри сделать. Если хочется дать возможность выбора - удалять или нет сессию, которая !isValid - можно параметр в конструкторе добавить для этого со значением по-умолчанию...
„SRP (принцип единственной ответственности) в ООП обозначает, что на каждый класс нужно возложить только одну определённую ответственность, и его поведения должны быть направлены исключительно на обеспечение этой ответственности.“ — из описания SOLID Principles
Это была реакция на "всё остальное внутри". ПМСМ, класс "Сессия" сам не должен принимать решений. Он обеспечивает уровень хранения и только. А выше может быть класс "Защитник" или типа того, из которого мы можем запросить объект "Идентифицированный пользователь", например.
Ну он и не будет принимать решений, он будет просто выполнять. А решение принимает тот, кто передаёт или не передаёт аргумент в конструктор
Просто не ефективно. Это примерно так: === ко всему и в 80% случаев ваша сессия буде отваливаться. (обновил браузер, изменился IP и т.д.) Также очень забавные куки. Реализалия всего "в одном месте" как минимум головная боль следующему программисту или вас ще, через 6 месяцев не использование этого скрипта. Вложите клас создания сессий, класс для создание cockie и клас который все это проверяет на "беопасность". + если это "смертельно важное место" - логирование всего что происходить с сохранение в БД и/или в файл на сервере если БД упала.
В случае с сессиями вообще очень трудно накосячить, гораздо более слабое место - SQL-инъекции и XSS. Вообще в PHP-сессиях мне больше всего не нравится лёгкость подмены, но в вашем случае эта дыра залатана, так что не знаю, в чём упрекнуть.