Напишем систему авторизации и регистрации пользователей на 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
— соль для шифрования пароля
Структура файлов и директорий
Создадим директорию «php-auth» для нашего проекта. Добавим в нее следующие файлы и папки:
login.php
— страница авторизации пользователяregister.php
— страница регистрацииajax.php
— Файл для обработки Ajax-запросовjs
— клиентские скриптыajax-form.js
— скрипт для работы с Ajax-формами
css
— стили CSSstyle.css
— основной файл стилейreset.css
— файл сброса кастомных стилей браузеров
vendor
— сторонние third-party библиотекиbootstrap
— Twitter Bootstrap 2.3.2
classes
— основной функционал модуляAjaxRequest.class.php
— обертка для работы с Ajax-запросамиAuth.class.php
— класс для работы с пользователями
Полный архив с исходниками (обновленная версия).
Регистрация пользователей
При регистрации, пользователю нужно ввести логин, пароль и подтверждения пароля. После отправки формы будем делать проверку на существование введенного логина. Если логин уже существует — уведомляем об этом посетителя.
Вся работа с базой данных будет происходить через расширение PDO для PHP. Оно включено в стандартную библиотеку PHP, начиная с версии 5.1.
Основная логика приложения находится в файле classes/Auth.class.php
. Разберем подробнее его содержимое.
<?php
namespace Auth;
class User
{
private $id;
private $username;
private $db;
private $user_id;
private $db_host = "localhost";
private $db_name = "testdb";
private $db_user = "testdb";
private $db_pass = "testdb";
private $is_authorized = false;
public function __construct($username = null, $password = null)
{
$this->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
.
<?php
/*
* Обертка для работы с Ajax-запросами
*/
class AjaxRequest
{
public $actions = array();
public $data;
public $code;
public $message;
public $status;
public function __construct($request)
{
$this->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
.
Содержимое ajax.php:
<?php
include './classes/Auth.class.php';
include './classes/AjaxRequest.class.php';
if (!empty($_COOKIE['sid'])) {
// check session id in cookies
session_id($_COOKIE['sid']);
}
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
проверяет наличие в ответе имени поля с ошибкой. Если такое поле существует, подствечивает его и отображает текст самой ошибки.
Если нашли ошибку в коде или тексте статьи — обязательно напишите о ней в комментариях.
UPDATE: Исправлены ошибки, связанные с хешированием пароля, нормально заработала функция «Запомнить меня». Спасибо пользователю santas156 за найденные баги.
Комментарии к статье: 148
Возможность комментировать эту статью отключена автором. Возможно, во всем виновата её провокационная тематика или большое обилие флейма от предыдущих комментаторов.
Если у вас есть вопросы по содержанию статьи, рекомендуем вам обратиться за помощью на наш форум.