💡 ตัวอย่างทำงานเต็มที่พร้อมใช้งานบน GitHub:
qr-sign-password-protected-pdf-python
บทนำ
มีรูปแบบสามขั้นตอนที่ทีมส่วนใหญ่ใช้เมื่อเอกสารที่ต้องการลงนามถูกเข้ารหัส: ถอดรหัส, ลงนามข้อความที่เป็น plaintext, แล้วเข้ารหัสผลลัพธ์ใหม่ มันทำงานได้ แต่ก็หมายความว่าเป็นเวลาหลายร้อยมิลลิวินาทีที่สำเนาที่อ่านได้ของเอกสารที่ตั้งใจจะป้องกันอยู่ในไดเรกทอรีชั่วคราว และในสายงานที่ตรวจสอบได้ หน้าต่างเวลานั้นคือการค้นพบมากกว่าลายเซ็น
การลงนาม 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 เป็นค่าเริ่มต้นจะไม่ทำให้เห็นผลอะไรเลย ธง 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
มีสองการใช้ประโยชน์จากมัน เมื่อรหัสผ่านมาจากฟอร์มผู้ใช้ สิ่งนี้จะตรวจสอบข้อมูลรับรองด้วยการเรียกที่ประหยัดแทนที่จะทำในระหว่างการประมวลผลเอกสารสองร้อยฉบับ และเมื่อสายงานไม่อนุญาตให้เก็บ plaintext เลย มันยังคงให้สายงานรายงานสิ่งที่กำลังถืออยู่ — จำนวนหน้าเพื่อบันทึกการตรวจสอบ, ขนาดเพื่อโควต้า — โดยไม่ต้องถอดรหัสใด ๆ
เปรียบเทียบวิธีการ: เมื่อใดควรใช้แต่ละวิธี
| วิธี | เหมาะสำหรับ | ข้อได้เปรียบหลัก | ข้อจำกัด |
|---|---|---|---|
| รักษารหัสผ่านเดิม | สายงานที่ลงนามในที่เดียว | ไม่ต้องใช้ SaveOptions, ไม่เขียนข้อมูลเป็น plaintext | ผู้รับต้องใช้รหัสผ่านต้นฉบับ |
| ตั้งรหัสใหม่เมื่อบันทึก | ส่งต่อให้ฝ่ายอื่น | ต้นฉบับยังคงใช้รหัสของตน, สำเนาได้รหัสใหม่ | ต้องใช้สองบรรทัดของ SaveOptions, ง่ายที่จะตั้งค่าแค่บรรทัดเดียวโดยบังเอิญ |
| ไม่มีรหัสผ่าน (ล้มเหลว) | พิสูจน์สัญญาในเทสต์ | ล้มเหลวตอนเปิด, ไม่เขียนอะไร | ไม่ใช่เส้นทางการลงนาม |
| รหัสผ่านผิด (ล้มเหลว) | แยกแยะข้อมูลรับรองที่ล้าสมัย | ชื่อข้อยกเว้นแยกต่างหาก | ไม่ใช่เส้นทางการลงนาม |
การอ่านผลลัพธ์กลับมาคุ้มค่ากับการเรียกเพิ่มหรือไม่?
ใช่, ด้วยสองเหตุผล การเปิดไฟล์ที่ลงนามใหม่ด้วย QrCodeVerifyOptions ยืนยันว่าลายเซ็นยังคงอยู่หลังการบันทึก และเพราะการเปิดใหม่ต้องระบุรหัสผ่าน มันยังยืนยันว่าผลลัพธ์ยังคงถูกเข้ารหัสจริง ๆ จำนวนศูนย์มักบ่งชี้ปัญหาลิขสิทธิ์มากกว่าการล้มเหลวของการลงนาม — คำสั่ง sign จะโยนข้อยกเว้นเมื่อมันล้มเหลวจริง ๆ ดังนั้นการเงียบและจำนวนศูนย์บ่งบอกว่าเป็นการสร้างที่ไม่มีลิขสิทธิ์
ค่าใช้จ่ายในการเปลี่ยนแปลง
ไม่มีโครงสร้างใด ๆ หากโค้ดของคุณเคยถอดรหัสไปยังไฟล์ชั่วคราว การเปลี่ยนแปลงคือการลบขั้นตอนนั้น, ย้ายรหัสผ่านเข้าไปใน LoadOptions, และลบการเรียก re-encrypt ที่ส่วนท้าย — ปกติจะทำให้บรรทัดลดลง การเรียกลงนามเองไม่เปลี่ยนรูปแบบ, และผลลัพธ์เป็น PDF ที่ลงนามโดยไบต์ต่อไบต์เหมือนกับไฟล์ต้นฉบับที่มีการป้องกันเดียวกัน
จุดเดียวที่ต้องตรวจสอบอย่างระมัดระวังคือโค้ดทำความสะอาด สายงานที่สร้างขึ้นรอบการถอดรหัส-ลงนาม-เข้ารหัสใหม่มักมีบล็อก finally ที่ลบไฟล์ชั่วคราว และเมื่อไฟล์ชั่วคราวหายไป บล็อกนั้นจะพยายามลบพาธที่ไม่มีอยู่แล้ว
แนวปฏิบัติที่ดีที่สุด
- อย่าแก้ไข
use_original_passwordเว้นแต่คุณต้องการเปลี่ยนรหัสโดยเจตนา; ค่าเริ่มต้นคือค่าที่ปลอดภัยที่สุด - แยกการแยกชื่อ proxy ออกเป็นฟังก์ชันช่วยเหลือหนึ่งครั้ง แล้วใช้ผลลัพธ์นั้นในการตัดสินใจทุกที่
- ตรวจสอบรหัสผ่านที่ผู้ใช้ป้อนด้วย
get_document_infoก่อนเริ่มแบช, เพื่อให้ข้อมูลรับรองที่ไม่ถูกต้องเสียค่าใช้จ่ายเพียงการเรียกที่ประหยัดแทนการรันครึ่งหนึ่งของงาน - อย่าเขียนผลลัพธ์ที่ลงนามทับพาธต้นฉบับ, เพื่อให้ความผิดพลาดไม่ทำให้ไฟล์ต้นฉบับสูญหาย
สรุป
รหัสผ่านไม่ใช่อุปสรรคที่ต้องหลีกเลี่ยงก่อนการลงนาม — มันเป็นอาร์กิวเมนต์ของการดำเนินการ เปิดไฟล์ด้วย LoadOptions, กำหนดการป้องกันผลลัพธ์ด้วย SaveOptions, แยกชื่อ proxy เมื่อมีข้อผิดพลาด, และตรวจสอบด้วยรหัสผ่านหลังจากนั้น ตัวอย่างจะรันทุกเส้นทางสี่แบบในครั้งเดียว ดังนั้นความแตกต่างระหว่างพวกมันจะเห็นได้ด้วยคำสั่งเดียวแทนที่จะต้องอ่านย่อหน้าที่ยาว