Создание простой системы регистрации пользователей на PHP и MySQL. Продолжение доступно только подписчикам

Процесс создания системы регистрации – это довольно большой объем работы. Вам нужно написать код, который бы перепроверял валидность email-адресов, высылал email-письма с подтверждением, предлагал возможность восстановить пароль, хранил бы пароли в безопасном месте, проверял формы ввода и многое другое. Даже когда вы все это сделаете, пользователи будут регистрироваться неохотно, так как даже самая минимальная регистрация требует их активности.

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

PHP

Теперь мы готовы к тому, чтобы заняться кодом PHP. Основной функционал системы регистрации предоставляется классом User, который вы можете видеть ниже. Класс использует (), представляющую собой минималистскую библиотеку для работы с базами данных. Класс User отвечает за доступ к базам данных, генерирование token-ов для логина и их валидации. Он представляет нам простой интерфейс, который можно без труда включить в систему регистрации на ваших сайтах, основанных на PHP.

User.class.php

// Private ORM instance
private $orm;

/**
* Find a user by a token string. Only valid tokens are taken into
* consideration. A token is valid for 10 minutes after it has been generated.
* @param string $token The token to search for
* @return User
*/

Public static function findByToken($token){

// find it in the database and make sure the timestamp is correct


->where("token", $token)
->where_raw("token_validity > NOW()")
->find_one();

If(!$result){
return false;
}

Return new User($result);
}

/**
* Either login or register a user.
* @return User
*/

Public static function loginOrRegister($email){

// If such a user already exists, return it

If(User::exists($email)){
return new User($email);
}

// Otherwise, create it and return it

Return User::create($email);
}

/**
* Create a new user and save it to the database
* @param string $email The user"s email address
* @return User
*/

Private static function create($email){

// Write a new user to the database and return it

$result = ORM::for_table("reg_users")->create();
$result->email = $email;
$result->save();

Return new User($result);
}

/**
* Check whether such a user exists in the database and return a boolean.
* @param string $email The user"s email address
* @return boolean
*/

Public static function exists($email){

// Does the user exist in the database?
$result = ORM::for_table("reg_users")
->where("email", $email)
->count();

Return $result == 1;
}

/**
* Create a new user object
* @param $param ORM instance, id, email or null
* @return User
*/

Public function __construct($param = null){

If($param instanceof ORM){

// An ORM instance was passed
$this->orm = $param;
}
else if(is_string($param)){

// An email was passed
$this->
->where("email", $param)
->find_one();
}
else{

If(is_numeric($param)){
// A user id was passed as a parameter
$id = $param;
}
else if(isset($_SESSION["loginid"])){

// No user ID was passed, look into the sesion
$id = $_SESSION["loginid"];
}

$this->orm = ORM::for_table("reg_users")
->where("id", $id)
->find_one();
}

/**
* Generates a new SHA1 login token, writes it to the database and returns it.
* @return string
*/

Public function generateToken(){
// generate a token for the logged in user. Save it to the database.

$token = sha1($this->email.time().rand(0, 1000000));

// Save the token to the database,
// and mark it as valid for the next 10 minutes only

$this->orm->set("token", $token);
$this->orm->set_expr("token_validity", "ADDTIME(NOW(),"0:10")");
$this->orm->save();

Return $token;
}

/**
* Login this user
* @return void
*/

Public function login(){

// Mark the user as logged in
$_SESSION["loginid"] = $this->orm->id;

// Update the last_login db field
$this->orm->set_expr("last_login", "NOW()");
$this->orm->save();
}

/**
* Destroy the session and logout the user.
* @return void
*/

Public function logout(){
$_SESSION = array();
unset($_SESSION);
}

/**
* Check whether the user is logged in.
* @return boolean
*/

Public function loggedIn(){
return isset($this->orm->id) && $_SESSION["loginid"] == $this->orm->id;
}

/**
* Check whether the user is an administrator
* @return boolean
*/

Public function isAdmin(){
return $this->rank() == "administrator";
}

/**
* Find the type of user. It can be either admin or regular.
* @return string
*/

Public function rank(){
if($this->orm->rank == 1){
return "administrator";
}

Return "regular";
}

/**
* Magic method for accessing the elements of the private
* $orm instance as properties of the user object
* @param string $key The accessed property"s name
* @return mixed
*/

Public function __get($key){
if(isset($this->orm->$key)){
return $this->orm->$key;
}

Return null;
}
}
Token-ы генерируются при помощи алгоритма , и сохраняются в базу данных. Мы используем из MySQL для установки значения в колонку token_validity, равного 10 минутам. При валидации token, мы сообщаем движку, что нам нужен token, поле token_validity пока еще не истекло. Таким образом мы ограничиваем время, в течение которого token будет валиден.

Обратите внимание на то, что мы используем волшебный метод __get () в конце документа, чтобы получить доступ к свойствам объекта user. Это позволяет нам осуществить доступ к данным, которые хранятся в базе данных в виде свойств: $user->email, $user->token. Для примера давайте посмотрим, как мы можем использовать этот класс в следующем фрагменте кода:


Еще один файл, в котором хранится необходимый функционал, это functions.php. Там у нас есть несколько вспомогательных функций, которые позволяют нам сохранить остальной код более опрятным.

Functions.php

Function send_email($from, $to, $subject, $message){

// Helper function for sending email

$headers = "MIME-Version: 1.0" . "\r\n";
$headers .= "Content-type: text/plain; charset=utf-8" . "\r\n";
$headers .= "From: ".$from . "\r\n";

Return mail($to, $subject, $message, $headers);
}

function get_page_url(){

// Find out the URL of a PHP file

$url = "http".(empty($_SERVER["HTTPS"])?"":"s")."://".$_SERVER["SERVER_NAME"];

If(isset($_SERVER["REQUEST_URI"]) && $_SERVER["REQUEST_URI"] != ""){
$url.= $_SERVER["REQUEST_URI"];
}
else{
$url.= $_SERVER["PATH_INFO"];
}

Return $url;
}

function rate_limit($ip, $limit_hour = 20, $limit_10_min = 10){

// The number of login attempts for the last hour by this IP address

$count_hour = ORM::for_table("reg_login_attempt")
->
->where_raw("ts > SUBTIME(NOW(),"1:00")")
->count();

// The number of login attempts for the last 10 minutes by this IP address

$count_10_min = ORM::for_table("reg_login_attempt")
->where("ip", sprintf("%u", ip2long($ip)))
->where_raw("ts > SUBTIME(NOW(),"0:10")")
->count();

If($count_hour > $limit_hour || $count_10_min > $limit_10_min){
throw new Exception("Too many login attempts!");
}
}

function rate_limit_tick($ip, $email){

// Create a new record in the login attempt table

$login_attempt = ORM::for_table("reg_login_attempt")->create();

$login_attempt->email = $email;
$login_attempt->ip = sprintf("%u", ip2long($ip));

$login_attempt->save();
}

function redirect($url){
header("Location: $url");
exit;
}
Функции rate_limit и rate_limit_tick позволяют нам ограничивать число попыток авторизации на определенный промежуток времени. Попытки авторизации записываются в базу данных reg_login_attempt. Эти функции запускаются при проведении подтверждения формы авторизации, как можно видеть в следующем фрагменте кода.

Нижеприведенный код был взят из index.php, и он отвечает за подтверждение формы авторизации. Он возвращает JSON-ответ, который управляется кодом jQuery, который мы видели в assets/js/script.js.

index.php

If(!empty($_POST) && isset($_SERVER["HTTP_X_REQUESTED_WITH"])){

// Output a JSON header

Header("Content-type: application/json");

// Is the email address valid?

If(!isset($_POST["email"]) || !filter_var($_POST["email"], FILTER_VALIDATE_EMAIL)){
throw new Exception("Please enter a valid email.");
}

// This will throw an exception if the person is above
// the allowed login attempt limits (see functions.php for more):
rate_limit($_SERVER["REMOTE_ADDR"]);

// Record this login attempt
rate_limit_tick($_SERVER["REMOTE_ADDR"], $_POST["email"]);

// Send the message to the user

$message = "";
$email = $_POST["email"];
$subject = "Your Login Link";

If(!User::exists($email)){
$subject = "Thank You For Registering!";
$message = "Thank you for registering at our site!\n\n";
}

// Attempt to login or register the person
$user = User::loginOrRegister($_POST["email"]);

$message.= "You can login from this URL:\n";
$message.= get_page_url()."?tkn=".$user->generateToken()."\n\n";

$message.= "The link is going expire automatically after 10 minutes.";

$result = send_email($fromEmail, $_POST["email"], $subject, $message);

If(!$result){
throw new Exception("There was an error sending your email. Please try again.");
}

Die(json_encode(array(
"message" => "Thank you! We\"ve sent a link to your inbox. Check your spam folder as well."
)));
}
}
catch(Exception $e){

Die(json_encode(array(
"error"=>1,
"message" => $e->getMessage()
)));
}
При успешной авторизации или регистрации, вышеприведенный код отсылает email человеку с ссылкой для авторизации. Token (лексема) становится доступной в качестве $_GET-переменной "tkn" ввиду сгенерированного URL.

index.php

If(isset($_GET["tkn"])){

// Is this a valid login token?
$user = User::findByToken($_GET["tkn"]);

// Yes! Login the user and redirect to the protected page.

$user->login();
redirect("protected.php");
}

// Invalid token. Redirect back to the login form.
redirect("index.php");
}
Запуск $user->login() создаст необходимые переменные для сессии, что позволит пользователю оставаться авторизованным при последующих входах.

Выход из системы реализуется примерно таким же образом:

Index.php

If(isset($_GET["logout"])){

$user = new User();

If($user->loggedIn()){
$user->logout();
}

Redirect("index.php");
}
В конце кода мы снова перенаправляем пользователя на index.php, поэтому параметр?logout=1 в URL исключается.

Нашему файлу index.php также потребуется защита – мы не хотим, чтобы уже авторизованные пользователи видели форму. Для этого мы используем метод $user->loggedIn():

Index.php

$user = new User();

if($user->loggedIn()){
redirect("protected.php");
}
Наконец, давайте посмотрим, как можно защитить страницу вашего сайта, и сделать ее доступной только после авторизации:

protected.php

// To protect any php page on your site, include main.php
// and create a new User object. It"s that simple!

require_once "includes/main.php";

$user = new User();

if(!$user->loggedIn()){
redirect("index.php");
}
После этой проверки вы можете быть уверены в том, что пользователь успешно авторизовался. У вас также будет доступ к данным, которые хранятся в базе данных в качестве свойств объекта $user. Чтобы вывести email пользователя и их ранг, воспользуйтесь следующим кодом:

Echo "Your email: ".$user->email;
echo "Your rank: ".$user->rank();
Здесь rank() – это метод, так как колонка rank в базе данных обычно содержит числа (0 для обычных пользователей и 1 для администраторов), и нам нужно преобразовать это все в названия рангов, что реализуется при помощи данного метода. Чтобы преобразовать обычного пользователя в администратора, просто отредактируйте запись о пользователе в phpmyadmin (либо в любой другой программе по работе с базами данных). Будучи администратором, пользователь не будет наделен какими-то особыми возможностями. Вы сами в праве выбирать, каким правами наделять администраторов.

Готово!

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

Company Name
Search All «Альянс Пул» "SOLAR WEB MASTER COMPANY" zaj4enko.anatoly Гипермаркет ГУМ Золотарёва Светлана Владимировна "ABSOLUTE NATURE" "Бахарев и Партнеры" . 1000i1sumka 120na80 1С:БУХУЧЕТ И ТОРГОВЛЯ (БИТ) 32424 5 КармаNов A.W. AB-Design ac-taxi Ace Target adidas adwizer afterdealer akelon Alex ALFA PREMIUM GROUP Aller Petfood ALP Group Amrita Apple-concierge Argus bbraun bcom Catzwolf CheckMates city-staff Cвязной datagroup dvsecurity Englishdom FIGARO International Management Group FREEDOM G4S Glamurchik GlobalLogic Ukraine Globus Helga Pro IN-TIME INCOMERSUALAT LTDA InTime LLC IT Marketing Group J-0-B jobint jobtrade jtrade Kentor maxilab Meda Michael Page Monsoon и Accessorize. MS Westfalia mvideo NASHARABOTA NETWORK NETWORK New Agency group NSP OOO "ПЕРЕСВЕТ" OOO AAA AUTO OOO HOME OOO WA-4 people Perevodone personum Procter & Gamble Profi-Group Profi-Group RegionCentr RunetSoft sertg sferapf Skanska Skanska AB SONETT SQI management TGS TOYOTA tret TUT-TRUD usn vitmark Walkservice Walkservice WORK_HOT X5 Retail Group N.V. YILI JAHAN YILI JAHAN Zemfira «7ЦВЕТОВ-Декор «AMAKS Hotels & Resorts» «Antegra сonsulting» «Аpple Real Estate» «АРС-Моторс» «ГЛОБАЛ КОНСАЛТИНГ» «НАДО ЕХАТЬ» АА Автофорум АВГУСТ АвтоВэй Автодоктор XXI век Агентство АЙЛЕНД Агентство мерчандайзинга АгроВет Атлантик Адвокатское бюро «Юрискон.» Азурит АИС АйБиТех-Персонал АКТИВ-К АЛЬТЭРОС АЛЬФА СТРАХОВАНИЕ Альянс АМРИТА Андрей Апр-инфо Аптечная сеть Артеос Асгард+ Ассистанская компания Ассоциация Галерея Чижова Афина АЭРОТРЭВЭЛ Банк Русский Стандарт Бая Безпека ЛТД Бюро “Skrzyzowanie” В деталях вакансии ВАТКОМ Ваш консультант Велкам Принт Версус Сервис Владислав Гранд Круиз Диана Домофоны Cтолицы Е-Електроника Евросеть Заработок в интернете ЗемляСтройМонтаж ЗКА ЗНАК ММ ИКЦ "Здоровое питание" ИНКОМ интернет Интернет проект. Интернет проект. Интернет проект. Интернет-магазин EXIST ИП Мохова Исток Кадровый Центр Эксперт Кадровый Центр Эксперт Казгарантсервиз Кайт Школа в Одессе KiteINFO.com.ua Киммерия, ООО Колумбийская компания INCOMERSUALAT LTDA занимается организацией производства и сбыта продукции, такой как: дизель D2 , топливо для реактивных двигателей JP -54 , мазута. Комплекс «ФАРА» Корпорация ИНКОМ Корпорация "Артериум" Кудряшовский мясокомбинат Кусов и партнеры Линк Мегаполис Бизнес Консалт Мегаполисправа Мегатэкс Медиа-холдинг «Пронто-Москва» Меркурий Метлес МИР чудес Модный дом Модный Дом Ия Йоц МОСКОВСКИЙ КРЕДИТНЫЙ БАНК Наталья нет информации Ника ОАО «Банкомсвязь» ОАО ТД «КОПЕЙКА» Огркомитет Международного Фестиваля "Будущее России" ООО ООО "А3 ИНЖИНИРИНГ" ООО "АГЕНТ" ООО "АЙНЬЮС" ООО "Аркада" ООО "АСКА-Недвижимость" ООО "БОР-сервис" ООО "Красная королева" ООО "Невские мастера" ООО "Оптима" ООО "Оптима" ООО "Агрокосм" ООО «АКВАВИТАЛИТИ» ООО БиЛа ООО КБ "Банк Расчетов и Сбережений" ООО КВАРТАЛ-СК ООО Мишинострой ООО ПК САН ООО Твист ООО Триумф ООО"ГК" "Г и Р" ООО"Стимул" Охранная компания «ТопГард» ПАО «Альфа-Банк» Печатьoff подробно в описании ПРИВАТ Продвижение нового проекта Прямой работодатель Прямой работодатель работа Работа без вложений. Дополнительный доход к основной работе от 50 долларов США в день! Рекрутинговое агентство «Альтернатива» Рекрутинговая компания ПРОЭКСПЕРТ СберИнвестКонсалт Сеть ресторанов быстрого обслуживания «BurgerClub» Служба "Диалог" СМК СМК Средняя школа с.Сокольниково Стимул и Ко Строй-профи Тим и Тома ТМ "Хортиця" тов КС ТПП-ПАРТНЕР ТСК Гельстер Удаленная работа на ПК Укрветком УП ПРИНТ Фирма СИНБИАС Фитнес Клуб "Адреналин" Холдинг «МедиаМаркет» Частное лицо ЧОП "Мангуст-С" Южный двор Южный двор Юридическая фирма "Версайд" Юридическая Компания «ТРИУМФ»

Reg.ru: домены и хостинг

Крупнейший регистратор и хостинг-провайдер в России.

Более 2 миллионов доменных имен на обслуживании.

Продвижение, почта для домена, решения для бизнеса.

Более 700 тыс. клиентов по всему миру уже сделали свой выбор.

*Наведите курсор мыши для приостановки прокрутки.

Назад Вперед

Создание простой системы регистрации пользователей на PHP и MySQL

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

И даже после того как вы всё это напишите, пользователи будут регистрироваться неохотно, т.к. это требует определённых усилий с их стороны.

В этом уроке, мы создадим очень простую систему регистрации, которая не требует и не хранит паролей вообще! Результат будет легко изменить и добавить к уже существующему PHP-сайту. Хотите выяснить, как это работает? Читайте ниже.



Вот как наша супер простая система будет работать:

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

После этого создаётся некий случайный уникальный набор символов (токен), который отправляется на указанную пользователем почту в виде ссылки, которая будет актуальна в течение 10 минут;
- По ссылке пользователь переходит на наш сайт. Система определяет наличие токена и авторизует пользователя;

Преимущества такого подхода:

Не нужно хранить пароли и осуществлять валидацию полей;
- Нет необходимости в восстановлении пароля, секретных вопросов и т.д.;
- С момента как пользователь зарегистрировался/авторизовался вы можете всегда быть уверены, что этот пользователь будет в вашей зоне доступа (что email-адрес является истинным);
- Невероятно простой процесс регистрации;

Недостатки:

Безопасность аккаунта пользователя. Если кто-то имеет доступ к почте пользователя, он может авторизоваться.
- Email не защищён и может быть перехвачен. Имейте в виду, что этот вопрос актуален и в случае, когда пароль был забыт и его необходимо восстановить, или в любой системе авторизации, которая не использует HTTPS для передачи данных (логин/пароль);
- Пока вы настроите как нужно почтовый сервер, существует шанс, что сообщения со ссылками на авторизацию будут попадать в спам;

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

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

Как пользоваться этой системой

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

Вам нужно скачать исходники, приложенные к уроку
- В архиве найти файл tables.sql Импортируйте его в вашу базу данных используя опцию импорта в phpMyAdmin. Альтернативный способ: открыть этот файл через текстовый редактор, скопировать SQL запрос и выполнить его;
- Открыть includes/main.php и заполнить настройки связи с вашей базой данных (указать пользователя и пароль для связи с базой а также хост и имя базы). В этом же файле, вы также должны указать email, который будет использован в качестве оригинального адреса для сообщений отправляемых системой. Некоторые хосты блокируют исходящие мейлы пока в форме не будет указан настоящий email адрес, который был создан из панели управления хостом, так что укажите реальный адрес;
- Загрузите все файлы index.php , protected.php и папки assets и includes через FTP на ваш хост;
- Добавьте код ниже на каждую PHP-страницу, где нужно отобразить форму авторизации;

Require_once "includes/main.php"; $user = new User(); if(!$user->loggedIn()){ redirect("index.php"); }
- Готово!

Для тех же, кому интересно, как это всё работает - вперёд к чтению ниже!

Первый шаг - написание HTM- кода формы авторизации. Данный код располагается в файле index.php . Этот файл также содержит PHP-код, обрабатывающий данные формы и другие полезные функции системы авторизации. Узнать об этом больше можно в разделе ниже, посвящённом обзору PHP кода.

index.php

Tutorial: Super Simple Registration System With PHP & MySQL Login or Register

Enter your email address above and we will send
you a login link.

Login / Register

В головной секции (между тегами и ) я подключил основные стили (в этом уроке они не разбираются, поэтому вы можете посмотреть их сами. Папка assets/css/style.css). До закрывающего тега я подключил библиотеку jQuery и файл script.js, который мы напишем и разберём чуть ниже.


JavaScript

jQuery отслеживает состояние кнопки "Зарегистрироваться/авторизоваться" с помощью функции e.preventDefault() и отправляет AJAX-запросы. В зависимости от ответа сервера, выводит то или иное сообщение и определяет дальнейшие действия/

assets/js/script.js

$(function(){ var form = $("#login-register"); form.on("submit", function(e){ if(form.is(".loading, .loggedIn")){ return false; } var email = form.find("input").val(), messageHolder = form.find("span"); e.preventDefault(); $.post(this.action, {email: email}, function(m){ if(m.error){ form.addClass("error"); messageHolder.text(m.message); } else{ form.removeClass("error").addClass("loggedIn"); messageHolder.text(m.message); } }); }); $(document).ajaxStart(function(){ form.addClass("loading"); }); $(document).ajaxComplete(function(){ form.removeClass("loading"); }); });

был добавлен в форму для отображения текущего состояния AJAX-запроса (это стало возможным благодаря методам ajaxStart() ) и ajaxComplete() , которые вы сможете найти ближе к концу файла).

Этот класс показывает крутящийся анимированный gif-файл (как бы намекающий нам на то, что запрос обрабатывается), и также выступает как флаг, предотвращающий повторную отправку формы (когда кнопка зарегистрироваться была уже однажды нажата). Класс .loggedIn - это другой флаг, - устанавливается тогда, когда был отправлен email. Этот флаг моментально блокирует любые дальнейшие действия с формой.

Схема базы данных

Наша невероятно простая система регистрации использует 2 MySQL таблицы (SQL-код находится в файле tables.sql ). Первая хранит данные об аккаунтах пользователей. Вторая хранит информацию о количестве попыток входа.


Схема таблицы пользователей.

Система не использует паролей, что видно на схеме. На ней же можно увидеть колонку token с токенами, соседствующую с колонкой token_validity . Токен устанавливается как только пользователь подключается к системе, задаёт свой email для отправки сообщения (чуть подробнее об этом в следующем блоке). Колонка token_validity устанавливает время на 10 минут позже, после которого токен перестаёт быть актуальным.


Схема таблицы, считающая количество попыток авторизации.

В обоих таблицах IP-адрес хранится в обработанном виде, с помощью функции ip2long в поле типа integer.

Теперь мы можем написать немножко PHP-кода. Основной функционал системы возложен на класс User.class.php , который вы можете видеть ниже.

Данный класс активно использует idorm (docs), эти библиотеки являются минимально необходимыми инструментами, для работы с базами данных. Он обрабатывает доступ к базе данных, генерацию токенов и их валидацию. Он представляет собой простой интерфейс, позволяющий легко подключить систему регистрации к вашему сайту, если он использует PHP.

User.class.php

Class User{ // Частный ORM случай private $orm; /** * Найти пользователя по токену. Только валидные токены, приняты к рассмотрению. Токен генерируется только на 10 минут с того момента как был создан * @param string $token. Это искомый токен * @return User. Вернуть значение функции User */ public static function findByToken($token){ // найти токен в базе и убедиться, что установлен корректный временной штамп $result = ORM::for_table("reg_users") ->where("token", $token) ->where_raw("token_validity > NOW()") ->find_one(); if(!$result){ return false; } return new User($result); } /** * Авторизовать или зарегистрировать пользователя * @param string $email. Пользовательский email-адрес * @return User */ public static function loginOrRegister($email){ // Если такой пользователь уже существует, вернуть значение функции User от заданного email-адреса хранимого в базе if(User::exists($email)){ return new User($email); } // В противном случае создать нового пользователя в базе и вернуть значение функции User::create от указанного email return User::create($email); } /** * Создать нового пользователя и сохранить в базу * @param string $email. Пользовательский email-адрес * @return User */ private static function create($email){ // Записать нового пользователя и вернуть результат функции User от этих значений $result = ORM::for_table("reg_users")->create(); $result->email = $email; $result->save(); return new User($result); } /** * Проверить, существует ли такой пользователь в базе и вернуть булево значение переменной * @param string $email. Пользовательский email-адрес * @return boolean */ public static function exists($email){ // Существует ли пользователь в базе? $result = ORM::for_table("reg_users") ->where("email", $email) ->count(); return $result == 1; } /** * Создать новый пользовательский объект * @param экземпляр $param ORM , id, email or 0 * @return User */ public function __construct($param = null){ if($param instanceof ORM){ // ORM проверка пройдена $this->orm = $param; } else if(is_string($param)){ // Проверка на email пройдена $this->orm = ORM::for_table("reg_users") ->where("email", $param) ->find_one(); } else{ $id = 0; if(is_numeric($param)){ // идентификатору пользователя передаётся значение переменной $param $id = $param; } else if(isset($_SESSION["loginid"])){ // В противном случае смотри сессию $id = $_SESSION["loginid"]; } $this->orm = ORM::for_table("reg_users") ->where("id", $id) ->find_one(); } } /** * Сгенерировать новый SHA1 токен авторизации, записывает в базу и возвращает его значение * @return string */ public function generateToken(){ // Сгенерировать токен для авторизованного пользователя и сохранить его в базу $token = sha1($this->email.time().rand(0, 1000000)); // Сохранить токен в базе // И пометить его, что он актуален только в течение 10 следующих минут $this->orm->set("token", $token); $this->orm->set_expr("token_validity", "ADDTIME(NOW(),"0:10")"); $this->orm->save(); return $token; } /** * Авторизовать пользователя * @return void */ public function login(){ // Отметить пользователя, как авторизованного $_SESSION["loginid"] = $this->orm->id; // Обновить значение поля базы last_login $this->orm->set_expr("last_login", "NOW()"); $this->orm->save(); } /** * Уничтожить сессию и разлогинить пользователя * @return void */ public function logout(){ $_SESSION = array(); unset($_SESSION); } /** * Проверка, заходил ли пользователь * @return boolean */ public function loggedIn(){ return isset($this->orm->id) && $_SESSION["loginid"] == $this->orm->id; } /** * Проверка является ли пользователь администратором * @return boolean */ public function isAdmin(){ return $this->rank() == "administrator"; } /** * Найти тип пользователя, может быть либо administrator либо regular * @return string */ public function rank(){ if($this->orm->rank == 1){ return "administrator"; } return "regular"; } /** * Метод позволяющий получить приватную информацию пользователя в *качестве свойств объекта User * @param string $key Имя свойства, получающего доступ * @return mixed */ public function __get($key){ if(isset($this->orm->$key)){ return $this->orm->$key; } return null; } }

Токены генерируются с помощью SHA1 алгоритма и сохраняются в базе данных. Я использую функции времени MySQL, дабы задать 10-минутное ограничение актуальности токена.

Когда токен проходит процедуру валидации, мы напрямую говорим обработчику, что мы рассматриваем только токены, у которых ещё не истёк срок годности, хранимый в столбце token_validity.

Обратите внимание, что я использую волшебный метод __get библиотеки docs в конце файла, чтобы перехватить доступ к свойствам объекта User.

Благодаря этому становится возможным получить доступ к информации, хранящейся в базе, благодаря свойствам $user->email, $user->token и др. В следующем фрагменте кода рассмотрим для примера, как использовать эти классы.


Защищённая страница

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

functions.php

Function send_email($from, $to, $subject, $message){ // Хелпер, отправляющий email $headers = "MIME-Version: 1.0" . "\r\n"; $headers .= "Content-type: text/plain; charset=utf-8" . "\r\n"; $headers .= "From: ".$from . "\r\n"; return mail($to, $subject, $message, $headers); } function get_page_url(){ // Определить URL PHP-файла $url = "http".(empty($_SERVER["HTTPS"])?"":"s")."://".$_SERVER["SERVER_NAME"]; if(isset($_SERVER["REQUEST_URI"]) && $_SERVER["REQUEST_URI"] != ""){ $url.= $_SERVER["REQUEST_URI"]; } else{ $url.= $_SERVER["PATH_INFO"]; } return $url; } function rate_limit($ip, $limit_hour = 20, $limit_10_min = 10){ // Количество попыток входа за последний час по этому IP-адресу $count_hour = ORM::for_table("reg_login_attempt") ->where("ip", sprintf("%u", ip2long($ip))) ->where_raw("ts > SUBTIME(NOW(),"1:00")") ->count(); // Количество попыток входа за последние 10 минут по этому IP-адресу $count_10_min = ORM::for_table("reg_login_attempt") ->where("ip", sprintf("%u", ip2long($ip))) ->where_raw("ts > SUBTIME(NOW(),"0:10")") ->count(); if($count_hour > $limit_hour || $count_10_min > $limit_10_min){ throw new Exception("Too many login attempts!"); } } function rate_limit_tick($ip, $email){ // Создать новую запись в таблице, считающей количество попыток входа $login_attempt = ORM::for_table("reg_login_attempt")->create(); $login_attempt->email = $email; $login_attempt->ip = sprintf("%u", ip2long($ip)); $login_attempt->save(); } function redirect($url){ header("Location: $url"); exit; }

Функции rate_limit и rate_limit_tick следят за количеством попыток авторизации за истёкший период времени с момента первой попытки. Попытка входа записывается в базе в столбец reg_login_attempt. Эти функции вызываются когда происходит обработка и отправка данных формы как вы можете видеть из следующего фрагмента кода.

Код ниже взят из файла index.php и он обрабатывает отправку формы. Он возвращает JSON-ответ, который, в свою очередь, обрабатывается jQuery в файле assets/js/script.js , который мы уже разбирали ранее.

index.php

Try{ if(!empty($_POST) && isset($_SERVER["HTTP_X_REQUESTED_WITH"])){ // Output a JSON header header("Content-type: application/json"); // Является ли этот email-адрес валидным if(!isset($_POST["email"]) || !filter_var($_POST["email"], FILTER_VALIDATE_EMAIL)){ throw new Exception("Please enter a valid email."); } // Проверка. Позволено ли пользователю авторизоваться, не превысил ли он количество допустимых подключений? (файл functions.php для большей информации) rate_limit($_SERVER["REMOTE_ADDR"]); // Записать эту попытку авторизации rate_limit_tick($_SERVER["REMOTE_ADDR"], $_POST["email"]); // Отправить письмо пользователю $message = ""; $email = $_POST["email"]; $subject = "Your Login Link"; if(!User::exists($email)){ $subject = "Thank You For Registering!"; $message = "Thank you for registering at our site!\n\n"; } // Попытка авторизовать или зарегистрировать пользователя $user = User::loginOrRegister($_POST["email"]); $message.= "You can login from this URL:\n"; $message.= get_page_url()."?tkn=".$user->generateToken()."\n\n"; $message.= "The link is going expire automatically after 10 minutes."; $result = send_email($fromEmail, $_POST["email"], $subject, $message); if(!$result){ throw new Exception("There was an error sending your email. Please try again."); } die(json_encode(array("message" => "Thank you! We\"ve sent a link to your inbox. Check your spam folder as well."))); } } catch(Exception $e){ die(json_encode(array("error"=>1, "message" => $e->getMessage()))); }

После успешной авторизации/регистрации код выше отправит пользователю ссылку для авторизации. Токен становится доступным, т.к. он передаётся в качестве переменной в генерируемой ссылке методом $_GET с маркером tkn

index.php

If(isset($_GET["tkn"])){ // Является ли этот токен валидным для авторизации? $user = User::findByToken($_GET["tkn"]); if($user){ // Да, является. Осуществить редирект на защищённую страницу $user->login(); redirect("protected.php"); } // Нет, токен не валидный. Осуществить редирект, на страницу с формой авторизации/регистрации redirect("index.php"); }

$user->login()

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

Похожим образом устроена и обработка функции на выход из системы.

index.php

If(isset($_GET["logout"])){ $user = new User(); if($user->loggedIn()){ $user->logout(); } redirect("index.php"); }

В конце кода, я снова поставил редирект на index.php, таким образом параметр ?logout=1 передаваемый посредством URL не требуется.

Наш файл index.php требует доп. защиты - мы не хотим, чтобы люди, которые когда-либо однажды авторизовались в системе опять видели форму регистрации. Для этих целей, мы используем метод $user->loggedIn() .

index.php

$user = new User(); if($user->loggedIn()){ redirect("protected.php"); }

Наконец-то, вот кусок кода, позволяющий защитить страницы вашего сайта и сделать её доступной только после авторизации.

protected.php

// Чтобы защитить каждую страницу на вашем сайте подключите к ней файл // main.php и создайте новый объект User. Вот как это просто! require_once "includes/main.php"; $user = new User(); if(!$user->loggedIn()){ redirect("index.php"); }

После этой проверки можете быть уверенными, что пользователь был успешно авторизован. Вы также можете получить доступ к хранимой информации в базе с помощью свойств объекта $user . Для вывода email-а пользователя и его статуса используйте этот код:

Echo "Your email: ".$user->email; echo "Your rank: ".$user->rank();

Метод rank() используется здесь потому что в базе обычно хранятся номера (0 для обычного пользователя, 1 для администратора) и нам нужно преобразовать эти данные в статусы, к которым они относятся, в чём нам и помогает этот метод.

Чтобы сделать из обычного пользователя администратора, просто отредактируйте пользовательскую запись через phpMyAdmin (или любую другую программу, позволяющую управлять базами данных). Статус администратора не даёт каких-либо привилегий, в данном примере на странице будет выведено, что вы администратор - и всё.

А вот что с этим делать - остаётся уже на ваше усмотрение, вы можете сами написать и составить код, задающий определённые привилегии и возможности для администраторов.

Мы закончили!

С этой невероятно супер квази простой формой мы закончили! Вы можете использовать её в ваших PHP-сайтах, это достаточно просто. Также вы можете модифицировать её под себя и сделать её такой, как вы хотите.

Материал подготовил Денис Малышок специально для сайта сайт

P.S. Хотите двигаться дальше в освоении PHP и ООП? Обратите внимание на премиум-уроки по различным аспектам сайтостроения, включая программирование на PHP, а также на бесплатный курс по созданию своей CMS-системы на PHP с нуля с использованием ООП:

Понравился материал и хотите отблагодарить?
Просто поделитесь с друзьями и коллегами!


Сегoдня мы рассмотрим эксплуатацию критической 1day-уязвимости в популярной CMS Joomla, которая прогремела на просторах интернета в конце октября. Речь пойдет об уязвимостях с номерами CVE-2016-8869 , CVE-2016-8870 и CVE-2016-9081 . Все три происходят из одного кусочка кода, который пять долгих лет томился в недрах фреймворка в ожидании своего часа, чтобы затем вырваться на свободу и принести с собой хаос, взломанные сайты и слезы ни в чем не повинных пользователей этой Joomla. Лишь самые доблестные и смелые разработчики, чьи глаза красны от света мониторов, а клавиатуры завалены хлебными крошками, смогли бросить вызов разбушевавшейся нечисти и возложить ее голову на алтарь фиксов.

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

6 октября 2016 года Дэмис Пальма (Demis Palma) создал топик на Stack Exchange , в котором поинтересовался: а почему, собственно, в Joomla версии 3.6 существуют два метода регистрации пользователей с одинаковым названием register() ? Первый находится в контроллере UsersControllerRegistration , а второй - в UsersControllerUser . Дэмис хотел узнать, используется ли где-то метод UsersControllerUser::register() , или это лишь эволюционный анахронизм, оставшийся от старой логики. Его беспокоил тот факт, что, даже если этот метод не используется никаким представлением, он может быть вызван при помощи сформированного запроса. На что получил ответ от девелопера под ником itoctopus, подтвердившего: проблема действительно существует. И направил отчет разработчикам Joomla.

Далее события развивались самым стремительным образом. 18 октября разработчики Joomla принимают репорт Дэмиса, который к тому времени набросал PoC, позволяющий регистрировать пользователя. Он опубликовал заметку на своем сайте , где в общих чертах рассказал о найденной проблеме и мыслях по этому поводу. В этот же день выходит новая версия Joomla 3.6.3, которая все еще содержит уязвимый код.

После этого Давиде Тампеллини (Davide Tampellini) раскручивает баг до состояния регистрации не простого пользователя, а администратора. И уже 21 октября команде безопасности Joomla прилетает новый кейс. В нем речь уже идет о повышении привилегий . В этот же день на сайте Joomla появляется анонс о том, что во вторник, 25 октября, будет выпущена очередная версия с порядковым номером 3.6.3, которая исправляет критическую уязвимость в ядре системы.

25 октября Joomla Security Strike Team находит последнюю проблему, которую создает обнаруженный Дэмисом кусок кода. Затем в главную ветку официального репозитория Joomla пушится коммит от 21 октября с неприметным названием Prepare 3.6.4 Stable Release , который фиксит злосчастный баг.

После этого камин-аута к междусобойчику разработчиков подключаются многочисленные заинтересованные личности - начинают раскручивать уязвимость и готовить сплоиты.

27 октября исследователь Гарри Робертс (Harry Roberts) выкладывает в репозиторий Xiphos Research готовый эксплоит , который может загружать PHP-файл на сервер с уязвимой CMS.

Детали

Что ж, с предысторией покончено, переходим к самому интересному - разбору уязвимости. В качестве подопытной версии я установил Joomla 3.6.3, поэтому все номера строк будут актуальны именно для этой версии. А все пути до файлов, которые ты увидишь далее, будут указываться относительно корня установленной CMS.

Благодаря находке Дэмиса Пальмы мы знаем, что есть два метода, которые выполняют регистрацию пользователя в системе. Первый используется CMS и находится в файле /components/com_users/controllers/registration.php:108 . Второй (тот, что нам и нужно будет вызвать), обитает в /components/com_users/controllers/user.php:293 . Посмотрим на него поближе.

286: /** 287: * Method to register a user. 288: * 289: * @return boolean 290: * 291: * @since 1.6 292: */ 293: public function register() 294: { 295: JSession::checkToken("post") or jexit(JText::_("JINVALID_TOKEN")); ... 300: // Get the form data. 301: $data = $this->input->post->get("user", array(), "array"); ... 315: $return = $model->validate($form, $data); 316: 317: // Check for errors. 318: if ($return === false) 319: { ... 345: // Finish the registration. 346: $return = $model->register($data);

Здесь я оставил только интересные строки. Полную версию уязвимого метода можно посмотреть в репозитории Joomla.

Разберемся, что происходит при обычной регистрации пользователя: какие данные отправляются и как они обрабатываются. Если регистрация пользователей включена в настройках, то форму можно найти по адресу http://joomla.local/index.php/component/users/?view=registration .


Легитимный запрос на регистрацию пользователя выглядит как на следующем скриншоте.


За работу с пользователями отвечает компонент com_users . Обрати внимание на параметр task в запросе. Он имеет формат $controller.$method . Посмотрим на структуру файлов.

Имена скриптов в папке controllers соответствуют названиям вызываемых контроллеров. Так как в нашем запросе сейчас $controller = "registration" , то вызовется файл registration.php и его метод register() .

Внимание, вопрос: как передать обработку регистрации в уязвимое место в коде? Ты наверняка уже догадался. Имена уязвимого и настоящего методов совпадают (register), поэтому нам достаточно поменять название вызываемого контроллера. А где у нас находится уязвимый контроллер? Правильно, в файле user.php . Получается $controller = "user" . Собираем все вместе и получаем task = user.register . Теперь запрос на регистрацию обрабатывается нужным нам методом.


Второе, что нам нужно сделать, - это отправить данные в правильном формате. Тут все просто. Легитимный register() ждет от нас массив под названием jform , в котором мы передаем данные для регистрации - имя, логин, пароль, почту (см. скриншот с запросом).

  • /components/com_users/controllers/registration.php: 124: // Get the user data. 125: $requestData = $this->input->post->get("jform", array(), "array");

Наш подопечный получает эти данные из массива с именем user .

  • /components/com_users/controllers/user.php: 301: // Get the form data. 302: $data = $this->input->post->get("user", array(), "array");

Поэтому меняем в запросе имена всех параметров с jfrom на user .

Третий наш шаг - это нахождение валидного токена CSRF, так как без него никакой регистрации не будет.

  • /components/com_users/controllers/user.php: 296: JSession::checkToken("post") or jexit(JText::_("JINVALID_TOKEN"));

Он выглядит как хеш MD5, а взять его можно, например, из формы авторизации на сайте /index.php/component/users/?view=login .


Теперь можно создавать пользователей через нужный метод. Если все получилось, то поздравляю - ты только что проэксплуатировал уязвимость CVE-2016-8870 «отсутствующая проверка разрешений на регистрацию новых пользователей».

Вот как она выглядит в «рабочем» методе register() из контроллера UsersControllerRegistration:

  • /components/com_users/controllers/registration.php: 113: // If registration is disabled - Redirect to login page. 114: if (JComponentHelper::getParams("com_users")->get("allowUserRegistration") == 0) 115: { 116: $this->setRedirect(JRoute::_("index.php?option=com_users&view=login", false)); 117: 118: return false; 119: }

А так в уязвимом:

  • /components/com_users/controllers/user.php:

Ага, никак.

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

Продолжение доступно только подписчикам Вариант 1. Оформи подписку на «Хакер», чтобы читать все материалы на сайте

Подписка позволит тебе в течение указанного срока читать ВСЕ платные материалы сайта. Мы принимаем оплату банковскими картами, электронными деньгами и переводами со счетов мобильных операторов.

Понравилась статья? Поделиться с друзьями: