💡 전체 작업 예제가 GitHub에 있습니다:
qr-sign-password-protected-pdf-python
소개
문서 서명이 필요하지만 암호화된 경우 대부분의 팀이 사용하는 세 단계 패턴이 있습니다: 문서를 복호화하고, 평문에 서명한 뒤, 다시 암호화합니다. 이 방법은 작동합니다. 하지만 몇 백 밀리초 동안 의도적으로 보호된 문서의 읽을 수 있는 복사본이 임시 디렉터리에 존재하게 되며, 감사 파이프라인에서는 그 창이 서명보다 더 큰 문제로 간주됩니다.
보호된 PDF에 서명하는 것은 .NET을 통해 Python에서 사용할 수 있는 GroupDocs.Signature 기능으로, 위 세 단계를 완전히 건너뛰고 바로 작업합니다: 비밀번호가 원본을 제자리에서 열고, 서명이 적용되며, 출력이 다시 보호된 상태로 저장됩니다. 이 문서는 네 가지 비밀번호 경로—작동하는 두 가지와 의도적으로 실패하는 두 가지—를 비교하고, 이 바인딩에 특화된 실패 계약을 다룹니다.
왜 이것이 중요한가
비밀번호 처리는 문서 파이프라인에서 누수가 발생하는 지점입니다. 보통 서명 라이브러리 자체가 아니라 그 주변 구조물—삭제되어야 할 임시 파일, 잘못된 비밀번호 오류를 무시하고 무한 재시도하는 예외 처리기, 수신자에게 알려지지 않은 비밀번호와 함께 전달된 서명된 복사본—에서 문제가 발생합니다.
세 경우 모두 근본 원인은 비밀번호를 작업 흐름에서 제외해야 할 대상이 아니라 작업의 일부로 취급하지 않았기 때문입니다. 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를 읽는 데 20분을 소비한 것이 이 섹션이 존재하는 이유입니다.
실제로 도착하는 것은 메시지가 Proxy error(<Name>): 로 시작하는 RuntimeError입니다. 이 접두사를 파싱하면 원인을 복구할 수 있습니다:
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
두 가지 활용 방법이 있습니다. 비밀번호가 사용자 폼에서 들어온 경우, 이 호출은 200개 문서 배치 중간에 발생하기보다 저렴한 호출로 자격 증명을 검증합니다. 또한 파이프라인이 평문을 전혀 저장할 수 없는 경우에도, 복호화 없이도 페이지 수(감사 로그용)와 크기(쿼터용)를 보고할 수 있게 해줍니다.
방법 비교: 언제 어떤 방법을 사용할까
| 방법 | 가장 적합한 경우 | 주요 장점 | 제한 사항 |
|---|---|---|---|
| 원본 비밀번호 유지 | 제자리 서명 파이프라인 | SaveOptions 불필요, 평문 파일 없음 | 수신자가 원본 비밀번호를 알아야 함 |
| 저장 시 새 비밀번호 적용 | 다른 파티에 인계 | 원본은 그대로, 복사본에 새 비밀번호 | SaveOptions 두 줄 필요, 하나만 설정하기 쉬움 |
| 비밀번호 없음 (실패) | 테스트에서 계약 검증 | 열기에 실패하고 아무 것도 쓰지 않음 | 서명 경로가 아님 |
| 잘못된 비밀번호 (실패) | 오래된 자격 증명 구분 | 별도 예외 이름 제공 | 서명 경로가 아님 |
재조회가 추가 호출만큼 가치가 있는가?
두 가지 이유로 그렇습니다. QrCodeVerifyOptions로 서명된 파일을 다시 열면 서명이 저장 과정에서 유지됐는지 확인할 수 있고, 재열기 시 비밀번호를 제공해야 하므로 출력이 실제로 여전히 암호화되어 있음을 증명합니다. 카운트가 0인 경우는 거의 항상 라이선스 문제이며, 서명 호출 자체는 실제 실패 시 예외를 발생시키므로, 0과 무음은 비라이선스 빌드를 가리킵니다.
전환 비용
구조적인 변화는 없습니다. 기존에 임시 파일로 복호화하고 있었다면, 해당 단계를 삭제하고 비밀번호를 LoadOptions로 이동시키며, 마지막에 재암호화 호출을 제거하면 됩니다—대체로 라인 수가 줄어듭니다. 서명 호출 자체는 형태가 변하지 않으며, 출력은 입력과 동일한 보호를 가진 바이트 단위 동일한 서명 PDF가 됩니다.
유의해야 할 한 곳은 정리 코드입니다. 복호화‑서명‑재암호화 파이프라인은 보통 finally 블록에서 임시 파일을 삭제합니다. 임시 파일이 사라지면 해당 블록은 존재하지 않는 경로를 삭제하려 시도하게 됩니다.
모범 사례
use_original_password를 특별히 회전시키려는 경우가 아니면 그대로 두세요; 기본값이 가장 안전합니다.- 프록시 이름을 한 번만 파싱하는 헬퍼를 만들고, 다른 곳에서는 그 결과를 기반으로 분기하세요.
- 배치를 시작하기 전에
get_document_info로 사용자 제공 비밀번호를 검증하면, 잘못된 자격 증명이 저렴한 호출 하나로 끝납니다. - 서명된 출력을 원본 경로에 덮어쓰지 마세요. 실수 시 원본을 복구할 수 있게 됩니다.
결론
비밀번호는 서명 전에 우회해야 할 장애물이 아니라 작업의 인수입니다. LoadOptions로 열고, SaveOptions로 출력 보호를 결정하고, 실패 시 프록시 이름을 파싱하며, 이후 비밀번호로 검증하십시오. 샘플은 네 가지 경로를 한 번에 실행하므로, 차이를 확인하려면 한 줄의 명령만 실행하면 됩니다.