静态资源加水印需通过过滤器拦截请求,识别图片/PDF格式后动态叠加文字或图片水印,保持原始文件不变、兼容缓存与CDN;须控制输入大小、异步处理、线程隔离,并验证ETag、CORS及提供开关端点。

静态资源(如图片、PDF)加水印,核心是拦截请求、识别资源类型、动态叠加水印后返回,不改动原始文件。关键在于轻量、低侵入、支持常见格式,且不影响原有缓存和 CDN 行为。
一、过滤器定位与触发时机
在 Web 容器(如 Tomcat、Jetty)或 Spring Boot 的 Servlet 过滤链中注册一个 URL 模式匹配型过滤器,例如 /static/**、/images/** 或通过后缀判断(*.png, *.jpg, *.pdf)。注意:必须在静态资源默认处理之前生效,且避免对非二进制资源(如 CSS/JS)执行水印逻辑。
- Spring Boot 中推荐用
@WebFilter(urlPatterns = "/static/**")+@ServletComponentScan,或注册FilterRegistrationBean - 手动判断请求路径是否为图片/PDF:提取后缀并转小写,检查是否属于
["png", "jpg", "jpeg", "gif", "bmp", "pdf"] - 跳过带查询参数
?raw=1或请求头X-No-Watermark: true的请求,便于调试或特殊场景绕过
二、水印生成与嵌入策略
水印分两类:文字水印(轻量、可配置)和图片水印(更隐蔽、需预加载)。生产环境建议优先用文字水印,支持透明度、旋转、重复平铺、字体大小自适应。
- 文字水印内容可从请求头(如
X-User-ID)、Session 或 JWT 解析出用户标识;也可固定为系统名 + 时间戳 - 使用 Java AWT 绘制 PNG/JPG:读取原始流 → 创建 BufferedImage → 获取 Graphics2D → 设置抗锯齿、透明度(AlphaComposite)、旋转角度(如 -30°)、字体(推荐 Noto Sans / SimHei 避免乱码)→ 循环绘制多个偏移位置的水印
- PdfBox 处理 PDF:用
PDDocument.load()加载,遍历每页 →PDPageContentStream添加透明文本,注意保留原 PDF 结构与交互元素(如表单、链接) - 务必设置响应头:
Content-Type保持原值,Content-Length重算,Cache-Control建议设为no-cache或按需透传原始缓存策略
三、性能与资源安全控制
水印操作是 CPU 密集型任务,不能阻塞主线程或耗尽堆内存。尤其要防恶意构造超大图片(如 100MB TIFF)触发 OOM。
- 限制输入大小:在读取前检查
Content-Length或流前若干字节(如 Magic Number),拒绝 >5MB 的图片、>50MB 的 PDF - 异步降级:对高并发场景,可引入本地缓存(Caffeine)缓存已加水印的响应体(key = 原始路径 + 水印参数 hash),TTL 设为 1 小时
- 线程隔离:将水印逻辑提交至专用线程池(
Executors.newVirtualThreadPerTaskExecutor()或固定大小的ForkJoinPool),避免拖慢整个过滤链 - 敏感路径白名单:只对
/public/assets/等明确开放目录生效,禁止对/admin/或上传临时目录自动加水印
四、上线前必验项
不是“能跑就行”,而是确保不破坏已有行为、可观测、可回滚。
- 验证原始 ETag/Last-Modified 是否失效:加水印后响应体变更,ETag 必须重新计算(可用 SHA256(原始内容+水印字符串))
- 检查跨域(CORS):若前端通过 fetch 加载加水印图片,确保响应头含
Access-Control-Allow-Origin - 日志埋点:记录「命中水印」、「跳过(因格式/参数/大小)」、「失败(异常类型)」三类指标,接入 Prometheus + Grafana
- 提供开关端点:如
POST /actuator/watermark/toggle,运行时启停,配合灰度发布

















