项目概述
本项目实现按需缩放的图片 CDN 代理:以签名 URL 编码宽高等参数,只有持密钥的调用方才能生成变体,并按原图与参数派生缓存键,未命中时抓取、缩放并存储。没有签名,代理就是一个人人可用的免费图像处理端点。
架构与关键决策
签名覆盖所有影响输出的参数,任一参数被改动都会导致拒绝;缓存键包含原点与每个影响输出的参数,避免把某一尺寸的字节当成另一尺寸返回。格式协商读取 Accept 头并选择最受支持的编码,回退到原格式,所选格式进入键使两种编码分别缓存。响应带与签名绑定的不可变缓存头,使共享缓存可反复服务同一次缩放,签名有效期则限定一个泄露链接的可用时长。
实施与测试
请求已签名变体断言尺寸与内容类型;复用同一 URL 断言命中缓存而非再次缩放;篡改任一签名参数断言 403;断言不同宽度产生不同缓存条目。
限制与后续
- 按需生成变体,不预生成,首个请求承担缩放成本。
- 原图变化不自动清理,需给原图 URL 加版本号。
- 无按调用方配额,滥用控制完全依赖签名。
代码示例
pnpm init
pnpm add sharp ioredis express
pnpm add -D typescript tsx vitest
image-cdn-proxy/
├── src/sign.ts
├── src/resize.ts
├── src/cache.ts
├── src/negotiate.ts
├── tests/cdn.test.ts
└── package.json
if (hmac(params) !== sig) return res.status(403).end();
const key = [sha256(src), w, h, fmt, q].join(':');
const fmt = req.accepts(['avif', 'webp', 'jpeg']);
res.set('Cache-Control', 'public, max-age=31536000, immutable');
if (req.headers['if-none-match'] === etag) return res.status(304).end();
pnpm vitest run