我的MP3都是上传到本地服务器调用的,没用CDN之前,面板后台有一个防盗链设置,可以防止外链调用MP3文件,只要把自己的域名写到“许可域名”即可。

Mianban

后来使用了CDN,不管是腾讯云的EO还是阿里云的ESA,发现这个防盗链系统失效了,原因是MP3文件被缓存到CDN各处的节点上,盗链的URL直接从边缘节点就可以拉去MP3文件。

今天和AI探讨了下,给了一个不错的解决方案分享下,如果你的音乐视频文件也是本地服务器上传调用的,可以参考设置。

先说结论:

最终方案是三件事配合:

  • 服务端(WordPress 插件)把媒体地址改写成带时效签名的临时地址;
  • CDN 边缘(边缘函数 / 原生 URL 鉴权)校验签名,不通过直接拒绝;
  • 受保护目录只走这条通道,原始直链形同废纸(签名一过期就无效)。

服务器端:插件

做一个PHP文件,放在 wp-content/mu-plugins/ (没有这个目录就新建), 好处是跟主题解耦 —— 以后换主题、升级主题都不用动它。

<?php
/**
 * Plugin Name: Audio Hotlink Guard
 * Description: 为受保护目录下的音视频生成带时效签名的 URL(配合 CDN 边缘验签)
 */

if (!defined('ABSPATH')) exit;

/** 受保护目录(按你的实际情况改) */
const MEDIA_DIR = '/media/';

/**
 * 密钥请放 wp-config.php:define('AUDIO_SIGN_SECRET', '一串随机字符');
 * 不要硬编码在主题或插件里 —— 那等于把钥匙和锁放在一起。
 */
function my_sign_secret() {
    return defined('AUDIO_SIGN_SECRET') ? trim((string) AUDIO_SIGN_SECRET) : '';
}

function my_is_media($path) {
    return (bool) preg_match('/\.(mp3|m4a|mp4|wav|ogg|flac|aac)$/i', (string) $path);
}

function my_signed_url($url) {
    $secret = my_sign_secret();
    if ($secret === '' || !is_string($url) || $url === '') return $url;

    $p = wp_parse_url($url);
    if (empty($p['path'])) return $url;
    $path = $p['path'];

    // 只签自己目录下的媒体文件;图片等其它资源原样放行
    if (strpos($path, MEDIA_DIR) !== 0) return $url;
    if (!my_is_media($path)) return $url;
    if (strpos($path, '..') !== false) return $url;          // 防路径穿越
    if (isset($p['query']) && strpos($p['query'], 'auth_key=') !== false) return $url; // 不重复签

    /* 关键:时间戳对齐到整点
     * 这样同一小时内生成的 URL 完全一致 → CDN 缓存才能命中
     * 若用 time() 原值,每次都不一样,等于让 CDN 全部失效 */
    $slot = time() - (time() % 3600);
    $sign = md5($path . '-' . $slot . '-0-0-' . $secret);

    return 'https://example.com' . $path . '?auth_key=' . $slot . '-0-0-' . $sign;
}

然后把它挂到内容输出上,这样文章里原本写的直链会被自动改写:

// 正文里的直链自动改写
add_filter('the_content', function ($content) {
    if (my_sign_secret() === '' || !is_string($content) || strpos($content, MEDIA_DIR) === false) {
        return $content;
    }
    return preg_replace_callback(
        '#https?://example\.com' . preg_quote(MEDIA_DIR, '#') . '[^\s"\'<>\)\]]+#i',
        function ($m) { return my_signed_url($m[0]); },
        $content
    );
}, 20);
上面这段是「最小示例」,可以粘贴复制后,放到AI里,然后把自己的网址也放进去,让AI访问你博客的MP3文件后,来帮你写出正常使用的PHP插件文件。

上传插件后,回到站点根目录,找到 wp-config.php 编辑,查找“请不要再继续编辑”,在它上面新起一行,加: define('AUDIO_SIGN_SECRET', '一串随机字符'); PS:这只是示例,请让AI修改后加入真实密匙填入。

下面的JS文件同理,也可以粘贴后交给AI进行本地化处理,然后导出给你。

CDN边缘:验签

如果你的 CDN 提供原生 URL 鉴权配置(比如腾讯云EO),直接在控制台填密钥和有效期就行,连代码都不用写。 如果没有、但提供边缘函数(比如阿里云ESA),就用下面这段(在边缘节点运行的 JS):

import { createHash } from "node:crypto";

// ⚠️ 与 wp-config.php 里的密钥逐字节一致;改一处必须改另一处
const SECRET = "YOUR_RANDOM_SECRET";

// 有效期窗口(秒)。配合服务端"整点对齐",实际有效期 1~2 小时
const DELTA = 7200;

// 只对音视频验签;其它文件(图片等)直接放行,避免误伤
const MEDIA_RE = /\.(mp3|m4a|mp4|wav|ogg|flac|aac)$/i;

// 本站域名,用于下面的"兜底放行"
const OWN_HOSTS = ["example.com", "www.example.com"];

/* 回源时注入一个自定义请求头,供源站校验。
 * 用途:防止有人拿到你的源站 IP 后绕过 CDN 直连。
 * 不配源站规则时它完全无害,就是个没人看的请求头。 */
const ORIGIN_HEADER = "X-Origin-Token";

function ownReferer(request) {
  const ref = request.headers.get("referer") || "";
  if (!ref) return false;
  try {
    return OWN_HOSTS.indexOf(new URL(ref).hostname.toLowerCase()) !== -1;
  } catch (_) { return false; }
}

/** 统一回源入口:注入来源标记,并保留原有 headers(Range 必须透传) */
function originFetch(request, urlOverride) {
  const h = new Headers(request.headers);
  h.set(ORIGIN_HEADER, SECRET);
  return fetch(urlOverride || request.url, { method: request.method, headers: h });
}

async function handleRequest(request) {
  const url = new URL(request.url);
  const path = url.pathname;

  // ① 非音视频文件 → 放行(避免误伤图片)
  if (!MEDIA_RE.test(path)) return originFetch(request);

  const authKey = url.searchParams.get("auth_key");
  const now = Math.floor(Date.now() / 1000);

  // ② 没有签名:本站来的放行,其它一律 401
  if (!authKey) {
    if (ownReferer(request)) return originFetch(request);
    return new Response("Unauthorized", { status: 401 });
  }

  const parts = authKey.split("-");
  if (parts.length !== 4) {
    return ownReferer(request) ? originFetch(request) : new Response("Unauthorized", { status: 401 });
  }
  const [timestamp, rand, uid, signature] = parts;
  const ts = parseInt(timestamp, 10);

  // ③ 过期
  if (!ts || isNaN(ts) || now > ts + DELTA) {
    if (ownReferer(request)) return originFetch(request);
    return new Response("Link expired", { status: 403 });
  }

  // ④ 验签(必须与服务端同一算法、同一密钥)
  const signString = [path, timestamp, rand, uid, SECRET].join("-");
  const md5 = createHash("md5").update(signString).digest("hex");
  if (md5 !== signature) {
    if (ownReferer(request)) return originFetch(request);
    return new Response("Unauthorized", { status: 401 });
  }

  // ⑤ 通过 → 回源。注意剥掉 auth_key:
  //    缓存 key 才算干净,同一小时内的请求才能命中同一份边缘缓存。
  return originFetch(request, url.origin + url.pathname);
}

export default {
  async fetch(request) {
    try {
      return await handleRequest(request);
    } catch (e) {
      // 函数自身异常时放行:宁可漏拦,也别把整站音频打挂
      return originFetch(request);
    }
  },
};

这个JS文件是用于阿里云ESA配置的,腾讯云EO下面再说,不需要这个JS文件。

阿里云ESA配置

1、进入阿里云ESA,开通“边缘计算和AI”功能后,创建函数,选择“本地上传”,将上面的JS文件拉入到虚线框内。

2、部署好后,进入这个函数,点击“域名”,页面拉到底部,可以看到“添加路由”的按钮(不要去设置上面的域名,是设置路由):

3、对路由信息进行设置:

Ali5

完成所有部署工作,防盗链正式生效。

谁在请求结果
你页面里带票的正常播放正常播放 ✓
别人网站嵌你的直链403 拒绝 ✓
脚本/下载器裸拉401 拒绝 ✓
MP3目录下的图片文件正常显示 ✓(不查票)
站点其它所有路径(文章页、图片、JS…)完全不受影响 ✓(这正是选「路由」而非「域名绑定」的意义)

腾讯云EO配置

如果你使用的是腾讯云EO,那配置更加简单,首先将 服务器端:插件 的PHP文件上传到 wp-content/mu-plugins/ 后,JS文件都不需要去做。

1、打开腾讯云EO,进入站点,点击左侧导航里的“站点加速”–“规则引擎”,并创建规则:

2、填写相关参数,MP3所在的文件夹用“正则匹配”,需要从这个文件里剔除的图片等文件,就用“正则不匹配”,然后填写主鉴权密匙和加密串参数即可,可参考下图:

Txy2

保存发布后,防盗链生效。无论是ESA还是EO,都是在CDN上进行验证防盗链,可以完美解决MP3/MP4文件被缓存到CDN各处的节点上,导致面板后台防盗链失效。