图片被盗链是中小站点最常见、也最难第一时间发现的成本黑洞。很多站长是账单突然翻倍时才回头查日志,发现主站每天几万次图片请求里有相当一部分来自别人的页面。这篇指南讲清三件事:如何用 Referer 规则拦住站外引用,如何用签名 URL 限制单张图片的有效期限,以及如何通过独立图片域名把图片流量从主站分流出去,让回源带宽(单位时间内可传输的数据量)不再被无谓地打满。
下面按”先拦、再限、后分流”展开,每一步都给出可直接套用的 Nginx 配置和验证命令。

图片防盗链真正要解决的是三笔账
图片防盗链的核心不是”禁止别人看我图”,而是让图片请求只服务真实访客。判断一个站点是否被大规模盗链,看三个指标就够了:图片出向流量占月度总额的比例、图片目录的并发连接数,以及源站出口带宽的峰值曲线。
举个具体的量级。一张 200 KB 的商品图被五个同行动态引用,每个站点每天展示两万次,就是一百万次额外的图片请求,折算约 190 GB 月度出向流量。静态图片由 Web 服务器直接读盘返回,CPU 压力其实不大,但磁盘 IOPS、并发连接和出口带宽会被同时占走;一旦出口跑满,正常访客的首屏就会从 1 秒变成 4 秒以上,这个损失比带宽费用更难挽回。
所以处理顺序应该是:先把明确的站外引用拦掉(成本最低、见效最快),再给需要授权的图片加上时效签名,最后才谈域名分流和限速。反过来的顺序会让配置越叠越复杂,还容易误伤真实用户。
Referer 规则:先定白名单,再决定拦还是放
Referer 是浏览器请求图片时附带的来源页地址,成本最低。它的问题也很明显:头部可以被客户端改写,脚本请求可以完全不带。所以它只能挡住 90% 顺手引用的人,不算安全边界,真要防批量抓取还得靠签名 URL。
配置上建议用 map 把判断逻辑从 location 里拆出来,比堆多层 if 更好维护,也更少踩 Nginx 的 if 嵌套坑:
map $http_referer $hotlink_block {
default 1;
"" 0;
"~^https?://(www\.)?example\.com/" 0;
"~^https?://img\.example\.com/" 0;
}
server {
listen 443 ssl;
server_name example.com;
location ~* \.(jpe?g|png|webp|gif|svg)$ {
if ($hotlink_block) { return 403; }
expires 30d;
add_header Cache-Control "public, max-age=2592000";
}
}
正则必须用 ^ 锚定。写成 ~example.com 时,evil-example.com、example.com.attacker.net 都会命中白名单,这是排障里最常见的漏配。空 Referer 是否放行要按业务定:直接放行会被脚本绕过,全部拦截又会让部分微信或 App 内嵌浏览器的首屏图片裂图,折中做法是放行空 Referer,让签名 URL 承担防抓取职责。
改完立刻用两条命令验证两个方向,不要只看浏览器:
curl -s -o /dev/null -w "%{http_code}\n" -e "https://other-site.com/" https://example.com/uploads/demo.webp
curl -s -o /dev/null -w "%{http_code}\n" -e "https://example.com/" https://example.com/uploads/demo.webp
第一条期望 403,第二条期望 200;若两条都返回 200,通常是规则写在了错误的 location 里。
还有一个容易被忽略的点:只配源站是没用的。请求如果命中 CDN 缓存,根本不会到源站,防盗链规则等于不存在。CDN(内容分发网络)一侧要同步配置回源 Referer 校验或边缘鉴权,具体做法可以参考站内这篇CDN 回源策略与缓存规则设计的思路,把边缘判断和源站判断当成两层防线来设计。

签名 URL:把”能访问”变成”限时可访问”
当图片本身带有商业价值——会员素材、付费图纸、订单附件、限时活动图——Referer 就不够了,你需要让每个 URL 自带有效期和签名。思路是:服务端用密钥对”过期时间 + 路径”做 HMAC 摘要,客户端拿到的链接只能在设定时间内使用,过期后返回 410。
生成端(Python)可以这样写,注意签名原文的顺序要和 Nginx 侧完全一致:
import hmac, hashlib, time
def sign(path: str, secret: str, ttl: int = 600) -> str:
exp = int(time.time()) + ttl
msg = f"{exp}{path}".encode()
sig = hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()[:16]
return f"{path}?exp={exp}&sig={sig}"
校验端用 Nginx 自带的 secure_link 模块,不需要额外跑一个鉴权服务:
location /private-images/ {
secure_link $arg_sig,$arg_exp;
secure_link_md5 "$secure_link_expires$uri my-secret";
if ($secure_link = "") { return 403; }
if ($secure_link = "0") { return 410; }
}
secure_link_md5 的原文固定是”过期时间 + URI + 密钥”,密钥写在配置里,签名算法必须与生成端逐字节对齐。上线前先拿一张测试图跑通 200、403、410 三种返回,再批量切换,否则一次拼接错误会让整站私有图片集体 403。
这里有一个必须提前想清楚的权衡:签名 URL 与 CDN 缓存天然冲突。每个访客拿到的 sig 不同,如果把整个 query string 计入缓存 key,同一张图会被缓存成上千个副本,命中率掉到个位数,回源量反而暴涨。解决办法是让 CDN 的缓存 key 忽略签名参数,只按路径缓存;具体到静态资源上云的分工,可以参考对象存储加 CDN 加速实战里的路径与缓存键设计。
签名有效期按场景分级即可:订单附件 10 分钟足够,会员素材 1 到 2 小时,公开商品图不需要签名——把签名用在公开图上只会拖低命中率。
图片分流域名:把图片流量从主站剥出去
拦截和签名都在减少”不该来的请求”,分流做的是另一件事:让”该来的请求”走一条更便宜的路径。做法是给图片单独一个域名,例如 img.example.com,让它指向 CDN 或独立的静态资源站点,主站只管 HTML 和接口。
这样做有三个实打实的好处:图片请求与页面请求的连接数分开,主站的 keepalive 连接池不会被大图占满;静态域名可以设成不携带 Cookie,一个首页三十张图能省掉接近 18 KB 的请求头上行;图片域名能单独设缓存时长和限速策略,不必跟 HTML 的短缓存妥协。
代价是每个新域名多一次 DNS(域名解析系统)解析和 TLS 握手,缓解方式是在页面头部加预连接提示,把握手提前到解析 HTML 阶段:
<link rel="preconnect" href="https://img.example.com" crossorigin>
<link rel="dns-prefetch" href="https://img.example.com">

分流之后,媒体资源在源站的职责会发生变化:源站从”每次都要返回图片”变成”只在缓存未命中时回源”,容量规划的重点也随之从出口带宽转向缓存命中率和回源并发。图片和视频变重后 CDN 与源站怎么重新分工,这篇分析讲得更完整。
回源带宽控制:三个限流位置
回源带宽是可以算出来的,不是靠感觉调的。回源量 ≈ 图片总请求数 ×(1 – 缓存命中率)× 平均图片体积。一百万次日均图片请求、平均 180 KB、命中率 85%,回源流量约 27 GB/天;命中率只要掉到 70%,同一天的回源量就变成 54 GB,翻倍。所以控制回源的第一手段永远是命中率,而不是限速。
命中率之后才是限流,建议按三个位置分层设置,从外到内收紧:
- 边缘节点做回源并发上限与合并回源,避免缓存过期瞬间几百个请求同时穿透到源站
- 源站用
proxy_cache做二级缓存,命中时直接返回,不打到后端 - 出口用
limit_conn和limit_rate兜底,防止单个爬虫或大图把出口占满
源站二级缓存的配置大致如下,inactive=7d 表示七天未被访问的副本自动清理,避免磁盘被长期占满:
proxy_cache_path /var/cache/nginx/img levels=1:2 keys_zone=img_cache:100m
max_size=20g inactive=7d use_temp_path=off;
location /uploads/ {
proxy_cache img_cache;
proxy_cache_valid 200 7d;
proxy_cache_lock on;
limit_conn addr 8;
limit_rate_after 512k;
limit_rate 1m;
}
limit_rate 一定是兜底而非常规优化。单个连接限到 1 MB/s 对正常访客毫无影响,但能把异常抓取的效率压到原来的几十分之一。上线前后用压测确认承载边界,具体方法见服务器带宽压测:iperf3 与 wrk 评估网站承载量。
五个最常见的误配与验证方法
配置写完不等于生效,下面这几条是排障里反复出现的坑,上线前逐条对一遍:
- 只配源站不配 CDN:缓存命中时请求不回源,防盗链形同虚设,必须在边缘同步判断
- 白名单正则未锚定:
~example.com会命中evil-example.com,改用~^https?://(www\.)?example\.com/ - 签名参数计入缓存 key:命中率掉到个位数,回源量翻几倍,需在 CDN 侧忽略 query
- 空 Referer 全拦:部分 App 内嵌浏览器首屏图片裂图,改为放行空 Referer 并由签名兜底
验证不要只看浏览器里”图能显示”:用 curl -e 模拟站内和站外来源检查状态码,再看 CDN 回源日志里图片路径的请求量是否下降,最后对比图片目录的日均出向流量,三天趋势比单日数字更能说明问题。
总结:先止血,再优化,最后用数据验收
图片防盗链和图片分流不是两个功能,而是一条处理链:Referer 白名单挡住顺手引用,签名 URL 限制授权访问,独立图片域名把流量引导到正确的层,回源限速兜住异常峰值。任何一环单独上,都只能解决三分之一的问题。
落地节奏建议按风险从低到高排:第一周先开 Referer 白名单并放行空 Referer,几乎不会误伤真实访客,能立刻压掉大部分站外引用;第二到第四周再做签名 URL 与图片分流域名,同步调整 CDN 缓存 key;最后用命中率曲线和回源带宽曲线验收,把图片出向流量下降 30% 以上、CDN 命中率不低于 85%、主站首屏不退化作为通过标准。
如果你的站点跑在 Hostease 的共享主机上,面板里已经提供图片防盗链开关,可以直接打开;如果用的是独立服务器或 VPS(虚拟专用服务器)环境,上面的 Nginx 配置可以直接写进站点配置。推荐每次只改一层、观察一天再进入下一层,这样出问题时能立刻定位到是哪一步引入的。