S3 в 1С: работа с объектным хранилищем из кода

S3 в 1С — это работа с объектным хранилищем прямо из кода конфигурации: сложить скан, вложение или выгрузку в бакет и забрать обратно по HTTP. Запрос «s3 1с» и «1с s3 хранилище» почти всегда про одно и то же — как из 1С programmatically положить двоичные данные в S3-совместимое хранилище и получить список объектов, не поднимая для этого сервер взаимодействия. Ниже — два рабочих способа с реальным кодом BSL: прямой вызов S3 REST с ручной подписью AWS SigV4 и вариант через локальный микросервис-компаньон.

Сразу разведём два похожих сценария, чтобы не искать код не в той статье. Если вам нужно хранилище файлов сервера взаимодействия (обсуждения, вложения в чатах 1С) — это настраивается не кодом, а SQL-скриптом в таблице storage_server: об этом отдельный разбор — сервер взаимодействия 1С + Amazon S3. Здесь же речь про программную работу с S3 из прикладного кода: ваша обработка сама кладёт и читает объекты бакета.

Два способа подключить S3 к 1С

S3 — это HTTP REST API поверх бакетов. Проблема одна: каждый запрос надо подписать по алгоритму AWS Signature Version 4, иначе облако ответит 403 SignatureDoesNotMatch. Отсюда две стратегии:

  • Прямой REST + SigV4 в BSL. 1С сама строит подпись и ходит в https://{bucket}.s3.amazonaws.com. Ноль внешних зависимостей, но подпись придётся реализовать побайтово.
  • Микросервис-компаньон. Рядом с 1С крутится маленький Python-сервис (с готовым SDK boto3), а 1С шлёт ему простой JSON. Подпись берёт на себя SDK, но появляется отдельный процесс.

Что выгоднее в вашем случае — зависит от платформы кластера 1С, требований к проду и сложности сценариев. Переключите вкладки и сравните по пунктам:

Способ 1. Прямой вызов S3 REST с подписью SigV4

Это «академический», но самый автономный вариант: никаких exe и портов, только штатное HTTPСоединение. Вся сложность — в правильном формировании подписи. Разберём её по шагам, а потом соберём в код.

Начинается всё с канонического запроса и строки для подписи. Порядок строк и заголовков здесь фиксирован спецификацией — расхождение на один символ ломает подпись:

&НаСервере
Функция СформироватьStringToSign(ТекущаяДата)

    short_date  = Формат(ТекущаяДата, "ДФ=yyyyMMdd");
    dateISO8601 = Формат(ТекущаяДата, "ДФ=yyyyMMddTHHmmssZ");

    // Хеш пустого тела: для GET без содержимого это константа SHA-256("")
    ХешТела = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";

    CanonicalRequest =
        "GET"                                  + Символы.ПС +
        "/"                                    + Символы.ПС +
        "prefix=" + EncodeURL(Папка + "/")     + Символы.ПС +   // canonical query
        "host:" + Bucket + ".s3.amazonaws.com" + Символы.ПС +   // canonical headers
        "x-amz-content-sha256:" + ХешТела      + Символы.ПС +
        "x-amz-date:" + dateISO8601            + Символы.ПС +
        ""                                     + Символы.ПС +
        "host;x-amz-content-sha256;x-amz-date" + Символы.ПС +   // signed headers
        ХешТела;

    StringToSign =
        "AWS4-HMAC-SHA256"                                    + Символы.ПС +
        dateISO8601                                          + Символы.ПС +
        short_date + "/" + Region + "/s3/aws4_request"       + Символы.ПС +
        SHA256Hex(CanonicalRequest);

    Возврат StringToSign;
КонецФункции

Функция SHA256Hex — это штатный SHA-256 с представлением результата в hex через фабрику XDTO (проверенный приём, работает на любой платформе):

&НаСервере
Функция SHA256Hex(Строка)
    Хеширование = Новый ХешированиеДанных(ХешФункция.SHA256);
    Хеширование.Добавить(Строка);

    ТипHex = ФабрикаXDTO.Тип("http://www.w3.org/2001/XMLSchema", "hexBinary");
    Возврат НРег(ФабрикаXDTO.Создать(ТипHex, Хеширование.ХешСумма).ЛексическоеЗначение);
КонецФункции

Дальше — самое тонкое место: вывод ключа подписи. Секретный ключ не подписывает строку напрямую. Из него четырьмя раундами HMAC-SHA256 выводится одноразовый kSigning, привязанный к дате, региону и сервису. В учебном исходнике это сделано через COM-объект Windows:

// ⚠️ COM-вариант — работает ТОЛЬКО на Windows-сервере 1С
Криптография = Новый COMОбъект("System.Security.Cryptography.HMACSHA256");
Текст        = Новый COMОбъект("System.Text.UTF8Encoding");

Криптография.Key = Текст.GetBytes_4("AWS4" + СекретныйКлюч);
kDate    = Криптография.ComputeHash_2(Текст.GetBytes_4(short_date));
Криптография.Key = kDate;
kRegion  = Криптография.ComputeHash_2(Текст.GetBytes_4(Region));
// ... и так далее до kSigning

Проблема очевидна: COM-объект System.Security.Cryptography есть только в Windows. На Linux-кластере 1С (а это большинство продовых серверов) такой код упадёт. Начиная с платформы 8.3.21 HMAC считается штатной глобальной функцией HMAC() — она кроссплатформенная, и именно её стоит использовать в новых проектах:

&НаСервере
Функция ВДвоичные(Стр)
    Возврат ПолучитьДвоичныеДанныеИзСтроки(Стр, "UTF-8", Ложь);
КонецФункции

&НаСервере
Функция ВHex(Данные)  // ДвоичныеДанные → hex-строка
    ТипHex = ФабрикаXDTO.Тип("http://www.w3.org/2001/XMLSchema", "hexBinary");
    Возврат НРег(ФабрикаXDTO.Создать(ТипHex, Данные).ЛексическоеЗначение);
КонецФункции

&НаСервере
Функция ПодписатьSigV4(СекретныйКлюч, short_date, StringToSign)
    // 64 — размер блока SHA-256; результат каждого HMAC становится ключом следующего
    kDate     = HMAC(ВДвоичные("AWS4" + СекретныйКлюч), ВДвоичные(short_date),   ХешФункция.SHA256, 64);
    kRegion   = HMAC(kDate,     ВДвоичные(Region),        ХешФункция.SHA256, 64);
    kService  = HMAC(kRegion,   ВДвоичные("s3"),          ХешФункция.SHA256, 64);
    kSigning  = HMAC(kService,  ВДвоичные("aws4_request"),ХешФункция.SHA256, 64);

    signature = HMAC(kSigning,  ВДвоичные(StringToSign),  ХешФункция.SHA256, 64);
    Возврат ВHex(signature);
КонецФункции

Если платформа старше 8.3.21 или вы хотите код, гарантированно переносимый на Linux, штатный HMAC() можно заменить собственной реализацией HMAC на БуферДвоичныхДанных — классический алгоритм с ipad/opad (0x36/0x5C) через ЗаписатьПобитовоеИсключительноеИли и двойное ХешированиеДанных. Это чистый BSL, без COM и без требований к версии; именно так сделано в зрелой продовой версии этого клиента. Внешне функция та же — принимает ключ и данные, возвращает `ДвоичныеДанные`, — поэтому цепочка вывода ключа из шага 3 не меняется.

Осталось собрать заголовок Authorization и отправить запрос. Обратите внимание: секретный ключ по сети не уходит — в заголовке только публичный AccessKeyId, область действия и подпись:

&НаСервере
Функция ПолучитьСписокФайлов()
    Соединение = Новый HTTPСоединение(Bucket + ".s3.amazonaws.com", , , , , 30);

    // x-amz-date ОБЯЗАНА быть в UTC — берём универсальное время, а не локальное
    ТекДата     = ТекущаяУниверсальнаяДата();
    short_date  = Формат(ТекДата, "ДФ=yyyyMMdd");
    dateISO8601 = Формат(ТекДата, "ДФ=yyyyMMddTHHmmssZ");
    ХешТела     = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";

    Signature = ПодписатьSigV4(СекретныйКлюч, short_date, СформироватьStringToSign(ТекДата));

    Authorization =
        "AWS4-HMAC-SHA256 " +
        "Credential=" + AccessKeyID + "/" + short_date + "/" + Region + "/s3/aws4_request," +
        "SignedHeaders=host;x-amz-content-sha256;x-amz-date," +
        "Signature=" + Signature;

    Заголовки = Новый Соответствие;
    Заголовки.Вставить("Authorization",        Authorization);
    Заголовки.Вставить("Host",                 Bucket + ".s3.amazonaws.com");
    Заголовки.Вставить("x-amz-date",           dateISO8601);
    Заголовки.Вставить("x-amz-content-sha256", ХешТела);

    Запрос = Новый HTTPЗапрос("/?prefix=" + Папка + "/", Заголовки);
    Ответ  = Соединение.ВызватьHTTPМетод("GET", Запрос);

    Если Ответ.КодСостояния = 200 Тогда
        Возврат Ответ.ПолучитьТелоКакСтроку();   // XML со списком объектов
    КонецЕсли;
    Возврат Неопределено;
КонецФункции

Для загрузки файла в бакет принцип тот же, но метод PUT, а в x-amz-content-sha256 кладётся уже реальный SHA-256 тела файла (не константа пустой строки), и тело передаётся как ДвоичныеДанные. Точно так же собираются HEAD (проверить существование объекта) и DELETE (удалить) — меняется только HTTP-метод в подписи и запросе, вся обвязка подписи общая. В продовом варианте эти четыре операции (Put/Get/Head/Delete) выносят в один переиспользуемый общий модуль, а ключи доступа и регион держат не в коде, а в регистре сведений с ограничением прав на чтение — тогда секрет не виден в конфигурации и не утекает в выгрузку.

Способ 2. Через локальный микросервис-компаньон

Если переписывать SigV4 на BSL не хочется, а сценарии сложнее «положил-забрал» (multipart-загрузка, пресайн-ссылки, ретраи) — подпись отдают готовому SDK. Рядом с 1С запускается маленький Python-сервис s3server на 127.0.0.1:9988, а 1С общается с ним простым JSON. 1С здесь — тонкий UI поверх внешнего сервиса:

&НаСервере
Функция ВыполнитьHTTPЗапрос(s3method, Метод = "POST", КлючиМетода = Неопределено)
    Попытка
        HTTPСоединение = Новый HTTPСоединение("127.0.0.1", 9988, , , , 10);

        HTTPЗапрос = Новый HTTPЗапрос();
        HTTPЗапрос.Заголовки.Вставить("Content-type", "application/json");
        Если Метод = "POST" Тогда
            HTTPЗапрос.УстановитьТелоИзСтроки(СформироватьТелоЗапроса(s3method, КлючиМетода));
        КонецЕсли;

        Возврат HTTPСоединение.ВызватьHTTPМетод(Метод, HTTPЗапрос);
    Исключение
        Возврат Неопределено;   // сервис не поднят
    КонецПопытки;
КонецФункции

&НаСервере
Функция СформироватьТелоЗапроса(s3method, КлючиМетода)
    Тело = Новый Соответствие;
    Тело.Вставить("s3method",    s3method);      // list_files | download_files | upload_file
    Тело.Вставить("access_key",  access_key);
    Тело.Вставить("secret_key",  secret_key);
    Тело.Вставить("region_name", region_name);
    Тело.Вставить("bucket_name", bucket_name);
    Тело.Вставить("folder_name", folder_name);
    Если ЗначениеЗаполнено(endpoint_url) Тогда
        Тело.Вставить("endpoint_url", endpoint_url);  // ← для S3-совместимых хранилищ
    КонецЕсли;

    Если НЕ КлючиМетода = Неопределено Тогда
        Для каждого Стр Из КлючиМетода Цикл
            Тело.Вставить(Стр.Ключ, Стр.Значение);
        КонецЦикла;
    КонецЕсли;

    Возврат ОбъектВJSON(Тело);
КонецФункции

Список файлов, массовая загрузка на диск и выгрузка файла — это просто вызовы с разным s3method. Сервис отвечает единым конвертом {"error": "...", "data": [...]}, который 1С разбирает обёрткой JSONВОбъект. Ключевое преимущество — параметр endpoint_url: подставив свой адрес, тот же код работает с MinIO, Yandex Object Storage и любым другим S3-совместимым хранилищем без единой правки логики подписи.

Сам сервис не обязан быть «большим». В рабочем варианте это две короткие вещи: класс на boto3, который делает всю работу с S3, и лёгкий HTTP-роутер на стандартной библиотеке Python (http.server) — без FastAPI и прочих фреймворков. Вот ядро на boto3, где вся подпись SigV4 берётся из SDK:

from boto3.session import Session

class AmazonS3:
    def __init__(self, message):
        session = Session(
            aws_access_key_id=message['access_key'],
            aws_secret_access_key=message['secret_key'],
            region_name=message['region_name'],
        )
        endpoint = message.get('endpoint_url')      # ← MinIO / Yandex / VK Cloud
        self.s3 = (session.resource('s3', endpoint_url=endpoint)
                   if endpoint else session.resource('s3'))
        self.bucket_name = message['bucket_name']

    def list_files(self):
        bucket = self.s3.Bucket(self.bucket_name)
        return [{'filename': o.key} for o in bucket.objects.all()]

    def upload_file(self, full_name, key):
        self.s3.Bucket(self.bucket_name).upload_file(full_name, key)

А роутер — это буквально BaseHTTPRequestHandler: GET возвращает версию сервиса (по нему 1С проверяет, что он жив), POST принимает JSON, вызывает нужный метод класса и отдаёт конверт {data, error}. Порт 9988, ноль внешних веб-зависимостей.

Запуск компаньона в Docker вместо exe

Раньше такой сервис собирали в один s3server.exe через PyInstaller и запускали рядом с 1С. Минусы очевидны: отдельный бинарник, который надо распаковать и держать живым, и регулярные ложные срабатывания антивируса на самодельный exe. Docker снимает обе проблемы — сервис едет как обычный контейнер:

# Dockerfile
FROM python:3.8
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt /app/
RUN pip install -r requirements.txt   # boto3, botocore, yandexcloud
COPY . /app/
# docker-compose.yml
services:
  web:
    build: .
    restart: always
    command: python s3server.py
    ports:
      - "9988:9988"

docker compose up — и компаньон слушает тот же 127.0.0.1:9988, а restart: always поднимет его после перезагрузки. Для 1С со стороны кода ничего не меняется: она по-прежнему шлёт JSON на порт 9988. Это тот же приём, что мы применяем для публикации Python-сервисов рядом с 1С в контейнере, — как микросервис на FastAPI для 1С.

Написать и упаковать такой сервис-компаньон — работа на вечер, и это ровно тот навык связки 1С с Python-бэкендом, который мы разбираем на курсе «Python для 1С‑разработчика»: как вынести тяжёлую или «неудобную для BSL» логику в микросервис и аккуратно ходить в него из 1С.

S3-совместимые хранилища: MinIO, Yandex Object Storage, VK Cloud

С 2022 года прямой доступ к Amazon для российской аудитории 1С стал менее удобным, но это почти не важно: S3 давно стал стандартом де-факто, и российские облака его поддерживают. Практически весь код выше остаётся тем же — меняются только эндпоинт и ключи:

  • Yandex Object Storage — эндпоинт storage.yandexcloud.net, регион ru-central1; статический ключ доступа выдаёт сервисный аккаунт в консоли Yandex Cloud.
  • VK Cloud / Selectel — свой эндпоинт и ключи из панели управления; в подписи меняются host и регион.
  • MinIO — self-hosted S3 у себя на сервере: удобно для тестов и закрытого контура, эндпоинт указывает на ваш адрес.

Для варианта с прямой подписью подставляйте нужный host и регион в canonical headers и в scope. Для микросервиса — просто заполняйте endpoint_url. Если вы переносите в облако не сами файлы, а сервер взаимодействия целиком (с историей сообщений) — это отдельная процедура: переезд сервера взаимодействия 1С в Yandex Cloud.

Подводные камни, на которых теряют часы

  • Время должно быть в UTC. Заголовок x-amz-date — по Гринвичу. Берите ТекущаяУниверсальнаяДата(), а не ТекущаяДата(). Если часы сервера разошлись с S3 больше чем на 15 минут — получите 403 RequestTimeTooSkewed даже при абсолютно верной подписи.
  • Хеш тела обязателен. x-amz-content-sha256 для запроса без тела — это SHA-256 пустой строки (e3b0c442…), а для PUT — реальный хеш содержимого. Тот же хеш идёт и в конец канонического запроса.
  • COM ≠ прод. Подпись через System.Security.Cryptography привязывает вас к Windows. На Linux-кластере используйте штатный HMAC() (8.3.21+) — код выше.
  • Ключи — не в код. В учебных примерах secret_key лежит в открытой константе. В бою секрет храните в защищённом хранилище (безопасное хранилище данных, отдельная константа с ограничением прав), а доступ к обработке — по роли. Утёкший Secret Access Key = полный доступ к бакету.
  • URL-кодирование по правилам AWS. Канонический запрос кодирует query по своим правилам (пробел, спецсимволы) — «почти как обычный encodeURL, но не совсем». Расхождение здесь — самая частая причина SignatureDoesNotMatch.

Что выбрать

Нужен автономный серверный код без внешних процессов — регламентная выгрузка, бэкап, работа внутри одного контура — берите прямую подпись SigV4 (на Linux — со штатным HMAC). Нужна скорость разработки и богатая логика S3 — берите микросервис-компаньон и держите S3-специфику в Python. Оба подхода — это, по сути, одна и та же связка «1С ↔ внешний REST», и именно её мы ставим на поток на курсе «Python для 1С‑разработчика»: от HTTP-клиента до собственного микросервиса рядом с 1С.

Частые вопросы