一、结论先给
PDF 有且只有两种密码,很多人用错了一辈子。
| 用户密码(user password) | 所有者密码(owner password) | |
|---|---|---|
| 也叫 | 打开密码、文档密码 | 权限密码、主密码 |
| 作用 | 打开就要输,不输打不开 | 打开不需要,改权限才需要 |
| 防的是 | 陌生人打开 | 陌生人修改/打印/复制 |
| 强度 | 真防 | 只是君子协定 |
最关键的一句话:只设"禁止打印/禁止复制"而不设打开密码,等于没防。权限位是写在文件里的一段声明,阅读器选择遵守而已——不遵守的软件直接忽略,一秒解锁全部权限。
所以真正的用法是:
要防外传 → 设打开密码(user password)+ AES-256
要防改 → 设权限密码(owner password)
要溯源 → 铺可见水印(姓名/工号/时间)
要威慑 → 水印 + 打开密码 一起上
二、加密:qpdf 一条命令搞定
sudo apt install qpdf
2.1 基础用法
# 设打开密码(256 位 AES)
qpdf --encrypt "打开密码" "" 256 -- input.pdf out.pdf
# ↑ ↑
# 用户密码 所有者密码(留空=随机生成)
# 两个都设(推荐)
qpdf --encrypt "user123" "owner456" 256 -- input.pdf out.pdf
# 只设权限密码(不设打开密码,打开即看)
qpdf --encrypt "" "owner456" 256 -- input.pdf out.pdf
中间那个 -- 不能省,否则以 - 开头的文件名会被当成参数。
2.2 权限位:细粒度控制
qpdf --encrypt "user123" "owner456" 256 \
--print=full \
--modify=none \
--extract=n \
--annotate=n \
-- input.pdf out.pdf
| 参数 | 可选值 | 含义 |
|---|---|---|
--print |
full / low / none |
高质量打印 / 低质量打印 / 禁止打印 |
--modify |
all / annotate / form / none |
允许改 / 只填表单 / 禁止改 |
--extract |
y / n |
能否复制文字、抽图 |
--annotate |
y / n |
能否加批注 |
--assemble |
y / n |
能否增删页 |
--form |
y / n |
能否填表单 |
--accessibility |
y / n |
屏幕阅读器能否读(建议 y,关了影响无障碍) |
2.3 加密强度怎么选
| 参数 | 算法 | 兼容性 | 建议 |
|---|---|---|---|
40 |
RC4 40-bit | 万能,包括很老的阅读器 | 别用,可被暴力秒破 |
128 |
RC4 128-bit | 很好 | 一般场景够 |
256 |
AES-256 | 现代阅读器都支持(PDF 1.7+) | 默认选这个 |
# 完整推荐写法
qpdf --encrypt "强密码" "owner密码" 256 \
--print=low --modify=none --extract=n --accessibility=y \
--input.pdf out.pdf
2.4 校验加密结果
# 看是否加密、用的什么算法
pdfinfo out.pdf | grep -i -E "encrypted|security"
# 详细看权限(qpdf 独有)
qpdf --show-encryption out.pdf
典型输出:
R = 6
P = -1852
User password = required
extract for accessibility: allowed
extract for any purpose: not allowed
print low resolution: allowed
print high resolution: not allowed
modify document: not allowed
R = 6 表示 AES-256(PDF 2.0 的加密修订版本),R = 5 也是 AES-256,R = 4 是 RC4 128-bit。
三、加水印:三种铺法
3.1 pdftk:最快的方案
sudo apt install pdftk-java # 新版本是 java 实现
先做一张水印图(透明底 PNG):
# 用 ImageMagick 生成半透明斜向文字水印
magick -size 800x600 xc:none \
-font "$(fc-match -f '%{file}' 'WenQuanYi Zen Hei')" \
-pointsize 48 -fill 'rgba(0,0,0,0.18)' \
-draw "rotate -30 text 80,400 '机密 · 仅供内部使用'" \
watermark.png
三种铺法:
# 1) 单层水印(只盖在第一页)—— 封面场景
pdftk input.pdf stamp watermark.pdf output out.pdf
# 2) 多层水印(每页都盖)—— 最常用
pdftk input.pdf multistamp watermark.pdf output out.pdf
# 3) 背景水印(盖在内容下面,不遮字)
pdftk input.pdf background watermark.pdf output out.pdf
| 子命令 | 位置 | 覆盖 |
|---|---|---|
stamp |
内容之上 | 仅第一页 |
multistamp |
内容之上 | 每一页 |
background |
内容之下 | 每一页 |
选择建议:要显眼的用 multistamp(比如"作废""样例"),要低调的用 background(比如公司名底纹)。
3.2 pikepdf:精细控制(Python)
from pikepdf import Pdf, Page, Rectangle
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
import io
# 注册中文字体(系统有的话)
pdfmetrics.registerFont(TTFont('WQY', '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc'))
def make_watermark(text, pagesize=A4):
buf = io.BytesIO()
c = canvas.Canvas(buf, pagesize=pagesize)
w, h = pagesize
c.saveState()
c.setFont('WQY', 32)
c.setFillColorRGB(0, 0, 0, alpha=0.15)
c.translate(w / 2, h / 2)
c.rotate(30)
# 平铺多行多列
for row in range(-4, 5):
for col in range(-3, 4):
c.drawCentredString(col * 260, row * 150, text)
c.restoreState()
c.showPage()
c.save()
buf.seek(0)
return Pdf.open(buf)
wm = make_watermark('机密 内部资料')
src = Pdf.open('input.pdf')
for page in src.pages:
# as_xobject 会复用同一份对象,不会把文件撑大
page.add_overlay(wm.pages[0].as_xobject())
src.save('out.pdf')
as_xobject() 是关键:直接 add_overlay(wm.pages[0]) 会给每页嵌一份完整水印对象,100 页的文件会涨好几 MB;用 xobject 则只存一次。
3.3 用现成工具
命令行强大但要装环境,偶尔用一次不值当。浏览器里做也行,PDF 加密加水印工具 支持设密码 + 铺文字水印,文件不出本机——对要外发又含敏感信息的 PDF,这个"不上传"本身就是一半的安全性。
四、五个真实误区
误区 1:设了"禁止打印"就安全了
权限位是声明,不是加密。大量工具(包括 qpdf --decrypt)在有权限密码的情况下能解开,没有权限密码时更是直接忽略。真正防打开只能用 user password。
误区 2:加水印能防截图
不能。水印的作用是溯源和威慑——对方知道截图里带着他的名字/工号,动手前要掂量。技术上一张截图就绕过了。
误区 3:密码设得简单没关系
PDF 密码可以被离线暴力破解(hashcat 有专门的模式),40-bit RC4 在现代 GPU 上是秒级。所以:
- 一定要用 256 位 AES
- 密码至少 12 位,混合大小写数字符号
- 别用公司名+年份这类可猜的组合
可以用 哈希计算 生成一段随机串当密码(比如取 SHA256 的前 16 位)。
误区 4:加密和压缩能同时一次搞定
顺序很重要:先压缩,再加密。加密后的 PDF 内容流是密文,Ghostscript 之类的工具读不出图像,压不动。
gs -sDEVICE=pdfwrite -dPDFSETTINGS=/ebook -sOutputFile=small.pdf input.pdf
qpdf --encrypt "pwd" "owner" 256 -- small.pdf final.pdf
误区 5:加密后元数据还留着敏感信息
文件名改了,但 PDF 的元信息里可能还写着作者真名、公司名、原始路径。
# 看元数据
pdfinfo out.pdf
exiftool out.pdf
# 清元数据
exiftool -all= -overwrite_original out.pdf
qpdf --linearize out.pdf clean.pdf # qpdf 也会顺带丢弃部分元数据
五、解开自己文档的密码
仅限你自己拥有、忘记了密码的文档。 别人加密的文件不在讨论范围内。
# 知道打开密码,去掉密码方便自己用
qpdf --decrypt --password='你的密码' input.pdf plain.pdf
# 知道权限密码,解除权限限制
qpdf --decrypt --password='owner密码' input.pdf plain.pdf
# 批量
for f in *.pdf; do
qpdf --decrypt --password='pwd' "$f" "plain-${f}" 2>/dev/null || echo "跳过(密码不符): $f"
done
六、一条完整的外发流程
给客户发一份"能看、不能随便改、能溯源"的 PDF:
#!/usr/bin/env bash
set -euo pipefail
IN=$1; NAME=$2
OUT="out/${IN%.pdf}-外发.pdf"
mkdir -p out
# 1) 压缩(先压,后面加密了就压不动)
gs -sDEVICE=pdfwrite -dPDFSETTINGS=/ebook -dNOPAUSE -dBATCH -dQUIET \
-sOutputFile=/tmp/small.pdf "$IN"
# 2) 生成带客户名的水印(溯源)
magick -size 800x600 xc:none -pointsize 40 \
-fill 'rgba(0,0,0,0.16)' \
-draw "rotate -30 text 100,420 '仅提供给 ${NAME}'" /tmp/wm.png
cairosvg /dev/null 2>/dev/null || true
magick /tmp/wm.png /tmp/wm.pdf
# 3) 铺水印(每页,内容之上)
pdftk /tmp/small.pdf multistamp /tmp/wm.pdf output /tmp/wm.pdf.out.pdf
# 4) 加密(打开密码 + AES-256 + 低质量打印 + 禁改)
qpdf --encrypt "打开密码" "owner密码" 256 \
--print=low --modify=none --extract=n --accessibility=y \
-- /tmp/wm.pdf.out.pdf "$OUT"
# 5) 清元数据
exiftool -all= -overwrite_original "$OUT" 2>/dev/null || true
# 6) 校验
echo "页数: $(pdfinfo "$OUT" | awk '/^Pages/{print $2}')"
echo "大小: $(du -h "$OUT" | cut -f1)"
qpdf --show-encryption "$OUT" | head -3
七、几条纪律
- 要防打开就一定要设 user password,只设权限等于没防。
- 一律用 256 位 AES,40/128 位的 RC4 已经不够看。
- 先压缩再加密,顺序反了就压不动。
- 水印用
as_xobject()复用,别每页嵌一份。 - 发之前清元数据(exiftool -all=)。
- 密码要长且随机,别用公司名+年份。
- 水印是威慑不是技术防护,别指望它挡住截图。