💡 Полный рабочий пример доступен на GitHub:
qr-sign-password-protected-pdf-python
Введение
Существует трёхшаговый шаблон, к которому прибегают большинство команд, когда документ, требующий подписи, оказывается зашифрованным: расшифровать его, подписать открытый текст, заново зашифровать результат. Это работает. Но это также означает, что в течение нескольких сотен миллисекунд в временном каталоге существует читаемая копия намеренно защищённого документа, и в проверяемом конвейере именно это окно считается уязвимостью, а не подписью.
Подписание защищённого PDF — это возможность GroupDocs.Signature для Python через .NET, которая полностью исключает эти три шага: пароль открывает исходный файл «на месте», подпись применяется, а результат записывается обратно в защищённом виде. В этой статье сравниваются четыре пути работы с паролем — два работающих и два преднамеренно неудачных — и рассматривается контракт ошибок, специфичный для этой привязки.
Почему это важно
Обработка паролей — место, где в конвейерах документов происходят утечки. Обычно не через библиотеку подписи, а через обёртку вокруг неё: временный файл, который должен был быть удалён, обработчик исключений, «проглатывающий» ошибку неверного пароля и бесконечно повторяющий попытку, подписанная копия, переданная с паролем, о котором получатель никогда не был проинформирован.
Во всех трёх случаях одна и та же коренная причина — пароль рассматривается как нечто, от чего нужно избавиться, а не как часть операции. LoadOptions и SaveOptions возвращают его в процесс.
Требования
Python 3 и groupdocs-signature-net==26.1, а также PDF с пользовательским паролем. Без лицензии библиотека работает в режиме оценки, который всё равно подписывает, но добавляет собственный текст на страницу.
Установка
pip install groupdocs-signature-net==26.1
Метод 1 — Сохранить оригинальный пароль
По умолчанию и с наименьшим объёмом кода. Пароль передаётся через LoadOptions, а SaveOptions вовсе не используется:
load_options = LoadOptions()
load_options.password = password
options = _build_qr_options(qr_text)
with signature.Signature(source_path, load_options) as sign:
result = sign.sign(output_path, options)
return len(result.succeeded)
Отсутствие SaveOptions здесь и есть реальная работа. use_original_password по умолчанию True, поэтому GroupDocs повторно применяет исходный пароль к подписанному выводу. Не существует момента, когда существует незашифрованная версия, ни на диске, ни где‑либо ещё, а len(result.succeeded) сообщает, сколько подписей было записано.
Метод 2 — Переключить пароль подписанной копии
Когда подписанный документ передаётся другой стороне, разумным шагом является дать копии собственные учётные данные, оставив источник без изменений:
save_options = SaveOptions()
save_options.password = new_password
save_options.use_original_password = False
with signature.Signature(source_path, load_options) as sign:
result = sign.sign(output_path, options, save_options)
return len(result.succeeded)
Обе строки SaveOptions обязательны, и это деталь, которую стоит запомнить: установка password при оставленном по умолчанию use_original_password ничего не меняет. Флаг выигрывает, вывод сохраняет старый пароль, и вы обнаружите это, когда получатель сообщит, что отправленный вами пароль не работает.
Метод 3 и 4 — Два сбоя
Зашифрованный документ реагирует по‑разному на отсутствие пароля и на неверный пароль, и эту разницу стоит обрабатывать.
При полном отсутствии LoadOptions открытие не удаётся и ничего не записывается:
try:
with signature.Signature(source_path) as sign:
sign.sign(output_path, options)
return ""
except RuntimeError as error:
return proxy_error_name(error)
Это возвращает PasswordRequiredException. Если вместо этого передать неверный пароль, тот же код вернёт IncorrectPasswordException. Первый случай — запросить у пользователя учётные данные; второй — ваши данные устарели. Обработчик, который не может их различить, будет бесконечно пытаться использовать пароль, который никогда не сработает.
Договор о сбоях и почему очевидный код ломается
Вот часть, которая может отнять целый день, если никто вас не предупредит. Привязка раскрывает PasswordRequiredException, IncorrectPasswordException и GroupDocsSignatureException как «голые» имена, не наследующие BaseException. Пишете интуитивный обработчик:
except IncorrectPasswordException:
...
и Python бросает TypeError: catching classes that do not inherit from BaseException is not allowed. Исходная ошибка исчезает, заменяясь ошибкой, указывающей на вашу строку except, а не на пароль. Я написал именно такой обработчик в первый раз, и двадцать минут, потраченных на чтение TypeError, — причина существования этого раздела.
На самом деле приходит RuntimeError, сообщение которого начинается с Proxy error(<Name>): . Парсинг этого префикса восстанавливает причину:
message = str(error)
marker = "Proxy error("
if not message.startswith(marker):
return ""
start = len(marker)
end = message.find(")", start)
if end < 0:
return ""
return message[start:end]
Разветвляйтесь по возвращённому имени, а не по тексту сообщения, который содержит пути к файлам и меняется от запуска к запуску.
Проверка перед подписью
Существует пятый путь, о котором стоит знать, и он ничего не записывает. Открытие документа с LoadOptions и вызов get_document_info возвращают формат, количество страниц и размер, пока файл остаётся зашифрованным на диске:
with signature.Signature(source_path, load_options) as sign:
info = sign.get_document_info()
return info.file_type.file_format, info.page_count, info.size
Два применения. Когда пароль поступает из пользовательской формы, это позволяет быстро проверить учётные данные, не дожидаясь середины обработки сотни документов. И когда конвейер не имеет права хранить открытый текст вообще, он всё равно может сообщать, что у него есть — количество страниц для аудита, размеры для квоты — без расшифровки.
Сравнение методов: когда использовать каждый
| Метод | Для чего лучше | Ключевые преимущества | Ограничения |
|---|---|---|---|
| Сохранить оригинальный пароль | конвейеры, которые подписывают на месте | без SaveOptions, ничего не записывается в открытом виде |
получателю нужен исходный пароль |
| Переключить пароль при сохранении | передача другой стороне | источник сохраняет свои учётные данные, копия получает новые | две строки SaveOptions, легко забыть одну |
| Без пароля (сбой) | проверка контракта в тестах | ошибка при открытии, ничего не записывается | не путь подписи |
| Неверный пароль (сбой) | различие устаревшего пароля | отдельное имя исключения | не путь подписи |
Стоит ли выполнять повторное чтение, учитывая дополнительный вызов?
Да, по двум причинам. Повторное открытие подписанного файла с QrCodeVerifyOptions подтверждает, что подпись выжила после сохранения, а поскольку при повторном открытии нужно указать пароль, это также доказывает, что вывод действительно остаётся зашифрованным. Нулевой счёт почти всегда указывает на проблему лицензии, а не на сбой подписи — вызов sign бросает исключение при реальном сбое, поэтому отсутствие ошибок плюс ноль совпадений указывает на нелицензированную сборку.
Что стоит переключения
Никаких структурных изменений. Если ваш код уже расшифровывает во временный файл, достаточно удалить этот шаг, перенести пароль в LoadOptions и убрать вызов повторного шифрования в конце — обычно это чистая экономия строк. Сам вызов подписи не меняет форму, а вывод представляет собой байтово‑идентичный подписанный PDF с тем же уровнем защиты, что и исходный.
Единственное, на что стоит обратить внимание, — код очистки. Конвейер, построенный вокруг «расшифровать → подписать → зашифровать», обычно имеет блок finally, который удаляет временный файл; как только временного файла больше нет, этот блок пытается удалить несуществующий путь.
Лучшие практики
- Оставляйте
use_original_passwordбез изменений, если только не планируете намеренно менять пароль; значение по умолчанию безопасно. - Парсите имя прокси один раз в вспомогательной функции и используйте его во всех ветвях.
- Проверяйте пользовательский пароль с помощью
get_document_infoперед запуском пакета, чтобы плохие учётные данные стоили одного дешевого вызова, а не половины прерванного процесса. - Никогда не записывайте подписанный вывод поверх исходного пути, чтобы ошибка не привела к потере оригинала.
Заключение
Пароль — не препятствие, которое нужно обходить перед подписью, а аргумент операции. Откройте документ через LoadOptions, решите, какую защиту применить с помощью SaveOptions, парсите имя прокси при ошибке и проверяйте результат через пароль после подписи. Пример проходит все четыре пути за один запуск, так что различия видны одной командой, а не абзацем текста.