Cloudflare Workers和imgproxy实现图片反向代理实现html2canvas截图跨域图片

在开发纯前端的番剧九宫格工具时,我遇到了一个经典问题:html2canvas 无法截取来自第三方 API(如 AniList、Bangumi)的图片。原因是这些图片服务器没有设置 Access-Control-Allow-Origin 头,浏览器出于安全策略限制了 canvas 对跨域图片的读取。

解决方案是用反向代理——让图片请求先经过自己的服务器,在响应中手动添加 CORS 头。这里使用了两种方式:自建 Cloudflare Worker 和 朋友提供了公共 imgproxy 服务。两者可以配合使用,提高成功率。

原理简述

  1. 前端不直接请求图片源站,而是请求代理服务(格式如 https://代理地址/proxy?url=图片原始URL)。
  2. 代理服务收到请求后,用后端(Worker 或 imgproxy)去获取图片,同时可以自定义请求头(如 User-Agent、Referer)以绕过防盗链。
  3. 代理服务将图片返回给前端,并在响应头中强制添加 Access-Control-Allow-Origin: *
  4. 此时对浏览器来说,图片来源于同源的代理地址,跨域限制消失,html2canvas 就能正常绘制。

方法一:Cloudflare Workers(完全可控)

1. 创建 Worker

  • 登录 Cloudflare Dashboard,进入 Workers & Pages
  • 点击 创建应用程序Create application 选择Start with Hello World! ,或或 “+Add“ → 创建 Workers
  • 命名(例如 anime-image-proxy
  • 部署后点击 编辑代码,将以下 JavaScript 代码粘贴进去:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
export default {
async fetch(request) {
const url = new URL(request.url);

// --- 1. 测试请求---
if (url.searchParams.has("test")) {
return new Response('<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg"><rect width="200" height="200" fill="green"/><text x="20" y="110" fill="white" font-size="20">Worker OK</text></svg>', {
headers: { "Content-Type": "image/svg+xml", "Access-Control-Allow-Origin": "*" }
});
}

// --- 2. 获取目标图片地址 ---
const imageUrl = url.searchParams.get("url");
if (!imageUrl) {
return new Response("Missing 'url' parameter", { status: 400 });
}

try {
// --- 3.请求头 ---
// 模拟 Chrome 浏览器的完整请求头,降低被源站拒绝的概率
const response = await fetch(imageUrl, {
headers: {
// 伪造一个常见的 User-Agent
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
// 设置一个合理的 Referer,对于 Bangumi 图片,可以设为其主站
"Referer": "https://bangumi.tv/",
// 模拟浏览器接收的格式
"Accept": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
// 告诉源站我们支持压缩
"Accept-Encoding": "gzip, deflate, br",
// 保持连接活跃
"Connection": "keep-alive",
// 有些源站会检查这个头
"Sec-Fetch-Dest": "image",
"Sec-Fetch-Mode": "no-cors",
"Sec-Fetch-Site": "cross-site"
}
});

// --- 4. 处理源站返回的错误 ---
if (!response.ok) {
// 如果源站返回404或403,我们也可以返回一个友好的占位图
return new Response(`图片获取失败 (源站返回 ${response.status})`, {
status: response.status,
headers: { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" }
});
}

// --- 5. 检查内容类型 ---
const contentType = response.headers.get("content-type") || "";
if (!contentType.startsWith("image/")) {
return new Response("源站返回的内容不是图片", { status: 400 });
}

// --- 6. 构建成功响应,添加CORS头并转发图片 ---
const modifiedResponse = new Response(response.body, response);
modifiedResponse.headers.set("Access-Control-Allow-Origin", "*");
// 可以缓存图片,减少Worker请求次数
modifiedResponse.headers.set("Cache-Control", "public, max-age=86400");

return modifiedResponse;

} catch (error) {
// --- 7. 记录详细错误并返回友好信息 ---
console.error(`代理图片失败: ${imageUrl}`, error.message);
return new Response(`代理失败: ${error.message}`, {
status: 500,
headers: { "Access-Control-Allow-Origin": "*" }
});
}
}
}

2. 部署与使用

  • 部署后获得 Worker 域名:https://anime-image-proxy.你的用户名.workers.dev

  • 在前端中构造代理 URL:

    1
    const proxyUrl = `https://anime-image-proxy.你的用户名.workers.dev/proxy?url=${encodeURIComponent(原始图片URL)}`;
  • <img>src 或 CSS background-image 设为此 URL。


方法二:imgproxy(专用图片处理服务)

imgproxy是一个高性能的图片处理服务器,支持实时调整大小、格式转换,同时也支持简单的代理模式。可以自建,也可以使用公共实例。

使用公共实例

这次使用了朋友提供的 imgproxy 地址:

它已经配置好 CORS 头,可以直接在前端使用。用法与 Worker 完全相同,只需将代理地址替换即可。

自建 imgproxy(可选)

如果你有服务器,可以通过 Docker 一键部署:

1
docker run -p 8080:8080 darthsim/imgproxy

然后通过 http://你的服务器:8080/insecure/图片URL 访问(不安全模式),或配置签名密钥增加安全性。详细配置参考官方文档


前端整合:多代理备用策略

为了提高容错率,在前端维护一个代理列表,当一个代理加载失败时自动切换到下一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const PROXY_LIST = [
'https://你的worker域名/proxy?url=',
'https://imgproxy/api/image-proxy?url='
];

function getProxyUrl(originalUrl, proxyIndex = 0) {
return PROXY_LIST[proxyIndex] + encodeURIComponent(originalUrl);
}

// 在 <img> 上使用 onerror 尝试下一个代理
img.onerror = function() {
let nextIndex = (当前索引 + 1) % PROXY_LIST.length;
this.src = getProxyUrl(this.dataset.original, nextIndex);
};

注意事项

  1. URL 编码:原始图片 URL 必须经过 encodeURIComponent,否则参数可能被截断。
  2. 防盗链:部分图片源检查 Referer,可在代理请求中伪造合适的 Referer(如 https://anilist.cohttps://bangumi.tv)。
  3. 缓存:在代理响应中加入 Cache-Control 头可以减轻代理服务器压力。
  4. 稳定性:公共 imgproxy 可能随时不可用,最好同时维护自建 Worker 作为备选。
  5. 隐私:使用公共代理时,图片请求会经过第三方服务器,敏感数据请谨慎。

总结

通过 Cloudflare Workers 和 imgproxy,我们低成本地解决了前端图片跨域问题,让 html2canvas 能够顺利截图来自任何源的图片。Worker 提供完全自控的代理逻辑,imgproxy 则是一个专业、现成的图片处理服务,两者结合可以实现高可用、高性能的图片代理层。这套方案不仅适用于番剧工具,也可以用在任何需要跨域图片渲染的场景(如海报生成、社交卡片等)。

如果你也想在自己的项目中实现类似功能,不妨从部署一个 Cloudflare Worker 开始,它免费、简单,而且足够强大。

应用项目

Anime-pick-grid

项目简介

你好请吃我的番剧安利-番剧收集器是一个基于网页的番剧推荐表生成工具,用户可以通过搜索和选择番剧,创建自己的番剧推荐九宫格或更多格子的推荐表。

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

You Found Me.

支付宝
微信