教程

对 Bangumi 反向代理,解决大陆地区访问问题

TIP

关于为博客添加bangumi页面的教学见上一篇文章:为博客添加bangumi页面

缘由

继上篇文章后,我发现这个bangumi在国内访问是一片中国红啊,这就导致了天朝网络的环境下无法加载出图片。为了确保正常访问,就需要搭建一个反向代理,将图片资源从 lain.bgm.tv 替换为我们自己的。

实现原理

通过cf worker可以很轻松的实现这个想法,只需要构建好反向代理脚本,然后绑定一个自己的域名即可。(cf默认分配的域名大陆地区会有阻断。)

不过cf woker在大陆地区的网络情况并不理想,所以我在这基础上又套了一层eo的cdn。

所以最终流程就是

用户 -> lain.flygeon.top -> eo节点 -> cfwoker -> lain.bgm.tv

为了防止建立反代被cf斩杀,我们还需要移除登录注册相关的敏感路内容。(防止被判钓鱼站点)

完整代码

/**
 * Cloudflare Workers 反向代理脚本
 * 支持代理 bgm.tv 和 lain.bgm.tv
 */

// 默认目标域名(主站)
const DEFAULT_HOST = 'bgm.tv';

// 允许代理的目标域名列表
const ALLOWED_HOSTS = [
  'bgm.tv',
  'lain.bgm.tv',
];

// 允许的请求方法
const ALLOWED_METHODS = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'];

/** 需要屏蔽的固定路径 */
const BLOCKED_PATHS = [
  '/login',
  '/signup',
  '/register',
  '/auth',
  '/oauth',
];

/** 需要屏蔽的动态路径规则 */
const BLOCKED_PATTERNS = [
  /^\/subject\/\d+\/edit/,
  /^\/subject\/\d+\/related/,
  /^\/subject\/\d+\/episode_edit/,
  /^\/subject\/\d+\/merge/,
  /^\/character\/\d+\/edit/,
  /^\/person\/\d+\/edit/,
  /^\/subject\/\d+\/\(modify\)/,
  /^\/subject\/\d+\/delete/,
  /^\/subject\/\d+\/recollect/,
  /^\/subject\/\d+\/watched/,
  /^\/subject\/\d+\/interest/,
  /^\/subject\/\d+\/comments/,
];

/** 检查路径是否需要屏蔽 */
function isBlockedPath(path) {
  const normalizedPath = path.replace(/\/$/, '') || '/';

  for (const blocked of BLOCKED_PATHS) {
    if (normalizedPath === blocked || normalizedPath.startsWith(blocked + '/')) {
      return true;
    }
  }

  for (const pattern of BLOCKED_PATTERNS) {
    if (pattern.test(normalizedPath)) {
      return true;
    }
  }

  return false;
}

/** 替换 HTML 中的原始域名为代理域名 */
function rewriteHtmlUrls(html, proxyOrigin) {
  // 替换 lain.bgm.tv 的链接
  html = html.replace(
    /https?:\/\/lain\.bgm\.tv/gi,
    `${proxyOrigin}`
  );

  // 替换 bgm.tv 的链接
  html = html.replace(
    /https?:\/\/bgm\.tv/gi,
    `${proxyOrigin}`
  );

  // 处理相对协议 URL (//lain.bgm.tv)
  html = html.replace(
    /\/\/lain\.bgm\.tv/gi,
    `${proxyOrigin}`
  );

  // 处理相对协议 URL (//bgm.tv)
  html = html.replace(
    /\/\/bgm\.tv/gi,
    `${proxyOrigin}`
  );

  // 避免重复替换:清理可能出现的双重代理 URL
  const escapedOrigin = proxyOrigin.replace(/[.*+?^${}()|[\]\\]/g, '\\
#x26;'
);
const doubleProxyPattern = new RegExp( `${escapedOrigin}${escapedOrigin}/`, 'gi' ); html = html.replace(doubleProxyPattern, `${proxyOrigin}/`); return html; } /** 过滤 HTML 内容,移除登录/注册相关元素 */ async function filterHtmlContent(response, proxyOrigin) { const contentType = response.headers.get('content-type') || ''; if (!contentType.includes('text/html')) { return response; } let html = await response.text(); // 替换 HTML 中的原始域名为代理域名 html = rewriteHtmlUrls(html, proxyOrigin); // 移除登录/注册相关元素 const BLOCKED_HTML_PATTERNS = [ /<form[^>]*action="[^"]*(?:login|signin)[^"]*"[\s\S]*?<\/form>/gi, /<form[^>]*id="[^"]*login[^"]*"[\s\S]*?<\/form>/gi, /<form[^>]*class="[^"]*login[^"]*"[\s\S]*?<\/form>/gi, /<form[^>]*action="[^"]*(?:signup|register)[^"]*"[\s\S]*?<\/form>/gi, /<div[^>]*id="headerLogin"[^>]*>[\s\S]*?<\/div>/gi, /<a[^>]*href="[^"]*(?:login|signin|signup|register)[^"]*"[^>]*>[\s\S]*?<\/a>/gi, ]; for (const pattern of BLOCKED_HTML_PATTERNS) { html = html.replace(pattern, ''); } const filteredHeaders = new Headers(response.headers); filteredHeaders.delete('content-length'); return new Response(html, { status: response.status, statusText: response.statusText, headers: filteredHeaders, }); } /** * 主入口 */ addEventListener('fetch', event => { event.respondWith(handleRequest(event.request)); }); /** * 处理 OPTIONS 预检请求 */ function handleOptions(request) { const headers = new Headers(); headers.set('Access-Control-Allow-Origin', '*'); headers.set('Access-Control-Allow-Methods', ALLOWED_METHODS.join(', ')); headers.set('Access-Control-Allow-Headers', request.headers.get('Access-Control-Request-Headers') || '*'); headers.set('Access-Control-Max-Age', '86400'); return new Response(null, { status: 204, headers: headers, }); } async function handleRequest(request) { const url = new URL(request.url); // 处理 OPTIONS 预检请求 if (request.method === 'OPTIONS') { return handleOptions(request); } // 确定目标域名 let targetHost = DEFAULT_HOST; let targetPath = url.pathname; let targetSearch = url.search; // lain.bgm.tv 特有的资源路径前缀 const LAIN_PATH_PREFIXES = ['/r/', '/pic/', '/cover/', '/crt/', '/sub/', '/user/']; if (LAIN_PATH_PREFIXES.some(prefix => url.pathname.startsWith(prefix))) { targetHost = 'lain.bgm.tv'; } // 检查路径是否需要屏蔽 if (isBlockedPath(targetPath)) { return new Response( `<!DOCTYPE html> <html> <head><title>访问受限</title></head> <body style="text-align:center;padding:50px;font-family:sans-serif;"> <h1>访问受限</h1> <p>该页面已被屏蔽。</p> <a href="/">返回首页</a> </body> </html>`, { status: 403, headers: { 'Content-Type': 'text/html; charset=utf-8' }, } ); } // 构建目标 URL const targetUrl = `https://${targetHost}${targetPath}${targetSearch}`; // 验证目标域名是否在白名单中 if (!ALLOWED_HOSTS.includes(targetHost)) { return new Response( JSON.stringify({ error: 'Target host not allowed', allowed_hosts: ALLOWED_HOSTS, }), { status: 403, headers: { 'Content-Type': 'application/json' }, } ); } // 复制并修改请求头 const headers = new Headers(request.headers); headers.delete('host'); headers.delete('cf-connecting-ip'); headers.delete('cf-ray'); headers.delete('cf-visitor'); headers.delete('cf-worker'); headers.set('Host', targetHost); // 创建代理请求 const proxyRequest = new Request(targetUrl, { method: request.method, headers: headers, body: request.body, redirect: 'manual', }); try { // 发送代理请求 const response = await fetch(proxyRequest); // 复制响应并修改头部 const proxyHeaders = new Headers(response.headers); proxyHeaders.set('Access-Control-Allow-Origin', '*'); proxyHeaders.set('Access-Control-Allow-Methods', ALLOWED_METHODS.join(', ')); proxyHeaders.set('Access-Control-Allow-Headers', '*'); proxyHeaders.delete('server'); proxyHeaders.delete('x-powered-by'); proxyHeaders.delete('via'); // 处理重定向 if ([301, 302, 303, 307, 308].includes(response.status)) { const location = proxyHeaders.get('location'); if (location) { try { const redirectUrl = new URL(location, targetUrl); if (ALLOWED_HOSTS.includes(redirectUrl.hostname)) { const proxyRedirect = `${url.origin}${redirectUrl.pathname}${redirectUrl.search}`; proxyHeaders.set('location', proxyRedirect); } } catch (e) { // 保留原始 location } } } // 处理 Set-Cookie 中的 Domain 属性 const setCookie = proxyHeaders.get('set-cookie'); if (setCookie) { proxyHeaders.set('set-cookie', setCookie.replace(/domain=[^;]+;?/gi, '')); } // 过滤 HTML 内容 const filteredResponse = await filterHtmlContent( new Response(response.body, { status: response.status, statusText: response.statusText, headers: proxyHeaders, }), url.origin ); return filteredResponse; } catch (error) { return new Response( JSON.stringify({ error: 'Proxy request failed', message: error.message, }), { status: 502, headers: { 'Content-Type': 'application/json' }, } ); } }

博客 Bangumi 页面适配

搭建好反向代理后,还需要修改博客中 Bangumi 页面的图片加载逻辑,让封面图走代理域名而不是直连 lain.bgm.tv

修改 Card.astro

找到 src/components/bangumi/Card.astro,添加一个 getImageUrl 函数,将 Bangumi 的图片地址替换为代理地址:

---
import type { UserSubjectCollection } from "@/types/bangumi";

interface Props {
	item: UserSubjectCollection;
}

const { item } = Astro.props;

const subject_base_url = "https://bgm.tv/subject/";

function getImageUrl(originalUrl: string | undefined): string | undefined {
	if (!originalUrl) return undefined;
	return originalUrl.replace(
		"https://lain.bgm.tv/",
		"https://lain.flygeon.top/",
	);
}
---

然后在图片标签中使用这个函数:

<img
  data-src={getImageUrl(item.subject.images.medium)}
  alt={item.subject.name_cn || item.subject.name}
  class="bangumi-lazy-img w-full h-full object-cover pointer-events-none opacity-0 transition-opacity duration-300"
/>

为什么只改 Card 组件

Bangumi 页面中,图片地址只出现在 Card.astro 这一个组件里。BangumiSection.astro 负责数据分发和筛选,Pagination.astro 负责分页交互,FilterControls.astro 负责状态过滤,它们都不涉及图片 URL 的渲染。因此只需要修改 Card 组件即可覆盖所有图片请求。

enjoy it!

继续阅读

基于github托管图片相关
基于github托管图片

将图片放置到github仓库,得到直链访问

教程2026-06-13
部署你的Astrbot QQ机器人吧相关
部署你的Astrbot QQ机器人吧

从零开始部署一个功能强大的AstrBot QQ机器人,支持多种AI模型接入,实现智能群聊回复、私聊对话、自定义插件扩展等功能,成为赛博群老婆((

教程2026-06-18
相关
0成本将你的OneDrive部署至互联网,以及博客的一些小规划

使用CVercel和OneDrive API,无需服务器和域名即可将OneDrive网盘部署到互联网,实现文件在线预览和下载

教程2026-06-14
随机
关于我的常用软件

总结一下我的一些常用软件

分享2026-06-18
随机
记一次导航栏栏无法展开二级菜单的bug修复

屎山代码发力了,排查半天发现压根不是导航栏的问题,真凶另有其人

折腾记录2026-07-22
开发了一个个人导航页随机
开发了一个个人导航页

一款基于 Svelte 5 + Vite 6 的暗黑风个人导航页,具有包含动态粒子特效、音乐播放器、博客文章时间线显示等功能,可以通过

有趣的项目2026-07-11