💡 Pełny działający przykład dostępny na GitHub: qr-sign-password-protected-pdf-python

Wprowadzenie

Istnieje trzyetapowy schemat, którego większość zespołów używa, gdy dokument wymagający podpisu okazuje się zaszyfrowany: odszyfrować go, podpisać tekst jawny, ponownie zaszyfrować wynik. Działa. Oznacza to jednak, że przez kilka setek milisekund czytelna kopia celowo chronionego dokumentu istnieje w katalogu tymczasowym, a w audytowanym potoku to właśnie to okno jest wykryciem, a nie podpisem.

Podpisywanie chronionego PDF‑a jest funkcją GroupDocs.Signature dla Pythona poprzez .NET, która pomija te trzy kroki całkowicie: hasło otwiera źródło w miejscu, podpis jest nakładany, a wynik jest zapisywany ponownie chroniony. Ten artykuł porównuje cztery ścieżki hasła – dwie działające i dwie celowo nieudane – oraz omawia kontrakt błędów specyficzny dla tego powiązania.

Dlaczego to jest ważne

Obsługa haseł to miejsce, w którym wycieki pojawiają się w potokach dokumentów. Nie przez bibliotekę podpisującą, zwykle, ale przez otoczenie: plik tymczasowy, który miał zostać usunięty, obsługa wyjątków, która pochłonęła błąd nieprawidłowego hasła i próbowała w nieskończoność, podpisana kopia przekazana z hasłem, o którym odbiorca nigdy nie został poinformowany.

Wszystkie trzy przypadki mają tę samą przyczynę podstawową – hasło jest traktowane jako coś, co trzeba usunąć z drogi, a nie jako część operacji. LoadOptions i SaveOptions przywracają je do operacji.

Wymagania wstępne

Python 3 i groupdocs-signature-net==26.1, plus PDF z hasłem użytkownika. Bez licencji biblioteka działa w trybie ewaluacyjnym, który nadal podpisuje, ale dodaje własny tekst na stronie.

Instalacja

pip install groupdocs-signature-net==26.1

Metoda 1 – Zachowaj oryginalne hasło

Domyślna i wymagająca najmniej kodu. Hasło przekazywane jest przez LoadOptions, a SaveOptions w ogóle nie jest podawane:

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)

Brak SaveOptions wykonuje tutaj rzeczywistą pracę. use_original_password domyślnie ma wartość True, więc GroupDocs ponownie stosuje hasło źródłowe do podpisanego wyniku. Nie ma momentu, w którym istnieje niechroniona wersja, ani na dysku, ani w inny sposób, a len(result.succeeded) zwraca liczbę zapisanych podpisów.

Metoda 2 – Zmien hasło w kopii podpisanej

Gdy podpisany dokument trafia do innej strony, rozsądnym rozwiązaniem jest nadanie kopii własnych poświadczeń i pozostawienie źródła bez zmian:

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)

Obie linie SaveOptions są wymagane i to jest szczegół, który warto zapamiętać: ustawienie password przy pozostawieniu use_original_password w wartości domyślnej nie daje widocznego efektu. Flaga wygrywa, wynik zachowuje stare hasło i odkrywasz to, gdy odbiorca zgłasza, że przesłane hasło nie działa.

Metoda 3 i 4 – Dwie sytuacje niepowodzenia

Zaszyfrowany dokument reaguje inaczej na brak hasła i na błędne hasło, a różnicę warto obsłużyć.

Przy braku LoadOptions otwarcie się nie udaje i nic nie jest zapisywane:

try:
    with signature.Signature(source_path) as sign:
        sign.sign(output_path, options)
    return ""
except RuntimeError as error:
    return proxy_error_name(error)

Zwraca to PasswordRequiredException. Dostarczenie nieprawidłowego hasła zamiast tego zwraca IncorrectPasswordException. Jedno oznacza: poproś użytkownika o poświadczenie; drugie oznacza, że posiadane poświadczenie jest nieaktualne. Obsługa, która nie potrafi ich odróżnić, będzie ponawiać hasło, które nigdy nie zadziała.

Kontrakt błędów i dlaczego oczywisty kod się nie udaje

Oto część, która kosztuje popołudnie, jeśli nikt cię nie ostrzeże. Powiązanie udostępnia PasswordRequiredException, IncorrectPasswordException i GroupDocsSignatureException jako same nazwy, które nie dziedziczą po BaseException. Napisz intuicyjną obsługę:

except IncorrectPasswordException:
    ...

i Python podniesie TypeError: catching classes that do not inherit from BaseException is not allowed. Oryginalny błąd znika, zastąpiony jest tym, który wskazuje na twoją linię except, a nie na hasło. Napisałem dokładnie taką obsługę za pierwszym razem, a dwadzieścia minut spędzonych na czytaniu tego TypeError jest powodem, dla którego ta sekcja istnieje.

To, co faktycznie przychodzi, to RuntimeError, którego komunikat zaczyna się od Proxy error(<Name>): . Parsowanie tego prefiksu odzyskuje przyczynę:

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]

Rozgałęź się na podstawie zwróconej nazwy, a nie tekstu komunikatu, który zawiera ścieżki plików i zmienia się między uruchomieniami.

Inspekcja przed podpisaniem

Istnieje piąta ścieżka warta poznania, i nie zapisuje niczego. Otwarcie dokumentu z LoadOptions i wywołanie get_document_info zwraca format, liczbę stron i rozmiar, podczas gdy plik pozostaje zaszyfrowany na dysku:

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

Dwa zastosowania. Gdy hasło pochodzi z formularza użytkownika, to weryfikuje poświadczenie przy tanim wywołaniu, zamiast w połowie przetwarzania dwustu dokumentów. A gdy potok nie ma zezwolenia na przechowywanie tekstu jawnego, nadal pozwala temu potokowi raportować, co trzyma – liczbę stron dla dziennika audytu, rozmiary dla limitu – bez odszyfrowywania czegokolwiek.

Porównanie metod: Kiedy używać której

Metoda Najlepsze zastosowanie Kluczowe zalety Ograniczenia
Zachowaj oryginalne hasło potoki, które podpisują w miejscu brak SaveOptions, nic nie jest zapisywane w czystej postaci odbiorca potrzebuje hasła źródłowego
Zmien hasło przy zapisie przekazanie innej stronie źródło zachowuje swoje poświadczenie, kopia dostaje nowe dwie linie SaveOptions, łatwo pomylić i ustawić tylko jedną
Brak hasła (niepowodzenie) udowodnienie kontraktu w testach nie udaje się przy otwarciu, nic nie jest zapisywane nie jest ścieżką podpisywania
Błędne hasło (niepowodzenie) odróżnienie nieaktualnego poświadczenia odrębna nazwa wyjątku nie jest ścieżką podpisywania

Czy odczyt zwrotny jest warty dodatkowego wywołania?

Tak, z dwóch powodów. Ponowne otwarcie podpisanego pliku z QrCodeVerifyOptions dowodzi, że podpis przetrwał zapis, a ponieważ ponowne otwarcie wymaga podania hasła, dowodzi również, że wynik nadal jest zaszyfrowany. Zero wyników prawie zawsze wskazuje na problem z licencją, a nie na niepowodzenie podpisu – wywołanie sign podnosi wyjątek, gdy naprawdę się nie powiedzie, więc cisza plus zero dopasowań wskazuje na nielicencjonowaną wersję.

Co kosztuje przejście

Nic strukturalnego. Jeśli twój kod już odszyfrowuje do pliku tymczasowego, zmiana polega na usunięciu tego kroku, przeniesieniu hasła do LoadOptions i usunięciu wywołania ponownego szyfrowania na końcu – zazwyczaj netto mniej linii. Same wywołanie podpisu nie zmienia kształtu, a wynik jest bajt po bajcie podpisanym PDF‑em z taką samą ochroną, jaką miał przy wejściu.

Jedynym miejscem, które wymaga uwagi, jest kod czyszczący. Potok zbudowany wokół odszyfrowania‑podpisania‑ponownego szyfrowania zwykle ma blok finally, który usuwa plik tymczasowy, a gdy plik tymczasowy znika, ten blok próbuje usunąć ścieżkę, która już nie istnieje.

Najlepsze praktyki

  • Nie zmieniaj use_original_password, chyba że celowo rotujesz; domyślna wartość jest najbezpieczniejsza.
  • Parsuj nazwę proxy raz, w pomocniku, i rozgałęź się na niej wszędzie indziej.
  • Zweryfikuj hasło podane przez użytkownika przy pomocy get_document_info przed rozpoczęciem partii, aby złe poświadczenie kosztowało jedno tanie wywołanie zamiast przerwanego przetwarzania.
  • Nigdy nie zapisuj podpisanego wyniku nad ścieżką źródłową, aby w razie pomyłki oryginał pozostał odzyskiwalny.

Zakończenie

Hasło nie jest przeszkodą, którą trzeba obejść przed podpisaniem – jest argumentem operacji. Otwórz z LoadOptions, zdecyduj o ochronie wyjścia przy pomocy SaveOptions, parsuj nazwę proxy, gdy coś się nie uda, i zweryfikuj później przy użyciu hasła. Przykład uruchamia wszystkie cztery ścieżki jednocześnie, więc różnicę między nimi widać po jednym poleceniu, a nie po przeczytaniu akapitu.

Dodatkowe zasoby