Меню

Получение токена доступа API Firebase (HTTP v1) с помощью PHP для отправки push-уведомлений.

Просмотров: 315

До июня 2024 года API Firebase использовал постоянный токен доступа, который можно
было получить один раз в консоли. Однако с теперь необходимо
запрашивать новый токен каждый час. Такой подход повышает безопасность и защищает
от возможной компрометации ключей.

PHP класс для получения токена API Firebase (HTTP v1)

Данная заметка служит вспомогательным материалом к статье:
Настройка push-уведомлений в приложении Apache Cordova .

Также она используется для создания модуля 1С-Битрикс в видеоруке: Создание модуля для 1С-Битрикс, часть 2

Перейдите в консоль Firebase и скачайте JSON-файл с доступами к API. Он находится в разделе Project Settings → Service Accounts. В результате вы получите файл вида:

{
  "type": "service_account",
  "project_id": "ru-bxstore",
  "private_key_id": "e66ae616565f7dd332c18b7b",
  "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEoibl1N0w=\n-----END PRIVATE KEY-----\n",
  "client_email": "firebase-adminsdk-fbsvc@ru-tech-bxstore.iam.gserviceaccount.com",
  "client_id": "107969483695",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40ru.iam.gserviceunt.com",
  "universe_domain": "googleapis.com"
}

После этого добавьте и подключите нижеописанный класс на странице, где планируете использовать токен.

В рамках видеоруководства создан модуль, содержащий этот класс. Его можно скачать по ссылке, приведенной в видеоуроке.

Класс максимально прокомментирован, можете использовать не только в 1С-Битрикс но в любом PHP приложении (не забудте скорректировать namespace):

namespace mibazarow\pushsender;

class GetFirebaseToken
{
    // Приватное свойство для хранения приватного ключа из JSON
    protected $privateKey;
    // Приватное свойство для хранения email клиента из JSON
    protected $clientEmail;

    /**
     * Конструктор: принимает либо путь к файлу JSON, либо строку JSON
     * @param string $jsonInput - путь к файлу или JSON-строка
     */
    public function __construct(string $jsonInput)
    {
        // Проверяем, существует ли файл по пути $jsonInput
        if (file_exists($jsonInput)) {
            // Если файл есть, читаем его содержимое
            $content = file_get_contents($jsonInput);
            // Декодируем содержимое JSON в массив
            $data = json_decode($content, true);
            if ($data === null && json_last_error() !== JSON_ERROR_NONE) {
                throw new \Exception('Ошибка при декодировании JSON из файла: ' . json_last_error_msg());
            }
        } else {
            // Иначе предполагаем, что входная строка — JSON
            $data = json_decode($jsonInput, true);
            if ($data === null && json_last_error() !== JSON_ERROR_NONE) {
                throw new \Exception('Ошибка при декодировании JSON строки: ' . json_last_error_msg());
            }
        }

        // Проверяем наличие обязательных полей: private_key и client_email
        if (!isset($data['private_key']) || !isset($data['client_email'])) {
            throw new \Exception('Недостающие поля в JSON: private_key или client_email');
        }

        // Сохраняем полученные данные в свойства класса
        $this->privateKey = $data['private_key'];
        $this->clientEmail = $data['client_email'];
    }

    /**
     * Получение токена доступа Firebase
     * @param int|null $AccessTokenMaximumExpirationTime - максимально допустимое время жизни токена (секунд), по умолчанию 3600
     * @param int|null $RequestTimeout - таймаут запроса curl (секунд), по умолчанию 10
     * @return string - токен доступа
     */
    public function getAccessToken(?int $AccessTokenMaximumExpirationTime = 3600, ?int $RequestTimeout = 10): string
    {
        // Вызов статического метода для получения токена
        return self::requestAccessToken($this->privateKey, $this->clientEmail, $AccessTokenMaximumExpirationTime, $RequestTimeout);
    }

    /**
     * Статический метод для запроса access_token из Firebase с помощью JWT
     * @param string $PrivateKey - приватный ключ в формате PEM
     * @param string $AccountName - email учетной записи (client_email)
     * @param int|null $AccessTokenMaximumExpirationTime - время жизни токена (сек)
     * @param int|null $RequestTimeout - таймаут curl (сек)
     * @return string - токен доступа
     */
    public static function requestAccessToken(
        string $PrivateKey,
        string $AccountName,
        ?int $AccessTokenMaximumExpirationTime = 3600,
        ?int $RequestTimeout = 10
    ): string {
        // Проверка наличия приватного ключа и имени аккаунта
        if ($PrivateKey !== '' && $AccountName !== '') {
            // Время начала выполнения запроса
            $StartTime = time();

            // Создаем заголовок JWT (алгоритм RS256, тип JWT)
            $Header = json_encode([
                'alg' => 'RS256',
                'typ' => 'JWT'
            ]);

            // Проверка и установка значения таймаута
            if (!isset($RequestTimeout) || $RequestTimeout < 0) $RequestTimeout = 10;

            // Ограничение времени жизни токена, максимум 3600 секунд
            if (!isset($AccessTokenMaximumExpirationTime) || $AccessTokenMaximumExpirationTime <= 0 || $AccessTokenMaximumExpirationTime > 3600) {
                $AccessTokenMaximumExpirationTime = 3600;
            }

            // Рассчет времени окончания действия токена
            $AccessTokenExpirationEndTime = $StartTime + $AccessTokenMaximumExpirationTime;

            // URL аудитории (Firebase OAuth2 endpoint)
            $Aud = 'https://oauth2.googleapis.com/token';

            // Создаем тело JWT (заявку) с нужными данными
            $Data = json_encode([
                'iss' => $AccountName, // issuer — email аккаунта
                'scope' => 'https://www.googleapis.com/auth/firebase.messaging', // область
                'aud' => $Aud, // аудитория
                'exp' => $AccessTokenExpirationEndTime, // время истечения
                'iat' => $StartTime // время выпуска
            ]);

            // Кодируем заголовок и данные в base64url
            $Base64UrlHeader = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($Header));
            $Base64UrlData = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($Data));

            // Подписываем данные приватным ключом
            $Signature = '';
            $PrivateKeyResource = openssl_get_privatekey($PrivateKey);
            openssl_sign("{$Base64UrlHeader}.{$Base64UrlData}", $Signature, $PrivateKeyResource, OPENSSL_ALGO_SHA256);
            // Кодируем подпись в base64url
            $Base64UrlSignature = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($Signature));

            // Формируем JWT
            $JWT = "{$Base64UrlHeader}.{$Base64UrlData}.{$Base64UrlSignature}";

            // Подготовка данных для POST-запроса
            $PostFields = [
                'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
                'assertion' => $JWT
            ];

            // Инициализация curl-запроса
            $Ch = curl_init($Aud);
            curl_setopt($Ch, CURLOPT_CONNECTTIMEOUT, $RequestTimeout);
            curl_setopt($Ch, CURLOPT_TIMEOUT, $RequestTimeout);
            curl_setopt($Ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($Ch, CURLOPT_POSTFIELDS, http_build_query($PostFields));

            // Выполняем запрос и получаем ответ
            $Result = curl_exec($Ch);
            $Errno = 0;
            $Error = '';

            if ($Result === false) {
                // В случае ошибки curl сохраняем ошибку
                $Errno = curl_errno($Ch);
                $Error = curl_error($Ch);
            }

            // Закрываем curl-сессию
            curl_close($Ch);

            // Обработка ошибок curl
            if ($Errno > 0 && $Error != '') {
                throw new \Exception($Error, $Errno);
            } else {
                if ($Result !== false) {
                    // Декодируем ответ JSON
                    $ResultArray = @json_decode($Result, true);
                    // Проверяем наличие access_token
                    if (is_array($ResultArray)) {
                        if (isset($ResultArray['access_token'])) {
                            // Возвращаем полученный токен
                            return trim($ResultArray['access_token']);
                        } else if (isset($ResultArray['error'])) {
                            // Обрабатываем ошибку из API
                            throw new \Exception($ResultArray['error']['message'], $ResultArray['error']['code']);
                        }
                    }
                }
            }
        } else {
            // Проверяем наличие обязательных данных и выбрасываем исключения
            if ($PrivateKey == '') {
                throw new \Exception('Не указан закрытый ключ.', 110);
            }
            if ($AccountName == '') {
                throw new \Exception('Не указано имя учетной записи.', 110);
            }
        }
        // Возвращаем пустую строку, если что-то пошло не так
        return '';
    }
}
Использование класса:
use mibazarow\pushsender\GetFirebaseToken;

// Указание пути к файлу JSON
$firebase = new GetFirebaseToken('/ПУТЬ_К_ФАЙЛУ/ВАШ.json');

// Или создание объекта с помощью строки JSON
$jsonString = 'Здесь ваша JSON строка';
$firebase = new GetFirebaseToken($jsonString);

// Получение токена
$token = $firebase->getAccessToken();

// Ваш токен, можно использовать
echo $token;

Обратите внимание: Используйте либо полный путь к файлу JSON, либо передавайте строку с его содержимым.

В рамках создания модуля осуществляется чтение JSON из настроек модуля, сохраненных в базе данных. Поэтому в коде JSON получается в виде строки и передается в конструктор класса именно так.

Михаил Базаров 18.05.2025
Полный пример страницы Битрикс модуля с получением строки из настроек модуля (из БД):

Код
require_once($_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/main/include/prolog_admin_before.php');
$APPLICATION->SetTitle(("Отправка push уведомлений"));
require($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/main/include/prolog_admin_after.php");

if (!$USER->IsAdmin())
{
    return;
}

use Bitrix\Main\Loader;
use Bitrix\Main\Config\Option;

Loader::includeModule('mibazarow.pushsender');
use mibazarow\pushsender\GetFirebaseToken;

// Получить JSON-строку из настроек модуля
$jsonString = Option::get('mibazarow.pushsender', 'firebase_json');

// Из настроек модуля
$firebase = new GetFirebaseToken($jsonString);

// Получить токен
$token = $firebase->getAccessToken();

print_r($token);

require($_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/main/include/epilog_admin.php');
Михаил Базаров 07.06.2025
Пример отправки массового push уведомления
Код
require_once($_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_admin_before.php');
$APPLICATION->SetTitle(("Модуль mibazarow_pushsender_main"));
require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/prolog_admin_after.php");

use Bitrix\Main\Loader;
use Bitrix\Main\Config\Option;

Loader::includeModule('mibazarow.pushsender');

if (
    (!empty($_POST['PU_NAME'])) and
    (!empty($_POST['PU_MESS'])) and
    (!empty(bitrix_sessid_post()))
) {
    $jsonString = Option::get('mibazarow.pushsender', 'firibase_json');
    $token = \mibazarow\pushsender\FirebaseToken::getAccessToken($jsonString);

    // URL для отправки уведомлений
    $url = 'https://fcm.googleapis.com/v1/projects/ID ВАШЕГО ПРИЛОЖЕНИЯ/messages:send';

    $message = [
        "message" => [
            'topic' => 'main_topik',
            "notification" => [
                'title' => $_POST['PU_NAME'],
                'body' => $_POST['PU_MESS'],
            ]
        ]
    ];

    // Заголовки запроса
    $headers = [
        'Authorization: Bearer ' . $token, // Токен доступа
        'Content-Type: application/json'
    ];

    // Инициализация cURL
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($message));

    // Execute curl session
    $result = curl_exec($ch);
    if ($result === FALSE) {
        echo 'Ошибка: ' . curl_error($ch);
    }
//    echo '<pre>';
//    print_r($result);
//    echo '</pre>';
    curl_close($ch);
    echo 'Отправлено';

    // Закрытие cURL
    curl_close($ch);
}

Стоимость и сроки разработки сайтов и приложений

Окончательная стоимость и сроки разработки сайта формируются после обсуждения деталей на этапе заказа. Как правило, они редко выходят за обозначенные ниже рамки.

Интернет-магазин: индивидуальная разработка от 350 000 руб.
от 5-ти недель

Cоздание интернет-магазина на 1С-Битрикс. Разработка с нуля, оптимизация кода и производительности под конкретный проект и требования. Реализация любого функционала без ограничений готовых решений.

Интернет-магазин: на готовом решении от 60 000 руб.
от 7-ми дней

Готовое решение — вариант для тех, кто не хочет тратить много средств на индивидуальный проект, и не имеет серьезных требований к сайту. Магазин, быстро запускается на базе одного из 200-та готовых решений

Мобильное приложение от 400 000 руб.
от 1-го месяца

Разработка кроссплатформенного мобильного приложения, которое не уступает нативным решениям как в производительности, так и пользовательском опыте. Публикуется в AppStore, GooglePlay и RuStore

Опросник на разработку. После ознакомления, задам уточняющие вопросы и оценю проект по стоимости и срокам разработки.