Авторизация и регистрация на PHP и Ajax
Напишем систему авторизации и регистрации пользователей на PHP. Для работы скриптов потребуется интерпретатор PHP версии 5.3 и сервер MySQL 5.
Создание базы данных
Создадим новую базу данных с названием testdb, выполнив следующий запрос от привилегированного пользователя.
// Создание базы данных testdb с кодировкой utf8 CREATE DATABASE testdb CHARACTER SET utf8 COLLATE utf8_general_ci;
Для работы с базой, добавим отдельного пользователя testdb и предоставим ему необходимые права.
// Создание пользователя testdb с паролем testdb CREATE USER testdb IDENTIFIED by 'testdb'; // Выделение прав на все таблицы базы testdb GRANT ALL PRIVILEGES ON testdb@localhost TO testdb WITH GRANT OPTION;
Структура таблицы users
Для хранения пользователей создадим в базе данных таблицу users, выполнив следующий запрос.
// Создание таблицы для хранения пользователей CREATE TABLE IF NOT EXISTS `users` ( `id` MEDIUMINT NOT NULL AUTO_INCREMENT PRIMARY KEY, `username` VARCHAR(255) NOT NULL UNIQUE, `password` VARCHAR(255) NOT NULL, `salt` VARCHAR(100) NOT NULL ) ENGINE=INNODB;
- id — уникальный идентификатор пользователя
- username — логин
- password — шифрованный пароль
- salt — соль для шифрования пароля
Структура файлов и директорий
- login.php — страница авторизации пользователя
- register.php — страница регистрации
- ajax.php — Файл для обработки Ajax-запросов
- js — клиентские скрипты
- ajax-form.js — скрипт для работы с Ajax-формами
- css — стили CSS
- style.css — основной файл стилей
- reset.css — файл сброса кастомных стилей браузеров
- vendor — сторонние third-party библиотеки
- bootstrap — Twitter Bootstrap 2.3.2
- AjaxRequest.class.php — обертка для работы с Ajax-запросами
- Auth.class.php — класс для работы с пользователями
Регистрация пользователей
При регистрации, пользователю нужно ввести логин, пароль и подтверждения пароля. После отправки формы будем делать проверку на существование введенного логина. Если логин уже существует — уведомляем об этом посетителя.
Вся работа с базой данных будет происходить через расширение PDO для PHP. Оно включено в стандартную библиотеку PHP, начиная с версии 5.1.
Основная логика приложения находится в файле classes/Auth.class.php. Разберем подробнее его содержимое.
username = $username; $this->connectDb($this->db_name, $this->db_user, $this->db_pass, $this->db_host); > public function __destruct() { $this->db = null; > public static function isAuthorized() { if (!empty($_SESSION["user_id"])) { return (bool) $_SESSION["user_id"]; > return false; > public function passwordHash($password, $salt = null, $iterations = 10) { $salt || $salt = uniqid(); $hash = md5(md5($password . md5(sha1($salt)))); for ($i = 0; $i < $iterations; ++$i) { $hash = md5(md5(sha1($hash))); >return array('hash' => $hash, 'salt' => $salt); > public function getSalt($username) { $query = "select salt from users where username = :username limit 1"; $sth = $this->db->prepare($query); $sth->execute( array( ":username" => $username ) ); $row = $sth->fetch(); if (!$row) { return false; > return $row["salt"]; > public function authorize($username, $password, $remember=false) { $query = "select id, username from users where username = :username and password = :password limit 1"; $sth = $this->db->prepare($query); $salt = $this->getSalt($username); if (!$salt) { return false; > $hashes = $this->passwordHash($password, $salt); $sth->execute( array( ":username" => $username, ":password" => $hashes['hash'], ) ); $this->user = $sth->fetch(); if (!$this->user) { $this->is_authorized = false; > else { $this->is_authorized = true; $this->user_id = $this->user['id']; $this->saveSession($remember); > return $this->is_authorized; > public function logout() { if (!empty($_SESSION["user_id"])) { unset($_SESSION["user_id"]); > > public function saveSession($remember = false, $http_only = true, $days = 7) { $_SESSION["user_id"] = $this->user_id; if ($remember) { // Save session id in cookies $sid = session_id(); $expire = time() + $days * 24 * 3600; $domain = ""; // default domain $secure = false; $path = "/"; $cookie = setcookie("sid", $sid, $expire, $path, $domain, $secure, $http_only); > > public function create($username, $password) { $user_exists = $this->getSalt($username); if ($user_exists) { throw new \Exception("User exists: " . $username, 1); > $query = "insert into users (username, password, salt) values (:username, :password, :salt)"; $hashes = $this->passwordHash($password); $sth = $this->db->prepare($query); try { $this->db->beginTransaction(); $result = $sth->execute( array( ':username' => $username, ':password' => $hashes['hash'], ':salt' => $hashes['salt'], ) ); $this->db->commit(); > catch (\PDOException $e) { $this->db->rollback(); echo "Database error: " . $e->getMessage(); die(); > if (!$result) { $info = $sth->errorInfo(); printf("Database error %d %s", $info[1], $info[2]); die(); > return $result; > public function connectdb($db_name, $db_user, $db_pass, $db_host = "localhost") { try { $this->db = new \pdo("mysql:host=$db_host;dbname=$db_name", $db_user, $db_pass); > catch (\pdoexception $e) { echo "database error: " . $e->getmessage(); die(); > $this->db->query('set names utf8'); return $this; > >
Алгоритм регистрации
Для создания нового пользователя используется метод User::create(), который принимает логин и пароль в качестве аргументов.
Первым делом, проверяем существование пользователя. Для этого используем метод User::getSalt(), который выбирает из базы «соль» пользователя по его логину. Соль нужна для усложнения подбора паролей пользователей в случае утечки базы.
Если пользователь существует — выбрасываем исключение. Иначе, генерируем новую соль и хешируем ей пароль. После этого выполняем запрос на добавление данных в базу. Если при выполнении запроса происходит ошибка, печатаем соответствуюее сообщение и завершаем работу скрипта. Такая ситуация может произойти при отключении сервера MySQL или его внутренней ошибки.
Если пользователь был усшешно создан, функция User::create() возвращает его уникальный идентификатор. Это обычное числовое поле, которое автоматически увеличивается при добавлении записей в таблицу.
Алгоритм аутентификации
Для того, чтобы проверить правильность ввода логина и пароля, используется метод User::authorize(). Первым делом, мы проверяем существование юзера, пытаясь выбрать его соль из базы. Если пользователь не найден, сразу возвращаем false. Иначе, хешируем принятый пароль этой солью через функцию User::passwordHash(). Затем, делаем выборку из базы по логину и хешу пароля.
Если результат запроса оказался непустым, то логин и пароль верные. Сохраняем пользотельские данные в объекте класса User. Записываем id пользователя в сессию через метод User::saveSession(). Если в качестве первого аргумента — $remember, передать ей true, то идентификатор сессии сохранится в куках. Это позволит не вводить пароль каждый раз при перезапуске браузера.
Работа с формами через Ajax
Для обработки Ajax-запросов создадим класс AjaxRequest. Сохраним его в файле classes/AjaxRequest.class.php.
request = $request; $this->action = $this->getRequestParam("act"); if (!empty($this->actions[$this->action])) { $this->callback = $this->actions[$this->action]; call_user_func(array($this, $this->callback)); > else { header("HTTP/1.1 400 Bad Request"); $this->setFieldError("main", "Некорректный запрос"); > $this->response = $this->renderToString(); > public function getRequestParam($name) { if (array_key_exists($name, $this->request)) { return trim($this->request[$name]); > return null; > public function setResponse($key, $value) { $this->data[$key] = $value; > public function setFieldError($name, $message = "") { $this->status = "err"; $this->code = $name; $this->message = $message; > public function renderToString() { $this->json = array( "status" => $this->status, "code" => $this->code, "message" => $this->message, "data" => $this->data, ); return json_encode($this->json, ENT_NOQUOTES); > public function showResponse() { header("Content-Type: application/json; charset=UTF-8"); echo $this->response; > >
Этот класс облегчит нам обработку данных, отправленных пользователем из формы. В качестве конструктора, он принимает массив с данными запроса ($_GET или $_POST).
В запросе обязательно должно быть поле act, которое определяет текущее действие. Например, при регистрации значением $_POST[«act»] будет «register», а при авторизации — «login».
Метод getRequestParam() нужен для получения параметра из запроса. Он делает дополнительную проверку на существование ключа массива и возвращает null, если запрос не содержит нужных данных.
Функция setResponse() используется для формирования ответа. Метод setFieldError() нужен для передачи сообщения об ошибке в поле формы.
Для того, чтобы вернуть ответ пользователю, мы используем метод showResponse. Он генерирует строку в JSON-формате, задает нужные HTTP-заголовки и возвращает данные клиенту.
В файле ajax.php происходит непосредственная обработка запросов через класс AjaxRequest.
session_start(); class AuthorizationAjaxRequest extends AjaxRequest { public $actions = array( "login" => "login", "logout" => "logout", "register" => "register", ); public function login() { if ($_SERVER["REQUEST_METHOD"] !== "POST") { // Method Not Allowed http_response_code(405); header("Allow: POST"); $this->setFieldError("main", "Method Not Allowed"); return; > setcookie("sid", ""); $username = $this->getRequestParam("username"); $password = $this->getRequestParam("password"); $remember = !!$this->getRequestParam("remember-me"); if (empty($username)) { $this->setFieldError("username", "Enter the username"); return; > if (empty($password)) { $this->setFieldError("password", "Enter the password"); return; > $user = new Auth\User(); $auth_result = $user->authorize($username, $password, $remember); if (!$auth_result) { $this->setFieldError("password", "Invalid username or password"); return; > $this->status = "ok"; $this->setResponse("redirect", "."); $this->message = sprintf("Hello, %s! Access granted.", $username); > public function logout() { if ($_SERVER["REQUEST_METHOD"] !== "POST") { // Method Not Allowed http_response_code(405); header("Allow: POST"); $this->setFieldError("main", "Method Not Allowed"); return; > setcookie("sid", ""); $user = new Auth\User(); $user->logout(); $this->setResponse("redirect", "."); $this->status = "ok"; > public function register() { if ($_SERVER["REQUEST_METHOD"] !== "POST") { // Method Not Allowed http_response_code(405); header("Allow: POST"); $this->setFieldError("main", "Method Not Allowed"); return; > setcookie("sid", ""); $username = $this->getRequestParam("username"); $password1 = $this->getRequestParam("password1"); $password2 = $this->getRequestParam("password2"); if (empty($username)) { $this->setFieldError("username", "Enter the username"); return; > if (empty($password1)) { $this->setFieldError("password1", "Enter the password"); return; > if (empty($password2)) { $this->setFieldError("password2", "Confirm the password"); return; > if ($password1 !== $password2) { $this->setFieldError("password2", "Confirm password is not match"); return; > $user = new Auth\User(); try { $new_user_id = $user->create($username, $password1); > catch (\Exception $e) { $this->setFieldError("username", $e->getMessage()); return; > $user->authorize($username, $password1); $this->message = sprintf("Hello, %s! Thank you for registration.", $username); $this->setResponse("redirect", "/"); $this->status = "ok"; > > $ajaxRequest = new AuthorizationAjaxRequest($_REQUEST); $ajaxRequest->showResponse();
Формат JSON-ответа
На клиентской стороне, мы должны иметь возможность показать результат операции в понятном для человека виде. Для этого мы возвращаем JSON ответ в таком формате:
{ "status": "статус операции", // err или ok "code": "имя поля с ошибкой", "message": "сообщение об ошибке", "data": "другие произвольные данные" >
Создадим файл js/ajax-form.js. Он будет перехватывать событие отправки всех форм с классом ajax и отправлять асинхронный запрос серверу. Исходный код ajax-form.js.
Для работы скрипта нужен jQuery версии 2.0.3 (лежит в архиве с исходниками).
Обработка ответов происходит в методах объекта script.ajaxForm.callbacks. Пример обработчика ответа для авторизации на сайте:
login: function ($form, data) { if (data.status === 'ok') { // если авторизация успешна, делаем редирект на // нужную страницу if (data.data && data.data.redirect) { window.location.href = data.data.redirect; > > >
Эти коллбеки вызываются только, если валидация ответа прошла успешно. Метод script.ajaxForm.validate проверяет наличие в ответе имени поля с ошибкой. Если такое поле существует, подствечивает его и отображает текст самой ошибки.