Регистрация и авторизация пользователей на PHP. Часть шестая
С момента публикации предыдущей части статьи время авторизации, конечно же, уже истекло. Поэтому, если мы зайдем на главную страницу нашего сайта, мы не увидим информации о пользователе. Самое время теперь, наконец, заняться реализацией алгоритма авторизации пользователя через форму.
Внесем небольшие изменения в код страниц нашего сайта, подправив сценарий в файле/blocks/header.php.
<!-- Файл /blocks/header.php --> <header class="bg-dark text-white"> <div class="container-fluid container-lg"> <nav class="navbar navbar-dark bg-dark"> <a class="navbar-brand link-decor" href="/lk.php">Личный кабинет</a> <?php // Форму авторизации нужно показывать только неавторизованным пользователям // Если пользователь прошел авторизацию, ему должны быть достуные другие возможности // Для авторизованных пользователей if (!is_null(Core_Page::instance()->user)) { ?> <a class="nav-item" href="/logout.php">Выйти</a> <?php } // Для неавторизованных пользователей показываем форму авторизации else { ?> <form id="user-authorization" name="user-authorization" class="form-inline" autocomplete="off" onsubmit="return false;" method="post" action="/service.php"> <label class="sr-only" for="user_login">Логин</label> <div class="input-group mb-2 mb-lg-0 mr-sm-2"> <div class="input-group-prepend"> <div class="input-group-text">@</div> </div> <input class="form-control mr-sm-2" type="text" id="user_auth_login" name="user_auth_login" placeholder="Логин" aria-label="Логин" /> </div> <input class="form-control mb-2 mb-lg-0 mr-sm-2" type="password" id="user_auth_password" name="user__auth_password" placeholder="Пароль" aria-label="Пароль" /> <div class="form-check mb-2 mb-lg-0 mr-sm-2"> <input class="form-check-input" type="checkbox" id="user_auth_remember" name="user_remember" /> <label class="form-check-label" for="user_auth_remember"> Запомнить меня </label> </div> <!-- Скрытое поле формы, в котором хранится строка запроса к сценарию-обработчику --> <input type="hidden" id="user_auth_request" name="request" value="user_auth_login" /> <button class="btn btn-success my-2 my-sm-0" type="submit">Войти</button> </form> <?php } ?> </nav> <div class="row text-xl-right"> <div class="col-12"> <div class="mr-xl-3 ml-xl-0 ml-3 mb-3"> <a href="/registration.php" class="link-decor">Зарегистрироваться</a> </div> </div> </div> </div> </header>
Проверяем.
Мы видим форму аутентификации, пользователь у нас не авторизован. Всё верно. Теперь переходим к реализации непосредственно аутентификации. Она у нас будет выполняться посредством AJAX-запросов. Для формы авторизации в файл scripts.js добавляем обработчик.
Реализация алгоритма авторизации пользователя
Прежде, чем приступить, давайте подумаем, как вообще будет проходить процесс авторизации пользователя?
- Пользователь вводит логин и пароль.
- Пользователь может установить флажок «Запомнить меня», чтобы время его авторизации было больше предусмотренного PHP механизмом времени жизни сессий.
- Пользователь отправляет свои данные для авторизации.
- Нам вообще всё равно, соответствует ли логин пользователя корректному формату адреса электропочты.
- Нам аналогично все равно на то, какой пароль ввел пользователь.
- Проверяем, существует ли в базе данных пользователь с указанными данными.
- Если пользователя с такими данными нет, показываем сообщение об ошибке.
- Если такой пользователь есть, авторизуем его.
- Сообщаем пользователю об успешной авторизации, обновляем страницу.
// Файл /js/scripts.js // Используем строгий режим ECMAScript 5 "use strict"; (function($) { jQuery(function() { // Обработчик события отправки формы регистрации пользователя на сайте $("#user-registration").on("submit", $.userRegistrationHandler) // Добавляем обработчик на поля формы // Если поле формы в фокусе, очищаем его от ошибки .find("input:text, input:password, input[type='email']") .on("focus", $.cleanError); // Обработчик события отправки формы авторизации пользователя на сайте $("#user-authorization").on("submit", $.userAuthorization); }); })(jQuery);
Ну и непосредственно код самого обработчика события отправки формы метод $.userAuthorization(). Он у нас будет в файле func.js.
Обработка AJAX-запроса на аутентификацию на стороне сервера
// Файл /js/func.js // Используем строгий режим ECMAScript 5 "use strict"; $.extend({ /** * Параметры по умолчанию для AJAX-запросов */ request: { // Путь для запросов url: "", // Метод запросов type: "POST", // Тип данных, которые ожидаются в ответ от сервера dataType: "json", // Действия, выполняемые при неуспешных запросах error: function(jqXHR, textStatus, errorThrown) { // Пишем текст ошибки в консоль console.log("textStatus: " + textStatus + ". Error: " + errorThrown); // Выводим ошибку на экран alert("textStatus: " + textStatus + ". Error: " + errorThrown); } }, /** * Объект для хранения результатов AJAX-запросов */ response: { data: null, textStatus: null, jqXHR: null }, /** * Очищает все поля формы от сообщений об ошибках */ cleanAllErrors: function(form) { // Если не передана форма, ничего не делаем if (!form) { return false; } // Внутри формы ищем все поля для сообщений об ошибке, очищаем их и убираем с экрана $( form ).find(".error").hide().text("") // Для всех полей формы убираем классы валидации .end() .find("input:text, input:password, input[type='email']") .removeClass("is-valid").removeClass("is-invalid"); // Если есть поле для сообщения пользователю if ($("#user-message").length) { // Очищаем и его $("#user-message").html(""); } // Сбрасываем капчу if (typeof (grecaptcha) != "undefined" && typeof (grecaptcha.reset) == "function") { grecaptcha.reset(); } }, /** * Очищает поле формы от ошибки */ cleanError: function(event) { // Если не передано поле, ничего не делаем if (!event) { return false; } console.log("OK"); // Сохраняем идентификатор поля формы var field = event.currentTarget, id = $( field ).attr("id"); // Очищаем у поля классы валидации $( field ).removeClass("is-valid").removeClass("is-invalid"); // Очищаем поле ошибки, если оно есть $("#" + id + "-error").hide().text(""); }, /** * Показывает ошибку для поля формы */ showError: function(sFieldId, sMessage) { // Если не передан идентификатор поля, ничего не делаем if (!sFieldId) { console.log("Не передан идентификатор поля формы"); return false; } var field = $("#" + sFieldId), // Поле формы // Поле для сообщения об ошибке errorField = $("#" + sFieldId + "-error"), // Текст сообщения об ошибке по умолчанию sMessage = sMessage || "Поле заполнено неверно. Проверьте"; // Показываем сообщение об ошибке field.addClass("is-invalid"); errorField.text(sMessage).show(); }, /** * Помечает поле указанным способом */ validField: function(fieldId, validType) { // Результат проверки по-умолчанию validType = validType || true; if (!fieldId) { console.log("Не указано поле, которое необходимо отметить как проверенное"); alert("Не указано поле, которое необходимо отметить как проверенное"); return false; } var className = (validType === true) ? "is-valid" : "is-invalid"; $("#" + fieldId).addClass(className); }, /** * Проверяет корректность капчи */ checkCapctha: function(sCaptcha, url) { // Если нет капчи, проверка не может быть успешной if (!sCaptcha) { return false; } // URL для запроса url = url || ""; // Подготавливаем данные для AJAX-запроса var request = {}; // Строка AJAX-запроса request.data = "request=checkCaptcha&captchaValue=" + sCaptcha; // URL для AJAX-запроса request.url = url; // Запрос будет синхронный request.async = false; // Действия после выполнения запроса request.success = function(data, textStatus, jqXHR ) { // Сохраняем результат выполнения запроса $.response.data = data; $.response.textStatus = textStatus; $.response.jqXHR = jqXHR; }; request = $.extend({}, $.request, request); // Выполняем запрос $.ajax(request); // Возвращаем результат проверки капчи return $.response.data.success; }, /** * Проверяет пароль на соответствие требованиям */ checkPassword: function(sPassword) { var response = { success: false, message: "Пароль для проверки не передан" }, pattern1 = {}, pattern2 = {}, pattern3 = {}; // Если не передан пароль if (!sPassword) { return response; } /** * Нужно проверить пароль на соответствие нескольким требованиям * * 1. Длина пароля не менее 6 и не более 32 символов * 2. Пароль не может содержать символы кроме символов латинского алфавита и *-+!@#$[]{}'";:><,. * 3. Пароль доджен содержать как минимум одну цифру, и как минимум одну заглавную букву * * Для каждого из условий добавляем проверку */ // Для указанного щаблона регулярного выражения... pattern1.pattern = /^.{6,32}$/; // ...нужен положительный результат pattern1.result = true; // Для указанного щаблона регулярного выражения... pattern2.pattern = /[^-\w\d*+!@#$\[\]\{\}'";:><,.]/; // ...нужен отрицательный результат pattern2.result = false; // Для указанного щаблона регулярного выражения... pattern3.pattern = /([A-Z].+?\d|\d.+?[A-Z])/; // ...нужен положительный результат pattern3.result = true; if (pattern1.pattern.test(sPassword) !== pattern1.result) { response.message = "Пароль не соответствует требованиям по длине"; return response; } else if (pattern2.pattern.test(sPassword) !== pattern2.result) { response.message = "Пароль содержит недопустимые символы"; return response; } else if (pattern3.pattern.test(sPassword) !== pattern3.result) { response.message = "Пароль не содержит в себе обязательные симовлы"; return response; } // Если все проверки прошли else { response.success = true; response.message = "Пароль соответствует требованиям"; } return response; }, /** * Проверяет логин пользователя * * Проверка логина состоит из двух этапов. * * 1. Проверяем корректность указанного адреса электропочты * 2. Проверяем уникальность указанного адреса электропочты */ checkLogin: function(email, url) { var response = { success: false, message: "Не удалось проверить логин пользователя" }, request = {}; // Путь для выполнения AJAX-запросов по умолчанию url = url || ""; if (!email) { console.log("Не передано значение логина пользователя для проверки"); response.message = "Не передано значение логина пользователя для проверки"; return response; } var pattern = /^[-_.\da-z]+@[-.a-z\d]{1,}\.(ru|com|su|[a-z]{2,4})$/i; // Убеждаемся в корректности адреса электропочты if (!pattern.test(email)) { response.message = "Некорректный формат адреса электропочты"; return response; } // Проверяем уникальность логина пользователя request.data = "request=checkLoginExists&email=" + email; // URL для AJAX-запроса request.url = url; // Запрос будет синхронным request.async = false; // Действия после выполнения запроса request.success = function(data, textStatus, jqXHR ) { // Сохраняем результат выполнения запроса $.response.data = data; $.response.textStatus = textStatus; $.response.jqXHR = jqXHR; // Если запрос выполнен успешно if (typeof (data) != "undefined") { // Если email не уникален if (data.success === false) { response.message = data.message; } else { response.success = true; response.message = "Указанный логин доступен для регистрации"; } } }; request = $.extend({}, $.request, request); // Выполняем запрос $.ajax(request); return response; }, /** * Обработывает форму регистрации пользователя на сайте по событию отправки формы */ userRegistrationHandler: function() { var form = $( this ), // Сама форма регистрации пользователя на сайте login = $("#user_login").val(), // Логин пользователя password = $("#user_password").val(), // Пароль пользователя password_approve = $("#user_password_approve").val(), // Подтвержденный пароль пользователя sCaptcha = form.find(".g-recaptcha-response").val(), // Значение капчи Google reCAPTCHA checkPassword, checkLogin; // Сначала проверяем капчу // Продолжаем обработку формы только в случае корректной капчи if ($.checkCapctha(sCaptcha, form.attr("action"))) { // Очищаем все поля ошибок $.cleanAllErrors(this); // Если введенные пароли не идентичны, прекращаем обработку if (password !== password_approve) { $.showError("user_password_approve", "Введенные вами пароли не совпадают"); // Помечаем поле как проверенное $.validField("user_password_approve", false); return false; } else { // Помечаем поле как проверенное $.validField("user_password_approve", true); } // Проверяем пароль на соответствие требованиям // Если он не подходит, прекращаем работу checkPassword = $.checkPassword(password); if (!checkPassword.success) { $.showError("user_password", checkPassword.message); // Помечаем поле как проверенное $.validField("user_password", false); return false; } else { // Помечаем поле как проверенное $.validField("user_password", true); } // Проверяем логин пользователя, являющийся одновременно и адресом электропочты // Если он не прошел проверку, прекращаем проверку checkLogin = $.checkLogin(login, form.attr("action")); if (!checkLogin.success) { $.showError("user_login", checkLogin.message); // Помечаем поле как проверенное $.validField("user_login", false); return false; } else { // Помечаем поле как проверенное $.validField("user_login", true); } // Если нет ошибок, регистрируем пользователя if ($("#user-registration .error:visible").length == 0) { var request = {}; // Формируем объект данных, который будет передан AJAX-запросом request.data = { request: "userRegistration", login: login, password: password, password_approve: password_approve }; // URL для AJAX-запроса request.url = $("#user-registration").attr("action"); // Запрос будет синхронным request.async = false; // Действия после выполнения запроса request.success = function(data, textStatus, jqXHR ) { // Сохраняем результат выполнения запроса $.response.data = data; $.response.textStatus = textStatus; $.response.jqXHR = jqXHR; // Если запрос выполнен успешно if (typeof (data) != "undefined") { // Если при регистрации что-то пошло не так if (data.success === false) { showError("user_message", data.message); } else { // Очищаем поля формы $("#user-registration").trigger("reset"); alert(data.message + "\nСейчас вы будете перенаправлены на главную страницу сайта"); window.location.href = "/"; } } }; request = $.extend({}, $.request, request); // Выполняем запрос $.ajax(request); } else { alert("При заполнении полей формы были найдены ошибки. Пожалуйста, исправьте."); } } // Если капча была заполнена некорректно, показываем ошибку else { alert("Вы не подтвердили, что вы не робот. Ответьте на вопрос, пожалуйста."); // Больше ничего не делаем return false; } }, /** * Авторизует пользователя на сайте */ userAuthorization: function() { var form = $( this ), // Форма авторизации пользователя login = $( "#user_auth_login" ).val(), // Логин пользователя password = $("#user_auth_password").val(), // Пароль пользователя remember = ($("#user_auth_remember").prop("checked")) ? 1 : 0, // Нужно ли запоминать авторизацию пользователя request = {}; // Создаем объект запроса // Данные для запроса request.data = { request: "userAuthorization", login: login, password: password, remember: remember }; // URL для AJAX-запроса request.url = form.attr("action"); // Запрос будет синхронным request.async = false; // Действия после выполнения запроса request.success = function(data, textStatus, jqXHR ) { // Сохраняем результат выполнения запроса $.response.data = data; $.response.textStatus = textStatus; $.response.jqXHR = jqXHR; // Если запрос выполнен успешно if (typeof (data) != "undefined") { // Если при авторизации что-то пошло не так if (data.success === false) { alert(data.message); } else { alert(data.message + "\nСейчас страница сайта будет перезагружена"); window.location.reload(); } } }; request = $.extend({}, $.request, request); // Выполняем запрос $.ajax(request); } }); (function($) { jQuery(function() { }); })(jQuery);
Пробуем теперь авторизоваться
Конечно же, в процессе выполнения запроса что-то пошло не так. Ведь мы не сделали ничего на стороне сервера, чтобы процесс прошел успешно. Переходим к сценарию в файле-обработчике AJAX-запросов — service.php.
<?php // Файл service.php // Инициализируем ядро сайта require_once("bootstrap.php"); // Делаем что-либо только если выполняется запрос if (!empty($_REQUEST) && !empty(Core_Array::getRequest("request"))) { // По умолчанию данные передаём в формате JSON $bJson = TRUE; // По умолчанию прерываем работу сценария $bExit = TRUE; // Данные, отправляемые в ответ на запрос по умолчанию $response = array( "success" => FALSE, "message" => "При выполнении запроса что-то пошло не так" ); // Сохраняем строку значения типа запроса $sRequest = strval(Core_Array::getRequest("request")); // Удаляем данные о типе запроса из суперглобального массива unset($_REQUEST["request"]); // Для разных типов запроса разные действия switch ($sRequest) { // Проверка корректности капчи Google reCAPTCHA case "checkCaptcha": // Сохраняем полученное значение капчи $sCaptcha = strval(Core_Array::getRequest("captchaValue")); // Получаем параметры конфигурации для Google reCAPTCHA $aCaptcha_Config = Core_Config::instance()->get("captcha"); // Формируем данные для передачи в запросе $aData = array( "secret" => $aCaptcha_Config["secret_key"], "response" => $sCaptcha ); // Запрос к серверу Google выполним через cURL $curl = curl_init(); // Указываем параметры запроса curl_setopt($curl, CURLOPT_URL, $aCaptcha_Config["url_verify"]); // Нам нужен возврат результата, а не вывод в браузер curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); // Будем ли передавать заголовки curl_setopt($curl, CURLOPT_HEADER, FALSE); // Подставляем передаваемые данные curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($aData)); // Выполняем запрос и сохраняем его результат $sResponse = curl_exec($curl); // Преобразуем ответ в массив $response = json_decode($sResponse, TRUE); break; // Проверка уникальности логина пользователя case "checkLoginExists": // Сохраняем переданное значение логина $sEmail = strval(Core_Array::getRequest("email")); // Пробуем найти пользователя по указанному логину $oUser = Core::factory("User") ->getByLogin($sEmail); // Если такого пользователя нет if (is_null($oUser)) { $response["success"] = TRUE; $response["message"] = "Указанный вами логин доступен для регистрации"; } // Если такой логин в системе уже зарегистрирован else { $response["message"] = "Пользователь с указанным логином уже зарегистрирован"; } break; // Регистрация нового пользователя case "userRegistration": // Сохраняем переданные данные $login = strval(Core_Array::getRequest("login")); $password = strval(Core_Array::getRequest("password")); $password_approve = strval(Core_Array::getRequest("password_approve")); // Создаем объект модели пользователя $oUser = Core::factory("User"); // Создаем объект контроллера пользователя $User_Controller = new User_Controller($oUser); // Добавляем в контроллер полученные данные $User_Controller->setLogin($login) ->setPassword($password) ->setPasswordApprove($password_approve); // Сначала проверяем все переданные данные // Если данные не прошли проверку, больше ничего не делаем if (!$User_Controller->checkRegistrationData()) { $response["message"] = "Переданные на регистрацию данные о пользователе не прошли проверку."; break; } // Регистрируем пользователя, сохраняя его в БД $iUserId = $User_Controller->registration(); // Если пользователь успешно сохранен if (!is_null($iUserId)) { // Авторизуем на сайте нового пользователя сразу же $oUser->authUser(); /** * Это может показаться странным, но методу authUser() действительно * не передаётся никакой идентификатор. В объект модели уже были * загружены необходимые данные пользователя в процессе его регистрации */ $response["success"] = TRUE; $response["message"] = "Пользователь с логином " . $oUser->login . " успешно зарегистрирован"; } else { $response["message"] = "Не удалось зарегистрировать пользователя с логином " . $login; } break; // Авторизация пользователей case "userAuthorization": // Сохраняем данные для авторизации $login = strval(Core_Array::getRequest("login")); $password = strval(Core_Array::getRequest("password")); $remember = boolval(Core_Array::getRequest("remember")); // Создаем новый массив из данных для авторизации $aUserData = array( "login" => $login, "password" => $password, "remember" => $remember ); // Создаем объект модели пользователя $oUser = Core::factory("User"); // Создаем контроллер пользователя $User_Controller = new User_Controller($oUser); // Авторизуем пользователя $response = $User_Controller->authorization($aUserData); break; } // Если требуется представить данные в формате JSON $bJson && print json_encode($response); // Если требуется прервать выполнение скрипта $bExit && exit(); } else { exit("Доступ к разделу запрещен"); } ?>
Что теперь? Теперь нужно в контроллере объекта модели пользователя реализовать метод User_Controller::authorization().
Пожалуйста!
Доработка контроллера объекта модели пользователя
<?php // Файл modules/core/user/controller.php defined("LEZH") || exit("Доступ к файлу запрещен"); class User_Controller extends Core_Controller { // Логин и пароль пользователя protected $_userData = array(); public function __construct(User_Model $oUser) { // Вызываем конструктор родительского класса, // который сохранит объект модели пользователя parent::__construct($oUser); } /** * Проверяет пользовательские данные перед регистрацией */ public function checkRegistrationData() { if (empty($this->_userData["login"])) { exit("Не установлено значение логина пользователя для проверки"); } if (empty($this->_userData["password"])) { exit("Не установлено значение пароля пользователя для проверки"); } if (empty($this->_userData["password_approve"])) { exit("Не установлено значение подтверждения пароля пользователя для проверки"); } // Введенные пароли должны быть идентичны if ($this->_userData["password"] !== $this->_userData["password_approve"]) { return FALSE; } // В качестве логина пользователя должен быть указан корректный адрес электропочты if (!filter_var($this->_userData["login"], FILTER_VALIDATE_EMAIL)) { return FALSE; } // Логин пользователя дожен быть уникальным $oUser = Core::factory("User") ->getByLogin($this->_userData["login"]); // Если такой пользователь есть, прекращаем выполнение сценария if (!is_null($oUser)) { return FALSE; } /** * Нужно проверить пароль на соответствие нескольким требованиям * * 1. Длина пароля не менее 6 и не более 32 символов * 2. Пароль не может содержать символы кроме символов латинского алфавита и *-+!@#$[]{}'";:><,. * 3. Пароль доджен содержать как минимум одну цифру, и как минимум одну заглавную букву * * Для каждого из условий добавляем проверку */ if (!preg_match("/^.{6,32}$/", $this->_userData["password"]) || preg_match("/[^-\w\d*+!@#$\[\]\{\}'\"