- PVSM.RU - https://www.pvsm.ru -
![[Laravel-Telegram] Пример сервиса на Laravel для авторизации пользователя через Telegram в mini-app - 1 [Laravel-Telegram] Пример сервиса на Laravel для авторизации пользователя через Telegram в mini-app - 1](http://www.pvsm.ru/images/2026/01/27/Laravel-Telegram-primer-servisa-na-Laravel-dlya-avtorizacii-polzovatelya-cherez-Telegram-v-mini-app.jpg)
Авторизация пользователя в Telegram mini-app сейчас как никогда важна. Но что если вам необходимо сделать это в вашей сервисной/микросервисной архитектуре?
Вот и у меня встал такой вопрос при разработке.
Всем привет! Меня зовут Гурский Алексей, я FullStack разработчик и сейчас я расскажу вам как это работает на самом деле.
Для тех кто не хочет читать то может посмотреть сразу на backend реализацию: https://github.com/palachX/laravel-telegram-example [1]
Код написан для примера пожалуйста сильно не ругайтесь, но можете написать свои комментарии по улучшению.
Авторизация пользователя посредством отправки смс-кода на указанный номер телефона. После чего при открытии mini-app пользователь должен увидеть главную страницу сайта и информацию личного кабинета.
Сервис авторизации уже написан для обычного frontend и мы не хотели бы его усложнять или костылить логикой Telegram.
Исходя из проблем и задачи было принято решение: мы должны реализовать Telegram-Service в нашей сервисной архитектуре. Он будет отвечать за обработку всей бизнес логики для Telegram, хранить данные пользователей, а также токены для доступа. И послужит в некотором роде backend-клиентом.
До начала выполнения работ вы должны были создать и привязать своего телеграм бота к вебхуку информации про это написано очень много, поэтому опускаю этот момент.
Ссылка на схему drawio: https://drive.google.com/file/d/1PIBkS3qy7Gart1nleAPo9x3s79J-EVhx/view?usp=sharing [2]
3 небольше схемы но работы предстоит много...
Саму реализацию авторизации, отправку смс-кодов, а также обработку состояний пользователя я не буду описывать, это много кода для статьи, но всё описано в проект-примере: https://github.com/palachX/laravel-telegram-example [1]
Там я написал пример того как можно обрабатывать ответы пользователя.
А также по схемам вы сможете реализовать именно тут функционал который устроит именно вас. Я же для вас опишу самый последний шаг проверки данных InitData которые приходят к нам от frotnend.
Я пишу на Vue/Nuxt в качестве примера буду показывать код на Nuxt (NoSSR).
Нам необходимо подключить telegram-mini-app в своё приложение идём в nuxt.config.ts/js и вставляем в блок app.head массив script
app: {
head: {
script: [
{ src: 'https://telegram.org/js/telegram-web-app.js?59' },
],
},
},
Если у вас ещё нет единой точки для входа в приложение тогда мы сейчас её напишем
Создаём composable useBootstrap внутри нам необходимо получить данные InitData отправить их на telegram-service получить токен, с помощью которого мы уже получим нужные нам данные пользователя:
export function useBootstrap(initData: string | null | undefined) {
const loading = ref(true)
// TODO Создайте нужные для вас composable в данном случае код написан для примера
const { loadInitData, telegramResponse } = useTelegramInitData()
const tokensStore = useTokensStore()
/**
* Хранилка useTelegramDataStore необходима для того чтобы ваше приложение
понимало открыто ли Mini-App к примеру в моём случае мне необходимо было
скрыть некоторый контент для Mini-App
*/
const telegramDataStore = useTelegramDataStore()
/**
* Если есть данные от телеграмм, то делаем запрос в телеграмм-сервис
*/
const initTelegram = async (): Promise<string | null> => {
if (!initData) {
telegramDataStore.hasTelegramInitData = false
console.warn('useBootstrap: initData is null')
return null
}
// TODO через свой $fetch или useApi делаете запрос на свой backend
await loadInitData(initData)
const response = telegramResponse.value
telegramDataStore.hasTelegramInitData = !!response?.data.token
return response?.data.token ?? null
}
const handleAuth = async (): Promise<void> => {
// TODO реализация получения данных авторизированного пользователя, вам необходимо взять полученные токен и отправить запрос на нужный сервис
}
const initApp () => {
try {
const telegramToken = await initTelegram()
if (telegramToken) {
tokensStore.setAccessToken(telegramToken)
}
await handleAuth()
}
catch (error: unknown) {
await handleError(error)
}
finally {
loading.value = false
}
}
}
Идём в файл app.vue - это стандартный файл в nuxt пишем:
const initData: string | null | undefined = window?.Telegram?.WebApp?.initData
const { initApp } = useBootstrap(initData)
initApp()
Для тех кто пишет на TypeScript лайфхак чтобы Window.Telegram не выдавал ошибку
Добавьте папку types в корень проекта
Внутри создайте type.d.ts
Добавьте в него код:
declare global {
interface Window {
Telegram: {
WebApp: {
initData: string | null
} | null
} | null
}
}
export {}
На frontend работа окончена теперь переходим к реализации backend
Данные на вход
<?php
declare(strict_types=1);
namespace AppUseCasesV1TelegramInitData;
use SpatieLaravelDataAttributesMapName;
use SpatieLaravelDataData;
use SpatieLaravelDataMappersSnakeCaseMapper;
#[MapName(SnakeCaseMapper::class)]
final class DataInput extends Data
{
public function __construct(
public readonly string $initData
) {
}
}
Данные на выход
<?php
declare(strict_types=1);
namespace AppUseCasesV1TelegramInitData;
use CarbonCarbon;
use SpatieLaravelDataAttributesMapName;
use SpatieLaravelDataMappersSnakeCaseMapper;
use SpatieLaravelDataResource;
#[MapName(SnakeCaseMapper::class)]
final class DataOutput extends Resource
{
public function __construct(
public readonly string $token,
public readonly ?Carbon $expiresAt,
) {
}
}
Класс для обработки данных
<?php
declare(strict_types=1);
namespace AppUseCasesV1TelegramInitData;
use AppRepositoriesUserRepository;
use AppRepositoriesUserTokenRepository;
use InvalidArgumentException;
use JsonException;
final readonly class Handler
{
public function __construct(
private UserRepository $userRepository,
private UserTokenRepository $userTokenRepository,
private string $botToken,
) {
}
/**
* @throws JsonException
*/
public function handle(DataInput $dataInput): DataOutput
{
$initData = $dataInput->initData;
/**
* @var array{
* query_id?: string,
* user?: string,
* auth_date?: string,
* hash?: string,
* signature?: string,
* chat_instance?: string,
* chat_type?: string,
* start_param?: string,
* can_send_after?: string
* } $parsed
*/
$parsed = [];
parse_str($initData, $parsed);
if (! isset($parsed['hash'])) {
throw new InvalidArgumentException('Invalid init data hash', 400);
}
/** @var string $hash */
$hash = $parsed['hash'];
$this->assertValidHash(
initData: $initData,
receivedHash: $hash,
token: $this->botToken,
);
if (! isset($parsed['user'])) {
throw new InvalidArgumentException('Invalid init data user', 400);
}
/** @var string $user */
$user = $parsed['user'];
/**
* @var array{
* id: int,
* is_bot: bool,
* first_name?: string,
* last_name?: string,
* username?: string
* } $userDecode
*/
$userDecode = json_decode($user, true, 512, JSON_THROW_ON_ERROR);
$user = $this->userRepository->getUserByChatId($userDecode['id']);
$tokenData = $this->userTokenRepository->getByUserId($user->id);
if ($tokenData === null) {
throw new InvalidArgumentException('Invalid init data token', 401);
}
return new DataOutput(
token: $tokenData->token,
expiresAt: $tokenData->expires_at
);
}
private function assertValidHash(string $initData, string $receivedHash, string $token): void
{
/**
* @var array<string, string> $data
*/
$data = [];
parse_str($initData, $data);
unset($data['hash']);
ksort($data);
$checkString = collect($data)
->map(function ($value, $key) {
if (is_array($value)) {
$value = json_encode($value, JSON_THROW_ON_ERROR);
}
return $key.'='.$value;
})
->implode("n");
$secretKey = hash_hmac('sha256', $token, 'WebAppData', true);
$calculatedHash = bin2hex(hash_hmac('sha256', $checkString, $secretKey, true));
if (! hash_equals($calculatedHash, $receivedHash)) {
throw new InvalidArgumentException('Invalid init data hash', 400);
}
}
}
Контроллер
<?php
declare(strict_types=1);
namespace AppHttpControllersApiV1;
use AppHttpControllersController;
use AppUseCasesV1TelegramInitDataDataInput;
use AppUseCasesV1TelegramInitDataHandler;
use IlluminateHttpJsonResponse;
use JsonException;
class TelegramDataController extends Controller
{
/**
* @throws JsonException
*/
public function handle(DataInput $data, Handler $handler): JsonResponse
{
return new JsonResponse(['data' => $handler->handle($data)]);
}
}
Тестирование успешного кейса
<?php
declare(strict_types=1);
namespace TelegramUseCase;
use AppModelsUser;
use AppModelsUserToken;
use TestsApiTestCase;
use TestsTelegramSetup;
final class TelegramInitHandlerTest extends ApiTestCase
{
use TelegramSetup;
private const string URL_DATA = '/api/v1/telegram/init-data';
protected function setUp(): void
{
parent::setUp();
$this->telegramSetup();
}
public function testSuccess(): void
{
$user = User::factory()->createOne();
UserToken::factory()->for($user)->createOne();
$authDate = now()->timestamp;
$payload = [
'query_id' => 'AAHdF6IQAAAAAN0XohDhrOrc',
'user' => [
'id' => $user->telegram_id,
'is_bot' => false,
'username' => $user->username,
],
'auth_date' => $authDate,
];
ksort($payload);
$dataForHash = collect($payload)
->map(function ($value, $key) {
if (is_array($value)) {
$value = json_encode($value, JSON_THROW_ON_ERROR);
}
return $key.'='.$value;
})
->implode("n");
$secretKey = hash_hmac('sha256', $this->tgBotToken, 'WebAppData', true);
$hash = bin2hex(hash_hmac('sha256', $dataForHash, $secretKey, true));
$initDataBase = http_build_query([
'query_id' => $payload['query_id'],
'user' => json_encode($payload['user'], JSON_THROW_ON_ERROR),
'auth_date' => $payload['auth_date'],
]);
$initData = $initDataBase.'&hash='.$hash;
$this->postJson(self::URL_DATA, [
'init_data' => $initData,
])->assertOk();
}
}
Маршрутизатор api.php
Route::prefix('v1')->group(function () {
Route::prefix('telegram')->group(function () {
// Отлавливание ответов от telegram
Route::post('webhook', [TelegramWebhookController::class, 'handle']);
// Авторизация пользователя
Route::post('init-data', [TelegramDataController::class, 'handle']);
});
});
Если вы как и я хотите при регистрации DI не писать внутри конструктора получение конфига, то вы можете зарегистрировать класс обработчика в ServiceProvider вручную и сразу передать в конструктор нужные конфиги:
<?php
declare(strict_types=1);
namespace AppProviders;
use AppRepositoriesUserRepository;
use AppRepositoriesUserStateRepository;
use AppRepositoriesUserTokenRepository;
use AppTelegramFactoriesCommandHandlerFactoryCommandsHandlersFactory;
use AppTelegramFactoriesStateHandlerFactoryStatesHandlersFactory;
use AppTelegramInterfacesBaseHandler;
use AppTelegramServicesTelegramService;
use AppUseCasesV1TelegramInitDataHandler as InitDataHandler;
use IlluminateSupportServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
$this->app->bind(InitDataHandler::class, function () {
/** @var string $botToken */
$botToken = config('telegram-bot.bot_token');
return new InitDataHandler(app(UserRepository::class), app(UserTokenRepository::class), $botToken);
});
}
}
Пользователь открывает телеграм бота
Указывает номер телефона
Подтверждает код который к нему пришёл
Открывает mini-app = он авторизирован и видит личный кабинет
Сервис работает отлично. Бизнес получил требуемый функционал - все счастливы!
P.S. Моя первая статья на habr, я немного волнуюсь, вы можете высказывать критику любого вида, а также задавать свои вопросы на которые я постараюсь ответить.
Моя реализация, мои схемы и мой пример проекта это не истина, возможно для вас что-то подойдёт и я помог вам и направил вас, а возможно это не то что вы искали.
Спасибо, что прочитали до конца!
Автор: adobry96
Источник [3]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/backend/443073
Ссылки в тексте:
[1] https://github.com/palachX/laravel-telegram-example: https://github.com/palachX/laravel-telegram-example
[2] https://drive.google.com/file/d/1PIBkS3qy7Gart1nleAPo9x3s79J-EVhx/view?usp=sharing: https://drive.google.com/file/d/1PIBkS3qy7Gart1nleAPo9x3s79J-EVhx/view?usp=sharing
[3] Источник: https://habr.com/ru/articles/989350/?utm_source=habrahabr&utm_medium=rss&utm_campaign=989350
Нажмите здесь для печати.