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

Рассмотрим простой вариант авторизации на сайте с помощью Телеграм без использования официального виджета.

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

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

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

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

  1. в бот мы передадим значение в параметре start, которое обработаем и запишем в базу вместе с данными пользователя
  2. запустим 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_auth.zip
6.1
29 комментариев
Авторизуйтесь через Telegram, чтобы оставить комментарий.
Откройте по ссылке или QR бот @iMakeBot, нажмите кнопку Старт/Start.
Следуйте инструкциям бота.

  • ©Мастер© © КЭП © Программист [6 лет назад]

    Спасибо. Очень интересно

  • TToJIoTeH4uK™ [5 лет назад → ©Мастер© © КЭП © Программист]

    Солидарен)

  • Азиз [3 года назад → ©Мастер© © КЭП © Программист]

    Согласен :)

  • bdika [4 года назад]

    Да, я так и знал что боту придётся самому писать.
    Но это вроде не сложно...

  • Dapster [4 года назад]

    Блин, как круто и бысто!

  • Alex Crowley [3 года назад]

    Круто

  • COCS. [2 года назад]

    После редиректа в бота и нажатию кнопки start ничего не происходит. В чём может быть проблема?

    API токен ввёл

  • iMakeBots [2 года назад → COCS.]

    WebHook настроен?

  • Евгений [1 год назад]

    Доброго дня 😉

    А можете поделиться, как у вас комментарии подвязаны к авторизации через телеграм?

  • iMakeBots [1 год назад → Евгений]

    Простая проверка авторизации на Laravel

    В  blade шаблоне через @auth

  • Евгений [1 год назад → iMakeBots]

    Благодарю за ответ, покопаю в этом направлении  👍

  • Hᴀᴢᴇ [1 год назад → iMakeBots]

    Здравствуйте, подскажите, пожалуйста, есть ли подобное решение с авторизацией через ТГ бота для Wordpress? Буду благодарен если отпишите немного подробнее! 

  • iMakeBots [1 год назад → Hᴀᴢᴇ]

    К сожалению здесь (на этом сайте) это единственный вариант решения.

  • Forum World Support [1 год назад]

    Круто

  • Gigalit [1 год назад]

    Я ещё не проверил работу этого кода, но судя по логике работы с базой, то key - он же token в cookie хранится в одной записи, привязанной к ТГ-id. Что из этого следует: если пользователь авторизовался в одном браузере (например Google Chrome), а потом еще зашел на тот же сайт в другом браузере (например, Fire Fox), то в первом браузере авторизация слетит. Я прав или ошибаюсь?

  • iMakeBots [1 год назад → Gigalit]

    Да здесь это реализовано через уникальность auth_key

  • Gigalit [1 год назад → iMakeBots]

    И еще прощу объяснить, почему auth_key, переданный из бота в базу, при проверке и обнаружении записи сразу меняется на new_key, он же прописывается в куку. Почему нельзя оставить тот же самый, вроде бы на безопасность это не влияет?

  • iMakeBots [1 год назад → Gigalit]

    Если честно то не помню)) видимо какие-то мысли были. 

  • Angel_Devill🇺🇦 [10 месяцев назад]

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

    Добрый люди помогите пожалуйста

  • Степан Гончаров 🇷🇺 [8 месяцев назад]

    Прям дай Бог автору статьи здоровья!

  • NEO🐸 [4 недели назад]

    После нажатия кнопки старт ничего не происходит. подскажите что делать?

  • iMakeBots [4 недели назад → NEO🐸]

    Webhook настроили?

  • NEO🐸 [4 недели назад → iMakeBots]

    что то пробовал но без результата. подскажите что надо сделать

  • NEO🐸 [3 недели назад]

    в браузере все работает. а вот в mini app авторизоваться не могу. подскажите можно как то сделать?

  • iMakeBots [3 недели назад → NEO🐸]

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

  • NEO🐸 [3 недели назад → iMakeBots]

    чтоб один скрипт и в браузере и в мини апп авторизовывал можно как то реализовать?

  • iMakeBots [3 недели назад → NEO🐸]

    Можно, это два разных события: Для сайта выводить авторизацию, для веб апп авторизовывать при входе

  • NEO🐸 [3 недели назад → iMakeBots]

    спасибо. подскажите как аватар пользователя получить при авторизации?

  • iMakeBots [3 недели назад → NEO🐸]

    В апи есть метод как-то называется примерно 

    getUserProfilePhotos