PHP动态水印防截图需服务端实时生成,用GD叠加含会话ID的斜向浅灰水印;隐写仅适用于PNG/WebP,推荐Imagick实现LSB嵌入;必须绕过CDN缓存,URL带会话唯一参数确保字节级差异。

PHP 动态水印防截图:别依赖纯前端,得在服务端实时生成图片
截图防不住,但让每张图都带唯一标识,能追溯泄露源头。PHP 本身不拦截图,只能确保用户看到的图是「刚画出来」的、带会话/用户 ID 的水印——不是静态图上盖个固定 logo。
- 用
imagecopymerge()或imagettftext()在输出前实时叠加水印,避免缓存图被复用 - 水印文字建议含短时序戳(如
date('Hi') . substr(session_id(), 0, 4)),防止同一张图多次下载得到完全相同像素 - 别把水印画在图片边缘或固定位置——截图工具常自动裁白边,优先选对角线浅灰斜字(透明度 20%~30%,角度 -25°)
- GD 库默认不支持中文路径字体,用
mb_convert_encoding($font_path, 'GB2312', 'UTF-8')转码,否则imagettftext()返回警告且无文字
PHP 嵌入不可见数字指纹:LSB 隐写可行,但别对 JPEG 用
所谓“不可见”指纹,本质是改像素最低有效位(LSB),人眼难察觉,但需图像格式配合。JPEG 有损压缩会直接抹掉 LSB 数据,实测 90% 以上概率失效;PNG 或无损 WebP 才靠谱。
- 只对 PNG 图操作:用
imagecreatefrompng()读取,imagesetpixel()改单个像素的 alpha 或蓝通道 LSB,再imagepng()输出 - 指纹内容建议哈希化:比如把
$_SERVER['REMOTE_ADDR'] . $_SESSION['user_id'] . time()做md5(),取前 8 字节转二进制,嵌入连续 64 个像素 - 别嵌太长——超过 200 bit 就容易因缩放、格式转换出错;更稳的做法是嵌 32-bit CRC 校验值 + 24-bit 用户 ID 片段
- 测试时用
file_get_contents()读输出的 PNG 二进制流,手动检查最后 100 字节是否含预期 marker(如"XFP:"),别只靠肉眼比对图
GD vs Imagick:抗截图水印选 GD,隐写选 Imagick
GD 更轻量、部署简单,适合常规动态水印;Imagick 支持通道级操作和无损重编码,LSB 隐写成功率高一倍以上,但需服务器装扩展且内存占用明显更高。
- GD 没有独立 alpha 通道操作函数,改 LSB 只能靠
imagecolorat()+imagesetpixel(),速度慢,大图易超时 - Imagick 可用
$img->getImagePixels(0,0,$w,$h,'RGB')批量读像素,用setPixelColor()精确控每个通道最低位,误差可控 - 线上环境若没装 Imagick,别硬上隐写——不如强化水印可见性+日志追踪,真要隐写就换方案(比如 Nginx + Lua 在响应头注入指纹 ID)
绕过 CDN 缓存是关键,否则所有努力白费
CDN 缓存的是 URL 对应的图片二进制,只要 URL 不变,用户拿到的就是同一份图,水印和指纹全失效。必须让每次请求 URL 带不可预测参数。
立即学习“PHP免费学习笔记(深入)”;
- 不要用
?t=1712345678这种秒级时间戳——CDN 可能按秒聚合缓存 - 推荐组合:
?v=<?php echo substr(md5(session_id().rand()),0,6); ?>,每次会话首次访问生成新 v 值,配合后端校验 session 存活 - Nginx 层加
Cache-Control: no-store头,但注意部分 CDN(如 Cloudflare 免费版)会忽略它;更可靠的是在 URL 中加入随机段,如/watermark/abc123/photo.png,由 PHP 路由拦截并返回真实图 - 检查响应头
X-Cache: HIT是否出现——出现就说明 CDN 把带水印的图缓存了,得立刻调 URL 策略
真正难的不是写几行 imagettftext(),而是让每个用户看到的图在字节层就不同,且这种不同不被中间层吃掉。CDN、浏览器缓存、图片格式、字体加载失败……任一环节断链,指纹就归零。











