💡 ตัวอย่างทำงานเต็มที่พร้อมใช้งานบน 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 เมื่อมีข้อผิดพลาด, และตรวจสอบด้วยรหัสผ่านหลังจากนั้น ตัวอย่างจะรันทุกเส้นทางสี่แบบในครั้งเดียว ดังนั้นความแตกต่างระหว่างพวกมันจะเห็นได้ด้วยคำสั่งเดียวแทนที่จะต้องอ่านย่อหน้าที่ยาว

แหล่งข้อมูลเพิ่มเติม