Применение системы регистрации
В том случае, если вы просто хотите использовать нашу систему регистрации на вашем сайте, и не хотите изучать руководство, то вам нужно сделать следующее:
* Сначала нужно скачать вышеприведенный zip-файл;
* В zip-файле найти файл tables.sql. Импортировать его в базу данных, используя опцию импорта в phpmyadmin. С другой стороны, вы можете открыть файл в текстовом редакторе, скопировать SQL и запустить его;
* Откройте includes/main.php, и внесите туда данные для соединения с базой данных, а также комбинацию логин/пароль. В том же файле вам также нужно добавить email, который будет использовать в качестве отправителя сообщений. Некоторые веб-хостинги блокируют исходящие сообщения до тех пор, пока вы не укажите настоящий email-адрес, который можно создать при помощи панели управления этого же хостера;
* Загрузите все необходимые файлы посредством FTP или других способов;
* Добавьте предпоследний фрагмент кода (см. ниже) на каждую PHP-страницу, которые должны стать доступными только после авторизации;
* Веселитесь!
Те, кто решил остаться с нами и дочитать статью до конца, предлагаем дальнейшую информацию:
HMTL-код
Первый этап заключается в создании HTML-кода для формы авторизации. Следующий HTML-код находится в index.php. Этот файл также хранит в себе PHP-код, который отвечает за подтверждение формы и другие полезные функции нашей системы авторизации. Вы узнаете об этом подробнее в разделе с PHP-кодом.
Index.php
Код
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Tutorial: Super Simple Registration System With PHP & MySQL</title>
<!-- The main CSS file -->
<link href="assets/css/style.css" rel="stylesheet" />
<!--[if lt IE 9]>
<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
</head>
<body>
<form id="login-register" method="post" action="index.php">
<h1>Login or Register</h1>
<input type="text" placeholder="your@email.com" name="email" autofocus />
<p>Enter your email address above and we will send <br />you a login link.</p>
<button type="submit">Login / Register</button>
<span></span>
</form>
<!-- javascript Includes -->
<script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script src="assets/js/script.js"></script>
</body>
</html>
В головной раздел мы включили основные таблицы стилей (они не представлены в руководстве, поэтому вам нужно будет открыть файл в редакторе, чтобы ознакомиться с ними). Перед закрывающих тегом body мы включили jQuery-библиотеку и файл script.js, разработкой которого мы займемся в следующем разделе статьи.
Теперь давайте займемся jQuery!
javascript-код
jQuery ожидает событие формы submit, запускает 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');
});
});
CSS-класс .loading добавляется к форме посредством Ajax-запроса в самом начале (это возможно благодаря методам ajaxStart() и ajaxComplete(), которые вы можете видеть практически в конце файла). Этот CSS-класс позволяет отобразить вращающийся индикатор, и вдобавок выступает в роли маркера, предотвращающего двойного подтверждения формы. Класс .loggedIn – это еще один маркер, который устанавливается как только email-сообщение будет отправлено. Он сразу же блокирует любые дальнейшие подтверждения.
Структура базы данных
Нашей простой системе регистрации понадобится 2 MySQL-таблицы (SQL-код вы можете видеть в файле tables.sql в zip-архиве). Первая таблица содержит пользовательские аккаунты, а вторая будет хранить данные о попытках авторизации.
Система не использует паролей, что приводит к отсутствию поля password в структуре. Здесь есть колонка token вместе с колонкой token_validity. Token (лексема) устанавливается, когда пользователь авторизуется в систему, и высылается на указанный пользователем email (подробнее об этом в следующем разделе). Затем token_validity устанавливается на 10 минут, после чего token станет недействительным.
Каждый раз, когда кто-нибудь пытается авторизоваться, во вторую таблицу вписывается новая запись. Как видно в нашем PHP-коде, благодаря этому у нас есть возможность реализовать ограничение количества попыток по IP-адресу. Мы устанавливаем ограничение в 10 попыток за 10 минут, и ограничение в 20 попыток в течение часа. Большее количество попыток приведет к блокировке IP-адреса.
В обеих таблицах IP-адрес хранится в виде целого числа с использованием PHP-функции
ip2long.
PHP
Теперь мы готовы к тому, чтобы заняться кодом PHP. Основной функционал системы регистрации предоставляется классом User, который вы можете видеть ниже. Класс использует
Idorm (
документация), представляющую собой минималистскую библиотеку для работы с базами данных. Класс User отвечает за доступ к базам данных, генерирование token-ов для логина и их валидации. Он представляет нам простой интерфейс, который можно без труда включить в систему регистрации на ваших сайтах, основанных на PHP.
User.class.php
Код
class User{
// 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
$result = ORM::for_table('reg_users')
->where('token', $token)
->where_raw('token_validity > NOW()')
->find_one();
if(!$result){
return false;
}
return new User($result);
}
/**
* Either login or register a user.
* @param string $email The user's email address
* @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->orm = ORM::for_table('reg_users')
->where('email', $param)
->find_one();
}
else{
$id = 0;
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-ы генерируются при помощи алгоритма
SHA1, и сохраняются в базу данных. Мы используем
функции даты и времени из 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('ip', sprintf("%u", ip2long($ip)))
->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
Код
try{
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']);
if($user){
// 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-сайте, либо модернизировать ее, придерживаясь собственных требований.