Telegram-бот на Java — это обычная Java-программа, которая по HTTPS забирает у Telegram Bot API новые события (сообщения, нажатия кнопок) и отвечает на них вызовами того же API. Слово «авторизация» в этой теме означает три разные вещи, и их важно не путать: бот доказывает Telegram, что он — это он (токен бота); бот решает, каким пользователям отвечать (проверка user id); сайт проверяет, что пользователь действительно вошел через Telegram (подпись Login Widget).
Содержание
- Мини-словарь
- Шаг 1. Получить токен в BotFather
- Шаг 2. Maven-проект
- Шаг 3. Бот с командами и списком доступа
- Как это работает
- Авторизация пользователей в боте
- Вход на сайт через Telegram: проверка подписи на Java
- Long polling или webhook
- Если не получилось
- Перед запуском в работу
- Выводы
- Где применяется / связь с практикой
- FAQ
Ниже — все три по порядку на одном проекте: токен в BotFather, Maven-проект на библиотеке TelegramBots, бот с командами и списком разрешенных пользователей, затем проверка входа через Telegram на Java. Код проверен 24.09.2026 на Java 25 (Temurin 25.0.4) и TelegramBots 10.3.0; обмен с Bot API прогнан на локальном тестовом сервере, который отвечает в формате Telegram.
Мини-словарь
| Термин | Что это | Где встречается в коде |
|---|---|---|
| Токен бота | Секрет вида 123456:ABC..., выдается BotFather; кто знает токен, тот управляет ботом |
переменная окружения TELEGRAM_BOT_TOKEN |
| Update | Одно событие для бота: новое сообщение, редактирование, нажатие кнопки | метод consume(Update) |
| user id и chat id | id человека и id чата; в личке совпадают, в группе chat id — это id группы | getFrom().getId() и getChatId() |
| Long polling и webhook | Бот сам спрашивает Telegram о новых событиях (getUpdates) или Telegram присылает их на ваш HTTPS-адрес |
TelegramBotsLongPollingApplication |
Шаг 1. Получить токен в BotFather
- Откройте официальный
@BotFather(с синей галочкой) и отправьте/newbot. - Введите отображаемое имя, затем уникальный username, который заканчивается на
bot, напримерmy_otus_bot. - BotFather пришлет токен. Храните его в менеджере паролей, не в коде и не в Git.
Если токен утек, отзовите его командой /revoke: старый перестанет работать, бот получит новый. Командой /setcommands задается меню команд у поля ввода.
Шаг 2. Maven-проект
С версии 7 TelegramBots разбита на модули: telegrambots-longpolling принимает обновления, telegrambots-client отправляет запросы. Учебники с TelegramLongPollingBot и getBotToken() написаны под старые версии и с актуальной не соберутся.
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>ru.example</groupId>
<artifactId>otus-bot</artifactId>
<version>1.0</version>
<properties>
<maven.compiler.release>25</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<telegrambots.version>10.3.0</telegrambots.version>
</properties>
<dependencies>
<dependency>
<groupId>org.telegram</groupId>
<artifactId>telegrambots-longpolling</artifactId>
<version>${telegrambots.version}</version>
</dependency>
<dependency>
<groupId>org.telegram</groupId>
<artifactId>telegrambots-client</artifactId>
<version>${telegrambots.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.20</version>
</dependency>
</dependencies>
</project>
slf4j-simple не обязателен, но без него библиотека молчит о своих ошибках и при старте печатает предупреждение No SLF4J providers were found.
Шаг 3. Бот с командами и списком доступа
Сначала весь класс целиком, затем разбор. Файл src/main/java/ru/example/bot/CommandBot.java:
package ru.example.bot;
import java.util.Locale;
import java.util.Set;
import org.telegram.telegrambots.longpolling.util.LongPollingSingleThreadUpdateConsumer;
import org.telegram.telegrambots.meta.api.methods.send.SendMessage;
import org.telegram.telegrambots.meta.api.objects.Update;
import org.telegram.telegrambots.meta.api.objects.message.Message;
import org.telegram.telegrambots.meta.exceptions.TelegramApiException;
import org.telegram.telegrambots.meta.generics.TelegramClient;
public class CommandBot implements LongPollingSingleThreadUpdateConsumer {
private final TelegramClient telegram;
private final String botUsername; // без @, например my_otus_bot
private final Set<Long> allowedUserIds; // кому бот отвечает
public CommandBot(TelegramClient telegram, String botUsername, Set<Long> allowedUserIds) {
this.telegram = telegram;
this.botUsername = botUsername.toLowerCase(Locale.ROOT);
this.allowedUserIds = Set.copyOf(allowedUserIds);
}
@Override
public void consume(Update update) {
// Нас интересуют только текстовые сообщения; стикеры, фото и т.п. пропускаем
if (!update.hasMessage() || !update.getMessage().hasText()) {
return;
}
Message message = update.getMessage();
boolean privateChat = message.isUserMessage();
// В группе реагируем только на команды, обычную переписку не трогаем
if (!privateChat && !message.getText().startsWith("/")) {
return;
}
Long userId = message.getFrom() == null ? null : message.getFrom().getId();
String reply;
if (userId == null || !allowedUserIds.contains(userId)) {
// Подсказка с id - только в личке, в группе чужим молчим
reply = privateChat ? "Доступ закрыт. Ваш Telegram id: " + userId : null;
} else {
reply = handle(message.getText());
}
if (reply != null) {
send(message.getChatId(), reply);
}
}
String handle(String text) {
String[] parts = text.strip().split("\\s+", 2);
String command = parts[0].toLowerCase(Locale.ROOT);
String argument = parts.length > 1 ? parts[1] : "";
// В группах команда может прийти как /help@имя_бота
int at = command.indexOf('@');
if (at >= 0) {
if (!command.substring(at + 1).equals(botUsername)) {
return null; // команда адресована другому боту
}
command = command.substring(0, at);
}
return switch (command) {
case "/start" -> "Привет! Я учебный бот на Java. Команды: /help, /echo <текст>";
case "/help" -> "/start - приветствие\n/echo <текст> - повторю текст";
case "/echo" -> argument.isEmpty() ? "Напишите текст после /echo" : argument;
default -> command.startsWith("/")
? "Не знаю команду " + command + ". Список: /help"
: "Я понимаю только команды. Список: /help";
};
}
private void send(long chatId, String text) {
SendMessage request = SendMessage.builder()
.chatId(chatId)
.text(text)
.build();
try {
telegram.execute(request);
} catch (TelegramApiException e) {
System.err.println("Не удалось отправить сообщение: " + e.getMessage());
}
}
}
Точка входа BotApp.java читает секреты из переменных окружения:
package ru.example.bot;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
import org.telegram.telegrambots.client.okhttp.OkHttpTelegramClient;
import org.telegram.telegrambots.longpolling.TelegramBotsLongPollingApplication;
public class BotApp {
public static void main(String[] args) throws Exception {
String token = requireEnv("TELEGRAM_BOT_TOKEN");
String username = requireEnv("TELEGRAM_BOT_USERNAME");
Set<Long> allowed = parseIds(System.getenv().getOrDefault("ALLOWED_USER_IDS", ""));
var telegram = new OkHttpTelegramClient(token);
try (var app = new TelegramBotsLongPollingApplication()) {
app.registerBot(token, new CommandBot(telegram, username, allowed));
System.out.println("Бот запущен, разрешенных пользователей: " + allowed.size());
Thread.currentThread().join(); // работаем до Ctrl+C
}
}
private static String requireEnv(String name) {
String value = System.getenv(name);
if (value == null || value.isBlank()) {
throw new IllegalStateException("Не задана переменная окружения " + name);
}
return value.strip();
}
static Set<Long> parseIds(String csv) {
return Arrays.stream(csv.split(","))
.map(String::strip)
.filter(s -> !s.isEmpty())
.map(Long::parseLong)
.collect(Collectors.toUnmodifiableSet());
}
}
Запуск (macOS/Linux, bash или zsh; exec-плагин Maven подтянется при первом вызове):
export TELEGRAM_BOT_TOKEN='сюда-токен-от-BotFather'
export TELEGRAM_BOT_USERNAME='my_otus_bot'
export ALLOWED_USER_IDS='111'
mvn -q compile exec:java -Dexec.mainClass=ru.example.bot.BotApp
Отправьте боту в личке /start. Если вашего id нет в ALLOWED_USER_IDS, бот ответит «Доступ закрыт» и назовет id — впишите его в переменную и перезапустите. Журнал прогона на локальном тестовом сервере Bot API:
getUpdates offset=1 -> 7
sendMessage chat_id=111 text='Привет! Я учебный бот на Java. Команды: /help, /echo <текст>'
sendMessage chat_id=111 text='привет, бот'
sendMessage chat_id=111 text='Не знаю команду /weather. Список: /help'
sendMessage chat_id=111 text='/start - приветствие\n/echo <текст> - повторю текст'
sendMessage chat_id=999 text='Доступ закрыт. Ваш Telegram id: 999'
getUpdates offset=8 -> 0
Входящие: /start, /echo привет, бот, /weather, /help@other_bot, /HELP@My_Otus_Bot, стикер и /start в личке от чужого пользователя 999. Команда для другого бота и стикер остались без ответа — так и задумано. В группе бот так же молчит на обычные сообщения и на команды посторонних: иначе он отвечал бы «Доступ закрыт» на каждую реплику участников.
Как это работает
- Регистрация.
registerBotсначала вызываетdeleteWebhook(в прогоне это первый запрос), потому что long polling и webhook у одного бота одновременно не работают. - Цикл опроса. Библиотека вызывает
getUpdatesс параметромoffset. После обработки событий 1-7 следующий запрос идет сoffset=8: так Telegram понимает, что события до 7 включительно доставлены. Неподтвержденные события Telegram хранит ограниченное время (по документации Bot API — до 24 часов). consume(Update)вызывается для событий по очереди, в одном потоке: медленная операция внутри задерживает все следующие ответы.handle(String)отделяет команду от аргумента, приводит к нижнему регистру и снимает суффикс@имя_бота. Сети он не касается и легко тестируется.
Частая ошибка: сравнивать команду через equals
public class NaiveCommands {
static String handle(String text) {
if (text.equals("/start")) return "Привет!";
return "Не знаю команду";
}
public static void main(String[] args) {
for (String text : new String[] {"/start", "/start@my_otus_bot", "/Start", "/start "}) {
System.out.println(text + " -> " + handle(text));
}
}
}
Результат:
/start -> Привет!
/start@my_otus_bot -> Не знаю команду
/Start -> Не знаю команду
/start -> Не знаю команду
В группах клиент добавляет к команде @имя_бота, а пользователь может набрать регистр или пробел иначе. Исправление — разбор из handle(): на те же четыре входа он отвечает приветствием, а /start@other_bot игнорирует (возвращает null).
Авторизация пользователей в боте
Бот открыт всем, кто найдет его по username. Если он управляет чем-то внутренним (сервер, заказы, отчеты), доступ ограничивают по user id: именно getFrom().getId(), а не username (его можно сменить) и не chat id (в группе он общий для всех участников). Для десятков пользователей и ролей список переносят в БД, а подсказку «Доступ закрыт» с id в рабочем боте убирают.
Вход на сайт через Telegram: проверка подписи на Java
Для входа на сайт служит Telegram Login Widget: пользователь нажимает кнопку, а сервер получает поля id, first_name, username, auth_date и hash. Верить им можно только после проверки подписи, иначе любой подставит чужой id. Алгоритм из документации Telegram: все поля, кроме hash, по алфавиту в виде ключ=значение через перевод строки; ключ = SHA-256 от токена бота; HMAC-SHA-256 этой строки сравнить с hash.
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Duration;
import java.time.Instant;
import java.util.HexFormat;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class TelegramLoginCheck {
static boolean isValid(Map<String, String> fields, String botToken,
Instant now, Duration maxAge) throws Exception {
String receivedHash = fields.get("hash");
String authDate = fields.get("auth_date");
if (receivedHash == null || authDate == null) {
return false;
}
// 1. data-check-string: все поля, кроме hash, по алфавиту, через \n
String dataCheckString = new TreeMap<>(fields).entrySet().stream()
.filter(e -> !e.getKey().equals("hash"))
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("\n"));
// 2. ключ = SHA-256 от токена бота, подпись = HMAC-SHA-256
byte[] secretKey = MessageDigest.getInstance("SHA-256")
.digest(botToken.getBytes(StandardCharsets.UTF_8));
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secretKey, "HmacSHA256"));
byte[] expected = mac.doFinal(dataCheckString.getBytes(StandardCharsets.UTF_8));
// 3. сравнение за постоянное время + проверка свежести
try {
byte[] received = HexFormat.of().parseHex(receivedHash);
long age = now.getEpochSecond() - Long.parseLong(authDate);
return MessageDigest.isEqual(expected, received)
&& age >= 0 && age <= maxAge.toSeconds();
} catch (IllegalArgumentException e) { // не hex или не число
return false;
}
}
public static void main(String[] args) throws Exception {
String token = "123456:TEST-TOKEN"; // учебный, не настоящий
Map<String, String> fromWidget = Map.of(
"id", "111",
"first_name", "Anna",
"username", "anna_dev",
"auth_date", "1790000000",
"hash", "50cb27bb97d52e33c86e9df76e9dc745c10dd05cd4c40ef9158ad7dc05d4c7fa");
Instant now = Instant.ofEpochSecond(1790000000L + 600); // через 10 минут
Duration maxAge = Duration.ofDays(1);
System.out.println("подлинные данные: " + isValid(fromWidget, token, now, maxAge));
var forged = new TreeMap<>(fromWidget);
forged.put("id", "222"); // подменили id пользователя
System.out.println("подмененный id: " + isValid(forged, token, now, maxAge));
Instant later = now.plus(Duration.ofDays(2));
System.out.println("через 2 дня: " + isValid(fromWidget, token, later, maxAge));
var broken = new TreeMap<>(fromWidget);
broken.put("hash", "not-hex");
System.out.println("мусор в hash: " + isValid(broken, token, now, maxAge));
}
}
Запуск без Maven, одним файлом: java TelegramLoginCheck.java. Результат:
подлинные данные: true
подмененный id: false
через 2 дня: false
мусор в hash: false
MessageDigest.isEqual сравнивает за постоянное время, проверка auth_date не дает бесконечно переиспользовать перехваченные данные, а токен остается на сервере: проверять подпись в JavaScript на странице нельзя. Проверка подтверждает лишь, что данные подписал Telegram для вашего бота; сессию, CSRF-защиту и HTTPS делают отдельно. Для Mini Apps ключ для initData вычисляется иначе.
Long polling или webhook
| Критерий | Long polling | Webhook |
|---|---|---|
| Нужен публичный HTTPS-адрес | нет | да |
| Работает с ноутбука за NAT | да | нет без туннеля |
| Сколько экземпляров бота на токен | один | несколько за балансировщиком |
| Когда брать | учеба, внутренние боты, небольшая нагрузка | продакшен с нагрузкой, serverless |
Если не получилось
Exception in thread "main" 401: ...TelegramApiErrorResponseException: Unauthorizedпри старте. Токен неверный, отозван или скопирован с пробелом. Проверьте его запросомcurl https://api.telegram.org/bot<токен>/getMe: на неверный токен Bot API отвечает{"ok":false,"error_code":401,"description":"Unauthorized"}.409 Conflict. Про другойgetUpdates— запущено два экземпляра бота с одним токеном (в IDE и на сервере), оставьте один. Про активный webhook —registerBotснимает его сам, в других клиентах вызовитеdeleteWebhook.- Бот молчит в группе. По умолчанию включен privacy mode, и бот видит в группе не все сообщения. Команды с
@имя_ботаон получает; для остального отключите режим в BotFather (/setprivacy) и заново добавьте бота в группу. - Не собирается: нет класса
TelegramLongPollingBot. Код из старого учебника под TelegramBots 6.x и раньше. Используйте API из примера выше. - Бот отвечает с задержкой. В
consume()идет долгая операция — вынесите ее вExecutorService.
Перед запуском в работу
Учебный бот — основа, а не готовый сервис. Для реальной работы добавьте: токен в секрет-хранилище сервера, логи без токена и текстов личных сообщений, ограничение частоты запросов от одного пользователя, повтор отправки при сбоях сети и автоперезапуск (systemd или контейнер).
Выводы
- Telegram-бот на Java — программа, которая получает события через
getUpdatesили webhook и отвечает вызовами Bot API; актуальная библиотека TelegramBots 10.x используетTelegramBotsLongPollingApplicationиOkHttpTelegramClient. - Токен от BotFather — это авторизация самого бота; его держат вне кода и отзывают через
/revokeпри утечке. - Ограничивать доступ к боту нужно по user id из
getFrom(), а не по username и не по chat id. - Вход на сайт через Telegram проверяется на сервере: HMAC-SHA-256 с ключом SHA-256(токен), сравнение за постоянное время и проверка
auth_date. - Команды надежнее разбирать отдельным методом без сети: так учитываются
@имя_бота, регистр и пробелы, а логику легко тестировать.
Где применяется / связь с практикой
Освойте тему на практике
Боты на Java пишут для внутренних уведомлений (сборки, мониторинг, заявки) и поддержки клиентов поверх существующего бэкенда. Сам бот — повод потренировать базу языка: интерфейсы, коллекции, исключения, HTTP, сборку через Maven. Разобрать эту базу по порядку, с домашними заданиями и ревью кода, можно на курсе «Java разработчик. Базовый уровень». Посмотреть, как проходят занятия, можно бесплатно на открытых уроках Otus.
FAQ
Можно ли написать бота на Java без библиотеки?
Да, Bot API — это обычные HTTPS-запросы, их можно отправлять через java.net.http.HttpClient. Но тогда разбор JSON, повторы при ошибках и цикл getUpdates с offset придется писать самому.
Подойдет ли Spring Boot?
Да, у TelegramBots есть отдельные модули для интеграции со Spring Boot. Для первого бота они не нужны: достаточно main из примера.
Где взять свой user id?
Проще всего запустить бота из статьи с пустым ALLOWED_USER_IDS: он ответит «Доступ закрыт» и покажет ваш id.



