Способ аутентификации через телеграм отлично описан в документации. В этой статье мы реализуем его в Spring Boot приложении.

Создаем туннель с помощью ngrok

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

Мы запустим ngrok в докере, вот так будет выглядеть docker-compose.yaml:

services:  
  ngrok:    
    image: ngrok/ngrok:latest    
    #команды для запуска ngrok    
    command:      
      - "start"      
      - "--all"     
      - "--config"      
      - "/etc/ngrok.yml"    
    volumes:      
      - ./ngrok.yml:/etc/ngrok.yml #файл с настройками    
    network_mode: host

network_mode: host нужно, чтобы контейнер использовал порты хоста и перенаправлял трафик на хост, а не в контейнер.

Для работы ngrok потребуется зарегистрироваться на их сайте и получить токен.

После этого, в той же папке, где находится docker-compose.yaml, создаем файл конфигурации ngrok.yml и добавляем туда токен:

version: 2
authtoken: YOUR_NGROK_AUTHTOKEN
tunnels:  
  httpbin:    
    proto: http    
    addr: 8080

Теперь весь трафик, проходящий через созданный нгроком туннель, будет перенаправлен на localhost:8080.

После запуска контейнера перейдите на localhost:4040, там будет url, по которому ваше приложение доступно в интернете. Url будет оставаться неизменным, пока вы не перезапустите контейнер.

Создаем бота для аутентификации

Далее перейдите в BotFather, создайте бота, который будет использован для аутентификации, и привяжите к нему полученный url. Для этого перейдите в Bot Settings -> Domain -> Edit domain и отправьте url:

Далее переходим на страницу документации, вставляем логин бота в поле для него, выбираем способ авторизации "Callback".

При авторизации Callback будет вызвана функция TelegramAuth, в которую передастся объект с данными пользователя.
Скопируем сгенерированный код и укажем в функции TelegramAuth эндпоинт нашего сервера, на который надо отправить данные для проверки.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Telegram auth</title>
</head>
<body>
<script async src="https://telegram.org/js/telegram-widget.js?22" data-telegram-login="your-bot-login" data-size="large"
        data-onauth="onTelegramAuth(user)"></script>
<script type="text/javascript">    function onTelegramAuth(user) {
    fetch("https://adc8-176-50-96-51.ngrok-free.app/auth/telegram/token", {method: 'POST', body: JSON.stringify(user)})
        .then(response => response.text())
        .then(body => {
            alert(body)
        });
}
</script>
</body>
</html>

Создаем приложение и контроллер для аутентификации

После создания обычного Spring Boot прилоения помещаем html файл в папку resources/static, создаем контроллер, который будет отдавать наш html (getAuthScript()) и проверять данные, полученные от телеграма (authenticate())

import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Map;

import static java.nio.charset.StandardCharsets.UTF_8;

@RestController
@RequestMapping("/auth/telegram")
public class AuthController {

    private final String tgBotToken = "YOUR_TELEGRAM_BOT_TOKEN";

    /**
     * возвращает html со скриптом для аутентификации
     */
    @GetMapping
    public ResponseEntity<Resource> getAuthScript() {
        Resource resource = new ClassPathResource("static/telegramAuth.html");
        var headers = new HttpHeaders();
        headers.add(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=telegramAuth.html");
        return ResponseEntity.ok().headers(headers).body(resource);
    }

    /**
     * сюда отправляются данные, полученные после аутентификации
     */
    @PostMapping
    public String authenticate(@RequestBody Map<String, Object> telegramData) {
        return telegramDataIsValid(telegramData) ? "pretend-that-it-is-your-token" : "error";
    }

    /**
     * проверяет данные, полученные из телеграм
     */
    private boolean telegramDataIsValid(Map<String, Object> telegramData) {
        //получаем хэш, который позже будем сравнивать с остальными данными
        String hash = (String) telegramData.get("hash");
        telegramData.remove("hash");

        //создаем строку проверки - сортируем все параметры и объединяем их в строку вида:
        //auth_date=<auth_date>\nfirst_name=<first_name>\nid=<id>\nusername=<username>
        StringBuilder sb = new StringBuilder();
        telegramData.entrySet().stream()
                .sorted(Map.Entry.comparingByKey())
                .forEach(entry -> sb.append(entry.getKey()).append("=").append(entry.getValue()).append("\n"));
        sb.deleteCharAt(sb.length() - 1);
        String dataCheckString = sb.toString();

        try {
            //генерируем SHA-256 хэш из токена бота
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] key = digest.digest(tgBotToken.getBytes(UTF_8));

            //создаем HMAC со сгенерированным хэшем
            Mac hmac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKeySpec = new SecretKeySpec(key, "HmacSHA256");
            hmac.init(secretKeySpec);

            // добавляем в HMAC строку проверки и переводим в шестнадцатеричный формат
            byte[] hmacBytes = hmac.doFinal(dataCheckString.getBytes(UTF_8));
            StringBuilder validateHash = new StringBuilder();
            for (byte b : hmacBytes) {
                validateHash.append(String.format("%02x", b));
            }

            // сравниваем полученный от телеграма и сгенерированный хэш
            return hash.contentEquals(validateHash);
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            throw new RuntimeException(e);
        }
    }
}

Алгоритм реализации функции проверки (telegramDataIsValid()) так же описан в документации:
Проверить аутентификацию и целостность полученных данных можно, сравнив полученный хэш‑параметр с шестнадцатеричным представлением подписи HMAC‑SHA-256 строки проверки данных с хешем SHA256 токена бота, используемого в качестве секретного ключа.

Строка проверки данных представляет собой объединение всех полученных полей, отсортированных в алфавитном порядке, в формате key= с символом перевода строки ('\n', 0×0A), используемым в качестве разделителя.

После запуска приложения переходим на https://adc8-176-50-96-51.ngrok-free.app/auth/telegram и проходим аутентификацию в телеграме.

Заключение

Статья написана в процессе разработки стартапа на Spring Boot ?
Подписывайтесь, если было интересно

Комментарии (0)