通过 ImgProxy 优雅的实现博客图片的尺寸缩放、水印叠加与格式转换
前言
之前博客的图片都没有添加水印,遇到过几次原封不动照搬整篇文章的,浏览量比我自己发布的还高不说,更可气的是连图片都懒得转存,链接直接是我自建的对象存储 MinIO!
老观众应该知道,我的网站是 VitePress 静态博客,所有的图片都是通过 PicList 上传到 MinIO 的,这就有两个问题:
- 链接:所有图片的链接都是写在 Markdown 里的,后期如果换到别的对象存储或者图床的话,需要替换每一篇文章的图片链接。
- 后期处理:虽然 PicList 可以在上传时直接处理图片,常用的转换格式、压缩、添加水印功能都支持,但这样上传到 MinIO 中的就是处理后的图片了,但我更希望 MinIO 中存储的是原图,只有对外展示的时候进行处理
警告
如果不经过任何处理就上传的实拍照片,还可能携带 EXIF 信息,造成隐私泄露。
除此之外,现在的图片链接是直接桶名+文件名,例如 bucket/image.png。如果桶名或者文件名带上时间等信息,就会显得不优雅。况且现在的图片是直接访问的 MinIO,没有对接额外的图床程序,也不太好像公开图床那样上传直接打乱文件名,否则 MinIO 中的文件将很难维护。
ImgProxy
优点
ImgProxy 正好就可以解决上面的所有痛点!有了它之后,MinIO 就只是起到一个存储的作用,里面存放的都是原格式、未经处理的图片,而对外展示的部分完全交由 ImgProxy 来完成。

以官网的图片为例,这张图经过 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 显得非常的长,细看的话每一个部分都有各自的作用,简单的换行分隔后,可以分为四个部分:
- ImgProxy 域名
- 签名:通过设置
KEY和SALT对后面的处理参数进行哈希运算,防止用户修改处理参数刷爆 CPU - 处理参数:缩放、裁切等等一系列操作都在这里
- 原图链接:完整 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 进行部署,以下是我的配置,大家可以参考着修改一下:
IMGPROXY_KEY和IMGPROXY_SALT可以通过echo $(xxd -g 2 -l 64 -p /dev/random | tr -d '\n')生成直接填入- ImgProxy 的端口
- ImgProxy 设置
- 连接 S3 对象存储(支持 MinIO),配置后可以直接用
s3://{bucket}/{image.png}这样的链接来访问图片 - 性能设置,避免 VPS 高负载
services:
imgproxy:
image: darthsim/imgproxy:latest
container_name: imgproxy
restart: always
network_mode: host
environment:
# 1. 填入刚才生成的十六进制字符串
- IMGPROXY_KEY=<KEY>
- IMGPROXY_SALT=<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=<ACCESS_KEY>
- AWS_SECRET_ACCESS_KEY=<SECRET>
# 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_KEY与IMGPROXY_SALT - 处理参数,也就是
http://127.0.0.1:8080/$1/wm:1:soea:15:15:0.1/$2中间的wm:1:soea:15:15:0.1
这 3 个变量用于生成签名,两边要始终保持一致,比如调整处理参数时,反代与博客链接都要同时修改,否则签名就会错误。
测试效果的时候可以暂时不填写
KEY和SLAT,用insecure替代,方便调试
下面是我在 VitePress 中,通过自定义 markdown-it 插件批量替换图片链接
import crypto from 'crypto';
import type MarkdownIt from 'markdown-it';
// 如果你在 ImgProxy 中配置了真实的 KEY 和 SALT,请在此填入 Hex 格式的字符串
const IMGPROXY_KEY = <IMGPROXY_KEY>;
const IMGPROXY_SALT = <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<ThemeConfig>({
markdown: {
config: (md) => {
md.use(imgproxyPlugin);
}
});最终效果
完成以上所有步骤后,构建后的博客图片链接就变成了 https://example.com/images/{signature}/{base64},也就是 ImgProxy 处理后转换为 WebP、添加上水印的图片了。
同时,对象存储中的仍是未处理的原图,Markdown 中的链接也还是 https://s3.example.com/images/image.png。
这样一来,加水印、转格式这些图片处理操作,对于原有的工作流没有任何影响。









