Авторизация на сайт через Телеграм без использования официального виджета

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

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

Сегодня рассмотрим простой алгоритм для регистрации и авторизации пользователей с помощью Телеграм. Взаимодействие сайта и Телеграм будет происходить через бот.

Весь процесс очень прост. Создадим ссылку по нажатию на которую запустятся два действия:

  • в бот мы передадим значение в параметре start, которое обработаем и запишем в базу вместе с данными пользователя
  • запустим JS скрипт, который будет с интервалом в 2 секунды делать AJAX запрос на сервер для проверки появления в базе записи переданного в бот значения, и в случае обнаружения произведет авторизацию пользователя

Для начала создадим таблицу users в базе MySQL (используйте кодировку utf8mb4_unicode_ci)

CREATE TABLE IF NOT EXISTS `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `telegram_id` bigint(20) DEFAULT NULL,
  `first_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `last_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `username` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `auth_key` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

index.php 

Этот файл будет выводить ссылку на авторизацию через бот в случае, если пользователь не авторизован. JS скрипт вешает на ссылку прослушку на событие click с функцией, которая через 2-х секундный интервал делает запрос на сервер. Когда ответ с сервера возвращается положительный, скрипт перезагружает страницу.

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

Не забудьте указать ___USERNAME__ВАШЕГО__БОТА___ 

<?php
// проверяем на действие - выхода
if($_GET['act'] == "logout") {
    // удаляем куку
    setcookie("token", "", time() - 60);
    // перезагружаем страницу
    header("Location: index.php");
} else {
    // подключаем соединение с БД
    require_once("Db.php");
    // создаем объект соединения
    $db = new Db();
    // проверяем на авторизацию
    if($_COOKIE['token']) {
        // проверяем на корректность
        if(!preg_match('~^[a-f0-9]{32}+$~', $_COOKIE['token'])) {
            // выходим если проверка не прошла
            exit();
        }
        // делаем запрос в БД
        $order = $db->connect()->prepare("SELECT * FROM users WHERE auth_key = :key LIMIT 1");
        $order->execute(['key' => $_COOKIE['token']]);
        // если запись есть то работаем
        if ($order->rowCount() > 0) {
            $orderRaw = $order->fetch();
            // Выводим имя
            echo trim($orderRaw['first_name'] .' '.$orderRaw['last_name'])." | ";
            // ссылка на выход
            echo "<a href='?act=logout'>Выход</a>";
        } else {
            // удаляем кукку
            header("Location: index.php?act=logout");
        }
    } else {
        // генерируем ключ
        $key = md5(rand(0, 1000));

        ///////////////////////////////////////////////
        //// username Bot
        ///////////////////////////////////////////////
        $bot = "___USERNAME__ВАШЕГО__БОТА___";

        // создаем ссылки на авторизацию
        echo "<a href='tg://resolve?domain=".$bot."&start=".$key."' data-key='".$key."' class='link_auth'>
Авторизоваться через Телеграм</a>";
        ?>
	<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
	<script>
       	    jQuery(function ($) {
	        $("body").on("click", ".link_auth", function () {
	            var auth_key = $(this).data("key");
	            var timerId = setInterval(function () {
	                $.ajax({
	                    type: "GET",
	                    dataType: "json",
	                    url: "checkKey.php",
	                    data: {
	                        key: auth_key
	                    },
	                    success: function (data) {
	                        if (data.result == "success") {
	                            clearInterval(timerId);
	                            setTimeout(function () {
	                                location.reload();
	                            }, 1000);
	                        }
	                    }
	                });
	            }, 2000);
	        });
            });
	</script>
        <?
    }
}
?>

webHook.php 

Это вебхук для бота, в который Telegram Bot API будет направлять объекты с данными запроса от пользователя. Мы подключаем класс Auth, создаем его объект и запускаем метод инициализации. Как установить вебхук можете почитать в статье "Регистрируем бот у @BotFather, устанавливаем WebHook"

<?php
// определим кодировку UTF-8
header("HTTP/1.1 200 OK");
header('Content-type: text/html; charset=utf-8');
// подключаем класс авторизации
require_once("Auth.php");
// создаем объект авторизации
$auth = new Auth();
// запускаем
$auth->init();
?>

Auth.php

После инициализации объекта, передаем данные от Bot API в роутер, в котором определяем, что делать с данными. Нас интересует объект message с переданным значением параметра start в виде ключа из 32 символов. Его мы передаем в метод авторизации. По telegram_id узнаем, есть ли такой пользователь у нас в базе, если есть, то обновляем ключ, если нет то добавляем пользователя в базу, при этом записываем все нужные нам данные и ключ для авторизации.

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

Не забудьте указать ___TOKEN__ВАШЕГО__БОТА___

<?php
// Подключаем класс для работы с БД
require_once("Db.php");

/**
 * Class Auth
 */
class Auth extends Db
{
    // токен API BOT
    private $token = "___TOKEN__ВАШЕГО__БОТА___";

    /** Инициализируем работу класса
     * @return bool
     */
    public function init()
    {
        // получаем данные от АПИ и преобразуем их в ассоциативный массив
        $rawData = json_decode(file_get_contents('php://input'), true);
        // направляем данные из бота в метод
        // для определения дальнейшего выбора действий
        $this->router($rawData);
        // в любом случае вернем true для бот апи
        return true;
    }

    /** Роутер
     * @param $data array
     * @return bool
     */
    private function router($data)
    {
        // проверяем на объект Message
        if (array_key_exists("message", $data)) {
            // получаем чат
            $chat_id = $data['message']['chat']['id'];
            // проверяем на наличие объекта Text
            if (array_key_exists("text", $data['message'])) {
                // Получаем значение отправленных данных
                $text = $data['message']['text'];
                // Если это просто старт бота
                if ($text == "/start") {
                    // отправляем сообщение
                    $this->botApiQuery("sendMessage", [
                        'chat_id' => $chat_id,
                        'text' => 'Бот авторизации'
                    ]);
                    // Если это старт бота с передаваемым значением параметра start    
                } elseif (preg_match('~^(\/start)+ ([a-f0-9]{32}+)$~', $text, $matches)) {
                    // передаем в метод авторизации
                    $this->setAuth($data['message'], $matches[2]);
                    // если это какой-то другой текст   
                } else {
                    // просто пишем чего-нибудь в чат
                    $this->botApiQuery("sendMessage", [
                        'chat_id' => $chat_id,
                        'text' => 'Бот авторизации'
                    ]);
                }
                // если это что-то другое - картинка или еще что-то    
            } else {
                // просто пишем чего-нибудь в чат
                $this->botApiQuery("sendMessage", [
                    'chat_id' => $chat_id,
                    'text' => 'Бот авторизации'
                ]);
            }
        }
        // другие объекты не рассматриваем 
        return true;
    }

    /** Авторизация
     * @param $data
     * @param $key
     */
    private function setAuth($data, $key)
    {
        // делаем запрос в БД
        $order = $this->connect()->prepare("SELECT * FROM users WHERE telegram_id = :id LIMIT 1");
        $order->execute(['id' => $data['chat']['id']]);
        // если запись есть то обновляем
        if ($order->rowCount() > 0) {
            $orderRaw = $order->fetch();
            // запрос на обновление
            $update = $this->connect()->prepare("UPDATE users SET auth_key = :new_key WHERE id = :id");
            // если обновили
            if ($update->execute(['id' => $orderRaw['id'], 'new_key' => $key])) {
                $text = "Вы успешно авторизовались. Возвращайтесь на сайт.";
            } else {
                $text = "При авторизации произошла ошибка";
            }
        } else {
            // если записи нет то добавляем
            $insert = $this->connect()->prepare("INSERT INTO users SET
	            telegram_id = :telegram_id,
	            first_name = :first_name, 
	            last_name = :last_name, 
	            username = :username, 
	            auth_key = :auth_key");
            // готовим данные
            $array = [
                'telegram_id' => $data['chat']['id'],
                'first_name' => $data['chat']['first_name'],
                'last_name' => $data['chat']['last_name'],
                'username' => $data['chat']['username'],
                'auth_key' => $key,
            ];
            // если удалось добавить товар
            if ($insert->execute($array)) {
                $text = "Вы успешно авторизовались. Возвращайтесь на сайт.";
            } else {
                $text = "При авторизации произошла ошибка";
            }
        }
        // отправляем сообщение
        $this->botApiQuery("sendMessage", [
            'chat_id' => $data['chat']['id'],
            'text' => $text
        ]);
    }

    /** Работаем с API BOT
     * @param $method
     * @param array $fields
     * @return mixed
     */
    private function botApiQuery($method, $fields = array())
    {
        $ch = curl_init('https://api.telegram.org/bot' . $this->token . '/' . $method);
        curl_setopt_array($ch, array(
            CURLOPT_POST => count($fields),
            CURLOPT_POSTFIELDS => http_build_query($fields),
            CURLOPT_SSL_VERIFYPEER => 0,
            CURLOPT_RETURNTRANSFER => 1,
            CURLOPT_TIMEOUT => 10
        ));
        $r = json_decode(curl_exec($ch), true);
        curl_close($ch);
        return $r;
    }
}
?>

Db.php

Класс для получения соединения с базой данных. Укажите данные ваших настроек базы данных.

<?php
/**
 * Class Db
 */
class Db {
    // для соединения с БД 
    private $host = 'localhost';
    private $db = '';
    private $user = '';
    private $pass = '';
    private $charset = 'utf8mb4';
    private $pdo = null;

    /** Получаем соединение
     * @return bool
     */
    public function connect()
    {
        if(is_null($this->pdo)) {
            $this->setPdo();
        }
        return $this->pdo;
    }

    /**
     *  Создаем соединение с БД
     */
    private function setPdo()
    {
        // задаем тип БД, хост, имя базы данных и чарсет
        $dsn = "mysql:host=".$this->host.";dbname=".$this->db.";charset=".$this->charset;
        // дополнительные опции
        $opt = [
            // способ обработки ошибок - режим исключений
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            // тип получаемого результата по-умолчанию - ассоциативный массив
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            // отключаем эмуляцию подготовленных запросов
            PDO::ATTR_EMULATE_PREPARES => false,
            // определяем кодировку запросов
            PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci"
        ];
        // // записываем объект PDO в свойство $this->pdo
        $this->pdo = new PDO($dsn, $this->user, $this->pass, $opt);
    }
}
?>

checkKey.php

Этот файл принимает AJAX запрос со страницы index.php когда пользователь нажимает ссылку для авторизации. Он по ключу проверяет наличии записи в базе, и в случае обнаружения обновляет ключ, записывает куку и возвращает положительный результат.

<?php
// подключаем соединение с БД
require_once("Db.php");
// создаем соединение с БД
$db = new Db();
// получаем ключ
$key = $_GET['key'];
// проверяем на корректность ключа
if(!preg_match('~^[a-f0-9]{32}+$~', $key)) {
    exit(json_encode(['result'=>'error']));
}
// создаем массив для возврата результата
$data = [];
// обращаемся в БД - проверяем наличие записи с переданным ключом
$order = $db->connect()->prepare("SELECT * FROM users WHERE auth_key = :key LIMIT 1");
$order->execute(['key' => $key]);
// если запись есть то работаем
if ($order->rowCount() > 0) {
    $orderRaw = $order->fetch();
    // создаем новый ключ
    $new_key =  md5(rand(0, 1000));;
    // записываем новый ключ в БД
    $update = $db->connect()->prepare("UPDATE users SET auth_key = :new_key WHERE id = :id");
    // если обновили то авторизуем
    if ($update->execute(['id' => $orderRaw['id'], 'new_key' => $new_key])) {
        // ставим куку
        setcookie('token', $new_key);
        // возвращаем результат
        $data['result'] = "success";
    } else {
        $data['result'] = "error";
    }
} else {
    $data['result'] = "error";
}
// возвращаем результат
echo json_encode($data);
?>

Заключение

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


Комментарии

Авторизуйтесь через Telegram, чтобы оставить комментарий.
Откройте бот @SiteAuthBot, нажмите кнопку Старт/Start. Следуйте инструкциям бота.


  • Спасибо. Очень интересно
    ©Мастер© © КЭП © Программист 23.09.2018 в 06:01
    • Солидарен)
      TToJIoTeH4uK™ 13.12.2018 в 13:15
  • Да, я так и знал что боту придётся самому писать.
    Но это вроде не сложно...
    bdika 01.02.2020 в 16:29
  • Блин, как круто и бысто!
    Dapster 31.08.2020 в 17:11