• Главная»
  • Уроки»
  • PHP»
  • Создание профессиональной утилиты восстановления пароля

Создание профессиональной утилиты восстановления пароля

Введение

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

sourse

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

С шифруемыми паролями дела обстоят немного сложнее. Когда пароль зашифровывается с помощью функции md5(), он не может быть расшифрован. По этой причине мы будем отправлять пользователю ссылку, пройдя по которой он сможет изменить пароль. Чтобы обезопасить данный процесс, мы будем пользоваться уникальным ключом, который будет проверяться при возврате пользователя на страницу аутентификации. Чтобы улучшить защиту, мы будем применять технологию прибавления соли к шифруемому паролю.

ВАЖНО: в данном уроке мы используем MySQLi вместо MySQL. Несмотря на то что в документации по PHP утверждается, что все должно работать корректно, у меня возникали проблемы с использованием классов MySQLi, пока я не обновил версию 5.1.4 до 5.2.9. Если у вас возникнут ошибки, связанные с использованием неподдерживаемого буффера (‘unsupported buffer’), попробуйте обновить PHP. В случае, если возникнут ошибки о ненахождении класса mysqli, вам также может понадобится изменить файл php.ini, чтобы загрузить расширение mysqli.

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

Шаг 1. Таблицы базы данных

После создания новой базы данных, нам нужно создать три таблицы:

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

Если у вас уже есть таблица пользователей, вам понадобится добавить поля секретного вопроса и ответа. Поле с вопросом будет содержать целочисленное значение, которое приравнивает секретный вопрос к массиву. Тип значения поля с ответом - текстовый (varchar). Секретный вопрос используется в качестве контроля перед отправкой электронного письма с паролем. Вот код создания таблиц (файл sql.txt доступен для загрузки).

CREATE TABLE IF NOT EXISTS `recoveryemails_enc` (
  `ID` bigint(20) unsigned zerofill NOT NULL auto_increment,
  `UserID` bigint(20) NOT NULL,
  `Key` varchar(32) NOT NULL,
  `expDate` datetime NOT NULL,
  PRIMARY KEY  (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
CREATE TABLE IF NOT EXISTS `users` (
  `ID` bigint(20) unsigned zerofill NOT NULL auto_increment,
  `Username` varchar(20) NOT NULL,
  `Email` varchar(255) NOT NULL,
  `Password` varchar(20) NOT NULL,
  `secQ` tinyint(4) NOT NULL,
  `secA` varchar(30) NOT NULL,
  PRIMARY KEY  (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=5 ;
INSERT INTO `users` (`ID`, `Username`, `Email`, `Password`, `secQ`, `secA`) VALUES (00000000000000000002, 'jDoe', 'jDoe@gmail.com', 'johnDoe2009', 0, 'Smith'),
(00000000000000000003, 'envato', 'webmaster@envato.com', 'envatouser', 1, 'Sydney'),
(00000000000000000004, 'sToaster', 'toast@yahoo.com', 'toastrules', 3, '2001');
CREATE TABLE IF NOT EXISTS `users_enc` (
  `ID` bigint(20) unsigned zerofill NOT NULL auto_increment,
  `Username` varchar(20) NOT NULL,
  `Password` char(32) NOT NULL,
  `Email` varchar(255) NOT NULL,
  `secQ` tinyint(4) NOT NULL default '0',
  `secA` varchar(32) NOT NULL,
  PRIMARY KEY  (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=4 ;
INSERT INTO `users_enc` (`ID`, `Username`, `Password`, `Email`, `secQ`, `secA`) VALUES (00000000000000000001, 'jDoe', 'fd2ba57673c57ac5a0650c38fe60b648', 'jDoe@gmail.com', 0, 'Smith'),
(00000000000000000002, 'envato', '1ecc663314777c8e3c2328027447f194', 'webmaster@envato.com', 1, 'Sydney'),
(00000000000000000003, 'sToaster', 'e05fd29cbca7ea9add48ba6dafc300e8', 'toast@yahoo.com', 3, '2001');

Шаг 2. Подключение базы данных

Нам необходимо создать файл для подключения базы данных. Для взаимодействия с базой данных мы будем использовать MySQLi, которая обеспечивает объектно-ориентированный подход к взаимодействию с базами данных. Создайте файл assets/php/database.php. Вставьте данный код (измените значения переменных на подходящие вашему хостингу):

<?php
session_start();
ob_start();
$hasDB = false;
$server = 'localhost';
$user = 'user';
$pass = 'password';
$db = 'db';
$mySQL = new mysqli($server,$user,$pass,$db);
if ($mySQL->connect_error)
{
    die('Connect Error (' . $mySQL->connect_errno . ') '. $mySQL->connect_error);
}
?>

В первой строке кода мы вызываем функцию session_start(). В действительности, мы не будем использовать переменные сессии, но вам понадобится сессия как часть системы входа пользователя на сайт. Затем мы вызываем функцию ob_start() для запуска буфера вывода.

Строки 4-8 устанавливают переменные для соединения с сервером. Затем создаем новый объект mysqli, передав ему наши переменные. Для оставшейся части кода $mySQL позволит взаимодействовать с базой данных как требуется.

MySQLi обрабатывает ошибки немного иначе, поэтому нам необходимо проверять свойство connect_error после подключения и выводить сообщение об ошибке в случае, если что-то пошло не так. То же самое касается создания запросов. Для обнаружения ошибок необходимо проверять свойство error нашего запроса или объекта соединения.

Шаг 3. Создание страницы восстановления пароля

Давайте начнем с создания файла forgotPass.php. Добавим следующий код:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Password Recovery</title>
<link href="assets/css/styles.css" rel="stylesheet" type="text/css">
</head>
<body>
<div id="header"></div>
<div id="page">
<!--PAGE CONTENT-->
</div>
</body>
</html>

Этот html код будет хорошей основой. Вот результат:

Затем вставьте следующий код в самое начало только что созданного файла:

<?php
include("assets/php/database.php");
include("assets/php/functions.php");
$show = 'emailForm'; // какую стадию отображать по молчанию
if ($_SESSION['lockout'] == true && (mktime() > $_SESSION['lastTime'] + 900))
{
	$_SESSION['lockout'] = false;
	$_SESSION['badCount'] = 0;
}
if (isset($_POST['subStep']) && !isset($_GET['a']) && $_SESSION['lockout'] != true)
{
	switch($_POST['subStep'])
	{
		case 1:
			// отправляем электронный адрес или имя пользователя
			$result = checkUNEmail($_POST['uname'],$_POST['email']);
			if ($result['status'] == false )
			{
				$error = true;
				$show = 'userNotFound';
			} else {
				$error = false;
				$show = 'securityForm';
				$securityUser = $result['userID'];
			}
		break;
		case 2:
			// отправляем секретный вопрос
			if ($_POST['userID'] != "" && $_POST['answer'] != "")
			{
				$result = checkSecAnswer($_POST['userID'],$_POST['answer']);
				if ($result == true)
				{
					// ответ верен
					$error = false;
					$show = 'successPage';
					$passwordMessage = sendPasswordEmail($_POST['userID']);
					$_SESSION['badCount'] = 0;
				} else {
					// ответ неверен
					$error = true;
					$show = 'securityForm';
					$securityUser = $_POST['userID'];
					$_SESSION['badCount']++;
				}
			} else {
				$error = true;
				$show = 'securityForm';
			}
		break;
		case 3:
			// отправляем новый пароль (для случая с шифруемыми паролями)
			if ($_POST['userID'] == '' || $_POST['key'] == '') header("location: login.php");
			if (strcmp($_POST['pw0'],$_POST['pw1']) != 0 || trim($_POST['pw0']) == '')
			{
				$error = true;
				$show = 'recoverForm';
			} else {
				$error = false;
				$show = 'recoverSuccess';
				updateUserPassword($_POST['userID'],$_POST['pw0'],$_POST['key']);
			}
		break;
	}
}

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

После этого мы проверяем некоторые переменные сессии. Если переменная сессии lockout = true, и со времени блокировки прошло больше 900 секунд (15 минут), мы завершим блокировку (строки 7 и 8).

Затем создадим if-блок, в котором будет проверяться, передано ли что-либо на страницу. Мы также должны удостовериться в том, что $_GET['a'] не установлена. Эта переменная будет установлена, когда пользователь перейдет по ссылке, отправленной ему в письме восстановления пароля. Итак, если это происходит, мы можем пропустить проверку передачи данных на страницу. В этом же логическом блоке мы должны удостовериться в том, что переменная блокировки не установлена в true.

Сразу после блока if с помощью switch($_POST['subStep']) мы проверяем, на какой стадии формы данные были отправлены. Нам необходимо будет обрабатывать три стадии. Первая стадия означает, что мы ввели только логин или адрес электронной почты для изменения пароля. Для этого мы вызываем функцию checkUNEmail(), которую мы вскоре напишем. Эта функция возвращает массив со значением boolean для установки, найден ли пользователь, и целочисленное значение с идентификатором пользователя, если он найден. В строке 17 проверяется, был ли найден пользователь, если нет, мы устанавливаем флаг ошибки и переменную $show, чтобы отобразить сообщение об ошибке ‘Пользователь не найден’. Если пользователь найден, нам следует отобразить форму с секретным вопросом и установить переменную $securityUser, чтобы узнать, для какого пользователя загружать вопрос.

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

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

ПРИМЕЧАНИЕ: если вы используете вариант с нешифруемыми паролями, вы можете упустить третью стадию, описанную выше. Это метод, который сохраняет новый введенный пароль. Мы можем отправить незашифрованный пароль в письме, так как пользователю нет необходимости его изменять.

Теперь необходимо добавить следующий код в блок if, написанный выше:

elseif (isset($_GET['a']) && $_GET['a'] == 'recover' && $_GET['email'] != "") {
	$show = 'invalidKey';
	$result = checkEmailKey($_GET['email'],urldecode(base64_decode($_GET['u'])));
	if ($result == false)
	{
		$error = true;
		$show = 'invalidKey';
	} elseif ($result['status'] == true) {
		$error = false;
		$show = 'recoverForm';
		$securityUser = $result['userID'];
	}
}
if ($_SESSION['badCount'] >= 3)
{
	$show = 'speedLimit';
	$_SESSION['lockout'] = true;
	$_SESSION['lastTime'] = '' ? mktime() : $_SESSION['lastTime'];
}
?>

В else-блоке проверяется, установлена ли переменная $_GET['a'], и если это так, означает, что пользователь перешел по ссылке изменения пароля в письме. После того как мы выяснили, что пользователь перешел по этой ссылке, мы проверяем на валидность ключ, отправленный письмом, с помощью функции checkEmailKey(). Если ключ не найден или не верен, функция возвращает false и выводится сообщение о неверности ключа. Тем не менее, если ключ валиден, он возвращает массив, из которого мы можем получить пользовательский идентификатор (так мы узнаем, какому пользователю менять пароль).

ПРИМЕЧАНИЕ: если вы используете нешифруемые пароли, можете пропустить блок elseif. В этом блоке происходит отображение формы изменения пароля после того как пользователь перешел по ссылке из письма.

Во втором блоке if проверяется, ответил ли пользователь на секретный вопрос более 3 раз. Если это так, устанавливаем переменную блокировки в true и запоминаем время блокировки. Заметьте, если время блокировки уже установлено, мы используем это значение, а если нет, то генерируем новое значение.

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

Шаг 4. Файл методов

Теперь давайте создадим файл assets/php/functions.php со всеми функциями, которые мы ранее вызывали. Давайте рассмотрим их.

ПРИМЕЧАНИЕ: в примерах в случае шифруемых паролей используйте таблицу users_enc, а в случае нешифруемых - таблицу users, так что название таблицы в ваших методах будет зависеть от версии, которую вы используете.

<?php
define(PW_SALT,'(+3%_');

function checkUNEmail($uname,$email)
{
	global $mySQL;
	$error = array('status'=>false,'userID'=>0);
	if (isset($email) && trim($email) != '') {
		// адрес электронной почты был введен
		if ($SQL = $mySQL->prepare("SELECT `ID` FROM `users_enc` WHERE `Email` = ? LIMIT 1"))
		{
			$SQL->bind_param('s',trim($email));
			$SQL->execute();
			$SQL->store_result();
			$numRows = $SQL->num_rows();
			$SQL->bind_result($userID);
			$SQL->fetch();
			$SQL->close();
			if ($numRows >= 1) return array('status'=>true,'userID'=>$userID);
		} else { return $error; }
	} elseif (isset($uname) && trim($uname) != '') {
		// имя пользователя было введено
		if ($SQL = $mySQL->prepare("SELECT `ID` FROM `users_enc` WHERE Username = ? LIMIT 1"))
		{
			$SQL->bind_param('s',trim($uname));
			$SQL->execute();
			$SQL->store_result();
			$numRows = $SQL->num_rows();
			$SQL->bind_result($userID);
			$SQL->fetch();
			$SQL->close();
			if ($numRows >= 1) return array('status'=>true,'userID'=>$userID);
		} else { return $error; }
	} else {
		// ничего не было введено
		return $error;
	}
}

В самом начале мы определяем PW_SALT, где будет хранится соль, с помощью которой мы будем зашифровывать пароли. Первая функция по значениям логина и пароля проверяет через $_POST, существует ли такой пользователь в базе данных. В первую очередь нам необходимо сделать глобальной $mySQL, чтобы можно было получить доступ к базе данных. Мы также создаем динамический массив ошибок, который будет возвращаться в случае, если пользователь не найден. Затем мы проверяем, существует ли такой электронный адрес. Если он существует, создаем готовое mySQLi выражение. Большое преимущество в использовании готовых выражений заключается в безопасности, особенно когда дело касается ввода данных пользователем. В общем, процесс напоминает использование метода sprintf().

Заметьте, мы вызываем метод ->prepare() в качестве параметра SQL запроса и используем вопросительные знаки в местах, где должны быть наши переменные. Также заметьте, что мы не заключаем знаки вопроса ни в никакие кавычки (даже в случае строк). Это позволяет нам параметризировать выражение. Если выражение создано успешно, mySQLi::prepare() вернет истину. И если оно создано, нам необходимо привязать параметры к запросу с помощью mySQLi::bind_param(). В этой функции как минимум 2 аргумента. Первый - это строка символов, представляющая типы данных, которые должны быть привязаны. Остальные аргументы - это переменные, которые вы вставляете в каждый параметр. В нашем случае, это ‘s’ (для строки) и $email, так как переменная содержит адрес электронной почты. Заметьте, что порядок имеет значение, так что первый знак вопроса в запросе соответствует первой букве названия типа данных string и первой переменной.

Затем необходимо вызвать ->execute() и ->store_result(). Эти два метода выполняют подготовленные запросы и сохраняют их в памяти до освобождения. Затем мы сверяем количество возвращенных строк, чтобы удостоверится, что пользователь найден. ->bind_result() подобна ->bind_param(), но работает в другом направлении. Она позволяет получить возвращаемое значение из результата выполнения запроса и поместить его в локальную переменную. Перменные устанавливаются на основе их порядка в результате. Локальные переменные в действительности не создаются, пока мы не вызываем ->fetch(), после чего в переменную $userID запишется значение из базы данных. Затем с помощью ->close() мы освобождаем результаты запроса. В завершении, мы возвращаем массив с логическим значением и целочисленным значением идентификатора пользователя.

function getSecurityQuestion($userID)
{
	global $mySQL;
	$questions = array();
	$questions[0] = "Девичья фамилия матери?";
	$questions[1] = "Город, в котором родился?";
	$questions[2] = "Какой ваш любимый цвет?";
	$questions[3] = "Год окончания школы?";
	$questions[4] = "Имя первой любви?";
	$questions[5] = "Любимая модель машины?";
	if ($SQL = $mySQL->prepare("SELECT `secQ` FROM `users_enc` WHERE `ID` = ? LIMIT 1"))
	{
		$SQL->bind_param('i',$userID);
		$SQL->execute();
		$SQL->store_result();
		$SQL->bind_result($secQ);
		$SQL->fetch();
		$SQL->close();
		return $questions[$secQ];
	} else {
		return false;
	}
}

function checkSecAnswer($userID,$answer)
{
	global $mySQL;
	if ($SQL = $mySQL->prepare("SELECT `Username` FROM `users_enc` WHERE `ID` = ? AND LOWER(`secA`) = ? LIMIT 1"))
	{
		$answer = strtolower($answer);
		$SQL->bind_param('is',$userID,$answer);
		$SQL->execute();
		$SQL->store_result();
		$numRows = $SQL->num_rows();
		$SQL->close();
		if ($numRows >= 1) { return true; }
	} else {
		return false;
	}
}

getSecurityQuestion() принимает идентификатор пользователя и возвращает его секретный вопрос как строку. Делаем $mySQL глобальной снова. Затем мы создаем массив из 6 разных возможных секретных вопросов. Используя подобный вышеприведенному метод, мы узнаем, какой секретный вопрос выбрал пользователь, и возвращаем индекс этого вопроса в массиве.

checkSecAnswer() принимает идентификатор пользователя и ответ на вопрос и проверяет, верно ли ответил пользователь. Заметьте, для того, чтобы увеличить шансы совпадения введенного значения с записанным в базу данных, мы предварительно преобразовываем их к строчным буквам (это вам не обязательно). Это отличный пример подготовленного выражения с множеством параметров. Обратите внимание на порядок аргументов в методе bind_param(). Этот метод вернет истину, если в базе данных найдена запись, совпадающая с идентификатором пользователя и полученным ответом. В противном случае, вернется ложь.

function sendPasswordEmail($userID)
{
	global $mySQL;
	if ($SQL = $mySQL->prepare("SELECT `Username`,`Email`,`Password` FROM `users_enc` WHERE `ID` = ? LIMIT 1"))
	{
		$SQL->bind_param('i',$userID);
		$SQL->execute();
		$SQL->store_result();
		$SQL->bind_result($uname,$email,$pword);
		$SQL->fetch();
		$SQL->close();
		$expFormat = mktime(date("H"), date("i"), date("s"), date("m")  , date("d")+3, date("Y"));
		$expDate = date("Y-m-d H:i:s",$expFormat);
		$key = md5($uname . '_' . $email . rand(0,10000) .$expDate . PW_SALT);
		if ($SQL = $mySQL->prepare("INSERT INTO `recoveryemails_enc` (`UserID`,`Key`,`expDate`) VALUES (?,?,?)"))
		{
			$SQL->bind_param('iss',$userID,$key,$expDate);
			$SQL->execute();
			$SQL->close();
			$passwordLink = "<a href=\"?a=recover&email=" . $key . "&u=" . urlencode(base64_encode($userID)) . "\">http://www.oursite.com/forgotPass.php?a=recover&email=" . $key . "&u=" . urlencode(base64_encode($userID)) . "</a>";
			$message = "Уважаемый(ая)$uname,\r\n";
			$message .= "Пройдите по ссылке, чтобы изменить пароль:\r\n";
			$message .= "-----------------------\r\n";
			$message .= "$passwordLink\r\n";
			$message .= "-----------------------\r\n";
			$message .= "Ссылка будет действительно в течении 3х дней.\r\n\r\n";
			$message .= "Ничего не делайте, если вы не запрашивали восстановение пароля.\r\n\r\n";
			$message .= "Спасибо,\r\n";
			$message .= "-- Команда сайта ...";
			$headers .= "From: Our Site <webmaster@oursite.com> \n";
			$headers .= "To-Sender: \n";
			$headers .= "X-Mailer: PHP\n"; // отправитель
			$headers .= "Reply-To: webmaster@oursite.com\n"; // адрес отправителя
			$headers .= "Return-Path: webmaster@oursite.com\n"; // для ошибок
			$headers .= "Content-Type: text/html; charset=iso-8859-1"; // тип данных
			$subject = "Восстановление пароля";
			@mail($email,$subject,$message,$headers);
			return str_replace("\r\n","<br/ >",$message);
		}
	}
}

Эта функция отправляет электронное письмо для изменения пароля. Начнем с создания SQL запроса, чтобы получить имя пользователя и его электронный адрес. После связывания параметров с результатами закрываем запрос. Для большей безопасности, мы оставим сгенерированную ссылку в рабочем состоянии в течении 3 дней. Для этого мы создаем новую дату, которая наступит через три дня. Затем, используя некоторые получаемые значения: дату, случайное число и соль, мы генерируем MD5 хэш, который и будет секретным кодом. Из-за того что дата и случайное число постоянно меняются, мы должны быть увернены в том, что секретный ключ уникален. Создаем SQL запрос, чтобы ввести его в базу данных. После выполнения запроса генерируем ссылку, которая будет отправляться в письме. Добавляем ‘a=recover’ и ключ, а также идентификатор пользователя, предварительно применив к нему base64_encode() и urlencode(), чтобы сделать его нечитабельным. Как только ссылка сгенерирована мы составляем остальную часть письма и отправляем его.

ПРИМЕЧАНИЕ: в случае нешифруемых паролей, измените письмо - выводите значение переменной $pword вместо ссылки на изменение пароля.

ПРИМЕЧАНИЕ: если вы разрабатываете сайт на локальном сервере, маловероятно, что smtp установлен, так что у вас не получится отправить письмо. Именно поэтому мы используем команду @mail(), чтобы избежать вывода сообщений об ошибке. По этой же причине эта функция возвращает строку с сообщением, так что его можно вывести на экран. А теперь последние 3 функции:

function checkEmailKey($key,$userID)
{
	global $mySQL;
	$curDate = date("Y-m-d H:i:s");
	if ($SQL = $mySQL->prepare("SELECT `UserID` FROM `recoveryemails_enc` WHERE `Key` = ? AND `UserID` = ? AND `expDate` >= ?"))
	{
		$SQL->bind_param('sis',$key,$userID,$curDate);
		$SQL->execute();
		$SQL->execute();
		$SQL->store_result();
		$numRows = $SQL->num_rows();
		$SQL->bind_result($userID);
		$SQL->fetch();
		$SQL->close();
		if ($numRows > 0 && $userID != '')
		{
			return array('status'=>true,'userID'=>$userID);
		}
	}
	return false;
}

function updateUserPassword($userID,$password,$key)
{
	global $mySQL;
	if (checkEmailKey($key,$userID) === false) return false;
	if ($SQL = $mySQL->prepare("UPDATE `users_enc` SET `Password` = ? WHERE `ID` = ?"))
	{
		$password = md5(trim($password) . PW_SALT);
		$SQL->bind_param('si',$password,$userID);
		$SQL->execute();
		$SQL->close();
		$SQL = $mySQL->prepare("DELETE FROM `recoveryemails_enc` WHERE `Key` = ?");
		$SQL->bind_param('s',$key);
		$SQL->execute();
	}
}

function getUserName($userID)
{
	global $mySQL;
	if ($SQL = $mySQL->prepare("SELECT `Username` FROM `users_enc` WHERE `ID` = ?"))
	{
		$SQL->bind_param('i',$userID);
		$SQL->execute();
		$SQL->store_result();
		$SQL->bind_result($uname);
		$SQL->fetch();
		$SQL->close();
	}
	return $uname;
}

Первая функция проверяет, верен ли ключ безопасности. Мы преобразовываем текущую дату в строку, чтобы узнать, не истек ли срок годности ключа. Затем проверяется, существуют ли записи, соответствующие пользователю, секретному ключу и что дата истечения следует прямо за текущей. Если такой ключ существует, мы возвращаем массив с идентификатором пользователя или ложь в противном случае.

Функция updateUserPassword() отвечает за изменение пароля в базе данных. Сперва мы сверяем ключ, отправленный письмом, с введенным пользователем ключом и информацию о ключе, чтобы удостовериться в безопасности. Генерируем новый пароль, соединив введенный пароль с ранее определенной солью, а затем применив к полученной строке метод md5(). Затем осуществляем обновление. После его заверешния удаляем запись кода восстановления из базы данных, чтобы исключить его повторное использование.

ПРИМЕЧАНИЕ: функция updateUserPassword() не требуется в случае нешифруемых паролей, так как мы не меняем пароли.

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

Шаг 5. Завершаем создание страницы восстановления пароля

После написания всех необходимых функций добавим кое-что еще в файл forgotPass.php. Вставьте этот код в блок (div) с идентификатором (id) "page":

<?php switch($show) {
	case 'emailForm': ?>
	<h2>Восстановление пароля</h2>
    <p>Это форма для восстановления пароля. Введите ваш логин или email для того, чтобы начать.</p>
    <?php if ($error == true) { ?><span class="error">Введите логин или пароль чтобы продолжить.</span><?php } ?>
    <form action="<?= $_SERVER['PHP_SELF']; ?>" method="post">
        <div class="fieldGroup"><label for="uname">Логин</label><div class="field"><input type="text" name="uname" id="uname" value="" maxlength="20"></div></div>
        <div class="fieldGroup"><label>- ИЛИ -</label></div>
        <div class="fieldGroup"><label for="email">Email</label><div class="field"><input type="text" name="email" id="email" value="" maxlength="255"></div></div>
        <input type="hidden" name="subStep" value="1" />
        <div class="fieldGroup"><input type="submit" value="Submit" style="margin-left: 150px;" /></div>
        <div class="clear"></div>
    </form>
    <?php break; case 'securityForm': ?>
    <h2>Восстановление пароля</h2>
    <p>Ответьте на секретный вопрос:</p>
    <?php if ($error == true) { ?><span class="error">Для восстановления пароля вы должны правильно ответить на секретный вопрос.</span><?php } ?>
    <form action="<?= $_SERVER['PHP_SELF']; ?>" method="post">
        <div class="fieldGroup"><label>Вопрос</label><div class="field"><?= getSecurityQuestion($securityUser); ?></div></div>
        <div class="fieldGroup"><label for="answer">Ответ</label><div class="field"><input type="text" name="answer" id="answer" value="" maxlength="255"></div></div>
        <input type="hidden" name="subStep" value="2" />
        <input type="hidden" name="userID" value="<?= $securityUser; ?>" />
        <div class="fieldGroup"><input type="submit" value="Submit" style="margin-left: 150px;" /></div>
        <div class="clear"></div>
    </form>

	 <?php break; case 'userNotFound': ?>    <h2>Восстановление пароля</h2>    <p>Логин или email не был найден в базе.<br /><br /><a href="?">Нажмите тут</a> чтобы попробывать снова.</p>    <?php break; case 'successPage': ?>    <h2>Восстановление пароля</h2>    <p>На ваш email было отправлено письмо с инструкциями. <strong>(Mail не заработает пока вы локально не активируете работу smtp сервера.)</strong><br /><br /><a href="login.php">Вернуться</a> на главную. </p>    <p>Это сообщение, которое отправится на email:</p>    <div class="message"><?= $passwordMessage;?></div>    <?php break;

Этот фрагмент кода в действительности осуществляет отображение всех элементов интерфейса. Начнем с проверки переменной $show с помощью switch(). Как вы помните, переменная может принимать множество различных значений, так что нам необходимо проверить каждое из них. В первом случае, "emailForm", это имя пользователя или адрес его электронной почты. Вообще-то нам нужны два текстовых поля и одно скрытое поле ввода (hidden), так что мы будем знать, на какой стадии заполнения была отправлена форма. Блок if ($error == true) отобразит ошибку, если флаг $error установлен в true.

Второй случай - "securityForm". В нем будет отображатся секретный вопрос. У нас также есть сообщение об ошибке, которое отображается с помощью getSecurityQuestion(). У нас также есть поле для ввода ответа, а также стадия заполнения формы и идентификатор пользователя.

Сообщение для случая "userNotFound" - это просто текстовое сообщение, которое сообщает пользователю, что такая запись не была найдена.

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

Далее следует фрагмент кода:

case 'recoverForm': ?>
    <h2>Восстановление пароля</h2>
    <p>И снова здрасти, <?= getUserName($securityUser=='' ? $_POST['userID'] : $securityUser); ?>.</p>
    <p>Введите ваш новый пароль.</p>
    <?php if ($error == true) { ?><span class="error">Новый пароль не может быть пустым.</span><?php } ?>
    <form action="<?= $_SERVER['PHP_SELF']; ?>" method="post">
        <div class="fieldGroup"><label for="pw0">Новый пароль</label><div class="field"><input type="password" class="input" name="pw0" id="pw0" value="" maxlength="20"></div></div>
        <div class="fieldGroup"><label for="pw1">Подтверждение пароля</label><div class="field"><input type="password" class="input" name="pw1" id="pw1" value="" maxlength="20"></div></div>
        <input type="hidden" name="subStep" value="3" />
        <input type="hidden" name="userID" value="<?= $securityUser=='' ? $_POST['userID'] : $securityUser; ?>" />
        <input type="hidden" name="key" value="<?= $_GET['email']=='' ? $_POST['key'] : $_GET['email']; ?>" />
        <div class="fieldGroup"><input type="submit" value="Submit" style="margin-left: 150px;" /></div>
        <div class="clear"></div>
    </form>
    <?php break; case 'invalidKey': ?>
    <h2>Неверный ключ</h2>
    <p>Вы ввели неверный ключ. Или срок восстановления (3 дня) уже истёк, или вы уже пользовались этим ключём.<br /><br /><a href="login.php">Вернуться</a> на главную страницу. </p>
    <?php break; case 'recoverSuccess': ?>
    <h2>Пароль изменён</h2>
    <p>Поздравляю! Вы успешно изменили свой пароль.</p><br /><br /><a href="login.php">Вернуться</a> на главную. </p>
    <?php break; case 'speedLimit': ?>
    <h2>Предупреждение</h2>
    <p>Вы много раз ответили неверно на секретный вопрос. Вы заблокированы на 15 минут.</p><br /><br /><a href="login.php">Вернуться</a> на главную. </p>
    <?php break; }
	ob_flush();
	$mySQL->close();
?>

"recoverForm" отображает форму для изменения пароля. Мы отображаем имя пользователя и сообщение об ошибке, если требуется. Далее следуют поля для ввода пароля, его подтверждения, стадии заполнения формы, идентификатора пользователя и ключа безопасности, полученного в письме. Для ключа и идентификатора пользователя используем специальную проверку в блоке if(), чтобы удостовериться, что мы получаем переменную из нужного места. Если строка запроса пуста, проверяем, есть ли значение у переменной post.

"invalidKey" означает, что введенный ключ не существует или срок его действия истек. Сообщение "speedLimit" отображается, если пользователь был заблокирован из-за многократного ввода неверных ответов. В завершение, вызываем ob_flush(), чтобы загрузить страницу в браузере, и $mySQL->close() для закрытия подключения к базе данных.

ПРИМЕЧАНИЕ: сообщения "recoverForm" и "invalidKey" не нужны в случае нешифруемых паролей.

Выводы

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

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

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

По поводу загрузки: в файлах для загрузки находятся версии для шифруемых и нешифруемых паролей в двух разных папках.

Данный урок подготовлен для вас командой сайта ruseller.com
Источник урока: www.net.tutsplus.com/tutorials/php/creating-an-advanced-password-recovery-utility/
Перевел: Станислав Протасевич
Урок создан: 10 Февраля 2011
Просмотров: 33129
Правила перепечатки


5 последних уроков рубрики "PHP"

  • Фильтрация данных с помощью zend-filter

    Когда речь идёт о безопасности веб-сайта, то фраза "фильтруйте всё, экранируйте всё" всегда будет актуальна. Сегодня поговорим о фильтрации данных.

  • Контекстное экранирование с помощью zend-escaper

    Обеспечение безопасности веб-сайта — это не только защита от SQL инъекций, но и протекция от межсайтового скриптинга (XSS), межсайтовой подделки запросов (CSRF) и от других видов атак. В частности, вам нужно очень осторожно подходить к формированию HTML, CSS и JavaScript кода.

  • Подключение Zend модулей к Expressive

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

  • Совет: отправка информации в Google Analytics через API

    Предположим, что вам необходимо отправить какую-то информацию в Google Analytics из серверного скрипта. Как это сделать. Ответ в этой заметке.

  • Подборка PHP песочниц

    Подборка из нескольких видов PHP песочниц. На некоторых вы в режиме online сможете потестить свой код, но есть так же решения, которые можно внедрить на свой сайт.

или авторизуйтесь, чтобы добавлять комментарии, оценивать уроки и сохранять их в личном кабинете
  • 10 Февраля 2011 09:07
    OdinecDenis
    вполне познавательно
  • 10 Февраля 2011 10:05
    Руслан Димитриев
    Конечно, информативно, но зачем так париться с восстановлением пароля?
  • 10 Февраля 2011 12:08
    IKLO
    Руслан, согласен. можно ведь все на много проще сделать. Просто при воде мыла на странице восстановления проверять есть ли пользователь с таким мылом , если есть то отправлять ссылку с каким нить ключом с ограниченным временем действия , пользователь введет новый пароль и ключ становится не действительным. вот простой алгоритм , который написать сможет даже не опытный кодер . Не надо все так усложнять .
    • 10 Февраля 2011 16:48
      notbot
      плюс один
    • 3 Июля 2012 14:54
      Bartalome
      а как так ссделать?вообще не представляю(
  • 10 Февраля 2011 16:49
    notbot
    не читал, т.к. увидел, что создаётся три лишних поля в таблице...
  • 10 Февраля 2011 19:51
    Constantine
    Для всех: Создание -->профессиональной<-- утилиты восстановления пароля.
    • 2 Апреля 2011 09:56
      budzin
      :) надо же им как-то проявить себя :)
  • 10 Февраля 2011 19:57
    eko24ive
    Вообщем очень даже ничего. Только с моментами безопасности уж очень перегнули. Согласен с IKLO
    • 10 Февраля 2011 21:03
      Станислав Протасевич
      Вопросы безопасности превыше всего))))
      • 10 Февраля 2011 21:18
        eko24ive
        Я имел в ввиду что такой метод восстановления очень "крут" для "простых" сайтов
  • 10 Февраля 2011 21:37
    Slavoz
    Мде, а не проще выслать новый пароль на почту?? нет, это конечно уже другой способ восстановления доступа к сайту, но все же так проще
  • 15 Февраля 2011 23:52
    Hekman (Roudy Rec.)
    10 $questions[5] = "Любимая можель машины?";
    хых, посмеялся))) а урок отличный, +1
  • 29 Апреля 2012 08:31
    a.igus
    Довольно познавательный урок. А излишняя безопасность еще никому не навредила, скорее наоборот.
^ Наверх ^