通过ImgProxy优雅的实现博客图片的尺寸缩放、水印叠加与格式转换

2026-05-18 12:01:38 1点赞 3收藏 0评论

前言

之前博客的图片都没有添加水印,遇到过几次原封不动照搬整篇文章的,浏览量比我自己发布的还高不说,更可气的是连图片都懒得转存,链接直接是我自建的对象存储 MinIO!

老观众应该知道,我的网站是 VitePress 静态博客,所有的图片都是通过 PicList 上传到 MinIO 的,这就有两个问题:

  1. 链接:所有图片的链接都是写在 Markdown 里的,后期如果换到别的对象存储或者图床的话,需要替换每一篇文章的图片链接。

  2. 后期处理:虽然 PicList 可以在上传时直接处理图片,常用的转换格式、压缩、添加水印功能都支持,但这样上传到 MinIO 中的就是处理后的图片了,但我更希望 MinIO 中存储的是原图,只有对外展示的时候进行处理

⚠️ 警告

如果不经过任何处理就上传的实拍照片,还可能携带 EXIF 信息,造成隐私泄露。

除此之外,现在的图片链接是直接桶名+文件名,例如 bucket/image.png。如果桶名或者文件名带上时间等信息,就会显得不优雅。况且现在的图片是直接访问的 MinIO,没有对接额外的图床程序,也不太好像公开图床那样上传直接打乱文件名,否则 MinIO 中的文件将很难维护。

ImgProxy

优点

ImgProxy 正好就可以解决上面的所有痛点!有了它之后,MinIO 就只是起到一个存储的作用,里面存放的都是原格式、未经处理的图片,而对外展示的部分完全交由 ImgProxy 来完成。

官网 Demo官网 Demo

以官网的图片为例,这张图经过 ImgProxy 处理后的 URL 是这样的

https://demo.imgproxy.net /O5HLYkkYstGA_lS0ifDlHPzj3H7el22SFgA-V5pTRuw /rs:fill:1160:540:1/g:ce/wm:0.5:soea:10:10:0.2 /plain/https:%2F%2Fmars.nasa.gov%2Fsystem%2Fdownloadable_items%2F39099_Mars-MRO-orbiter-fresh-crater-sirenum-fossae.jpg

粗看 URL 显得非常的长,细看的话每一个部分都有各自的作用,简单的换行分隔后,可以分为四个部分:

  1. ImgProxy 域名

  2. 签名:通过设置 KEYSALT 对后面的处理参数进行哈希运算,防止用户修改处理参数刷爆 CPU

  3. 处理参数:缩放、裁切等等一系列操作都在这里

  4. 原图链接:完整 URL 的以 /plain 开头;也可以是 base64

你可能有个疑问,这链接不是更复杂了,哪里优雅了?

如果直接在博客中用这样的链接确实很丑陋,我们可以在 Nginx 反代的时候,稍微处理一下把中间处理参数隐藏了,再把图片转成 base64,最终的链接就是 https://demo.imgproxy.net/{signature}/{base64},看上去就简洁多了。

例如,上面这个链接就会变成(原图 URL 太长导致转换后的 base64 之后也很长)

实际使用时,我们可以直接用 S3 连接 MinIO,这样链接就是 s3://{bucket}/{image.png},转换为 base64 也会很短

https://demo.imgproxy.net /O5HLYkkYstGA_lS0ifDlHPzj3H7el22SFgA-V5pTRuw /aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn

对于普通用户来说,这样的图片链接就看不出任何的有用信息了。

美中不足的是, ImgProxy 的免费版只支持 base64,可以还原出桶名文件名,不过这对于一般的博客来说已经足够了,AES-CBC 需要 Pro 版,价格非常的贵。

缺点

ImgProxy 最大的一个缺点是图片都是用户访问时实时处理的,对于 CPU 的压力会比较大,建议部署在配置较高的 VPS 上。

同时,在用 Nginx 反代的时候,一定要开启缓存,这样一来只有第一次访问需要处理,后面会直接走 Nginx 缓存,避免页面刷新时同一张图片被 ImgProxy 重复处理。

安装

推荐直接用过 Docker Compose 进行部署,以下是我的配置,大家可以参考着修改一下:

  1. IMGPROXY_KEYIMGPROXY_SALT 可以通过 echo $(xxd -g 2 -l 64 -p /dev/random | tr -d 'n') 生成直接填入

  2. ImgProxy 的端口

  3. ImgProxy 设置

  4. 连接 S3 对象存储(支持 MinIO),配置后可以直接用 s3://{bucket}/{image.png} 这样的链接来访问图片

  5. 性能设置,避免 VPS 高负载

services: imgproxy: image: darthsim/imgproxy:latest container_name: imgproxy restart: always network_mode: host environment: # 1. 填入刚才生成的十六进制字符串 - IMGPROXY_KEY= - IMGPROXY_SALT= # 2. 访问控制 - IMGPROXY_BIND=:8080 # 3. 核心功能配置:默认输出 WebP,添加水印 - IMGPROXY_QUALITY=100 - IMGPROXY_KEEP_COPYRIGHT=false - IMGPROXY_AUTO_WEBP=true - IMGPROXY_ENFORCE_WEBP=true - IMGPROXY_WATERMARK_URL=s3://images/watermark.webp # 可选:如果要用图片水印 - IMGPROXY_WATERMARK_OPACITY=0.7 # 4. S3/MinIO 源设置 (让 imgproxy 能认出 s3:// 协议) - IMGPROXY_USE_S3=true - IMGPROXY_S3_ENDPOINT=http://127.0.0.1:9000 - AWS_ACCESS_KEY_ID= - AWS_SECRET_ACCESS_KEY= # 5. 性能与限制 - IMGPROXY_READ_REQUEST_TIMEOUT=10 - IMGPROXY_TIMEOUT=10 - IMGPROXY_DOWNLOAD_TIMEOUT=5 - IMGPROXY_WORKERS=4 # 根据你的 VPS 核心数调整

反向代理

接下来在 Nginx 中配置缓存与反代,不过我对 Nginx 不是特别的了解,下面部分配置借助 AI 实现。

首先配置缓存路径、大小以及时间

proxy_cache_path /www/cache levels=1:2 keys_zone=img_cache:10m max_size=1g inactive=30d use_temp_path=off;

我这里是直接将 ImgProxy 反代在主域名的 /images 路径上,访问时就是 https://example.com/images/……,你也可以单独用一个域名 https://images.example.com

接着把图片的处理参数也直接放在反代中,这样最终的地址就只有签名base64 路径了。

再下面就是一些缓存的配置,目前有一个缺点,如果上传替换了同名图片,需要手动 Ctrl+ F5 或者加上 ?refresh=1 才能更新缓存,展示出新的图片。有更好的解决办法欢迎分享!

location ~ ^/images/([^/]+)/([^/]+)$ { # $1 是前面提取的 签名 (如 insecure 或 真实的 base64 签名) # $2 是前面提取的 图片源地址的 base64 # 自动拼装参数并反代给 imgproxy proxy_pass http://127.0.0.1:8080/$1/wm:1:soea:15:15:0.1/$2; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 建议加上缓存以减轻 Imgproxy 压力(根据你的 Nginx 缓存配置自行调整) proxy_cache img_cache; ​ # 定义缓存 Key proxy_cache_key $host$uri; ​ # 绕过缓存策略 (Ctrl+F5 或 ?refresh=1 强制更新) proxy_cache_bypass $http_pragma $arg_refresh; ​ # 缓存状态码有效时间 proxy_cache_valid 200 301 302 7d; proxy_cache_valid 404 1m; ​ # 防雪崩与容灾机制 proxy_cache_lock on; proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504; proxy_cache_background_update on; ​ # 添加响应头,方便在浏览器按 F12 抓包查看缓存命中状态 add_header X-Cache-Status $upstream_cache_status; }

实际应用

现在就已经可以使用 ImgProxy 了,不过想要在真正应用到博客中,需要对所有的图片链接进行处理,先记录下以下内容:

  • ImgProxy 环境变量中设置的 IMGPROXY_KEYIMGPROXY_SALT

  • 处理参数,也就是 http://127.0.0.1:8080/$1/wm:1:soea:15:15:0.1/$2 中间的 wm:1:soea:15:15:0.1

这 3 个变量用于生成签名,两边要始终保持一致,比如调整处理参数时,反代与博客链接都要同时修改,否则签名就会错误。

测试效果的时候可以暂时不填写 KEYSLAT,用 insecure 替代,方便调试

下面是我在 VitePress 中,通过自定义 markdown-it 插件批量替换图片链接

import crypto from 'crypto'; import type MarkdownIt from 'markdown-it'; ​ // 如果你在 ImgProxy 中配置了真实的 KEY 和 SALT,请在此填入 Hex 格式的字符串 const IMGPROXY_KEY = ; const IMGPROXY_SALT = ; const imgproxyParams = 'wm:1:soea:15:15:0.1'; ​ // 生成 ImgProxy 签名 function generateSignature(path: string): string { if (!IMGPROXY_KEY || !IMGPROXY_SALT) return 'insecure'; ​ const keyBin = Buffer.from(IMGPROXY_KEY, 'hex'); const saltBin = Buffer.from(IMGPROXY_SALT, 'hex'); ​ const hmac = crypto.createHmac('sha256', keyBin); hmac.update(saltBin); hmac.update(path); ​ // ImgProxy 要求使用 URL-Safe Base64 格式的签名 return hmac.digest('base64url'); } ​ export default function imgproxyPlugin(md: MarkdownIt): void { const defaultRender = md.renderer.rules.image || function (tokens, idx, options, env, self) { return self.renderToken(tokens, idx, options); }; ​ md.renderer.rules.image = function (tokens, idx, options, env, self) { const token = tokens[idx]; const srcIndex = token.attrIndex('src'); ​ if (srcIndex >= 0 && token.attrs) { const originalSrc = token.attrs[srcIndex][1]; ​ // 1. 拦截指定域名的图片: https://s3.example.com/(...) const match = originalSrc.match(/^https://s3.example.com/(.+)$/); if (match) { // 2. 转换为 s3:// 格式 const s3Uri = `s3://${match[1]}`; ​ // 3. 将原图 S3 地址转换为 URL-Safe Base64 const encodedUrl = Buffer.from(s3Uri).toString('base64url'); ​ // 4. 计算签名(针对 Nginx 拼装后的最终路径) const targetPath = `/${imgproxyParams}/${encodedUrl}`; const signature = generateSignature(targetPath); ​ // 5. 替换最终给前端渲染的 URL token.attrs[srcIndex][1] = `https://example.com/images/${signature}/${encodedUrl}`; } } ​ // 调用默认渲染器,返回最终 HTML 字符串 return defaultRender(tokens, idx, options, env, self); }; } ​ export { imgproxyPlugin };

最后在 VitePress 的 config.ts 中导入这个插件

import { imgproxyPlugin } from './theme/plugins/imgproxy-plugin'; ​ export default defineConfig({ markdown: { config: (md) => { md.use(imgproxyPlugin); } });

最终效果

完成以上所有步骤后,构建后的博客图片链接就变成了 https://example.com/images/{signature}/{base64},也就是 ImgProxy 处理后转换为 WebP添加上水印的图片了。

同时,对象存储中的仍是未处理的原图,Markdown 中的链接也还是 https://s3.example.com/images/image.png

这样一来,加水印、转格式这些图片处理操作,对于原有的工作流没有任何影响。

作者声明本文无利益相关,欢迎值友理性交流,和谐讨论~

展开 收起
0评论

当前文章无评论,是时候发表评论了
提示信息

取消
确认
评论举报

相关文章推荐

更多精彩文章
更多精彩文章
最新文章 热门文章
3
扫一下,分享更方便,购买更轻松