⭐ 推荐:社区规则条款 V1.0

小皮面板怎么设置防盗链防止图片被外站盗用?

chinaz
chinaz 初级会员萌新站长
发布于 2026-10-27 01:28 ·1 浏览 ·4 回复
内容摘要

小皮面板无内置防盗链开关,需在 Nginx 站点配置中用 valid_referers 校验 Referer、或在 Apache 的 .htaccess 中用 RewriteCond 拦截,将外站图片请求返回 403 或占位图,同时放行空

本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2600.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 4

juming
juming 作者初级会员 1楼 2026-10-27 01:41

配置思路没问题,但有两处最容易翻车的地方值得补一下:Nginx 那段 if 不用怕“if is evil”,真正的坑在 Apache 侧的 AllowOverride,以及 CDN 把 403 缓存下来误伤本站用户。

先说 Nginx。valid_referers ... ; if ($invalid_referer) { return 403; } 是官方文档认可的写法,因为 location 内只做 return,不涉及 rewrite 上下文,性能上没有额外开销。建议把 server_names 换成显式域名,比如 valid_referers none blocked example.com www.example.com *.example.com。泛解析或多域名共用 server_name 时,server_names 会把你不希望放行的域名也一起放行。放行名单里记得加上 *.baidu.com、*.google.com、*.sogou.com 这类搜索引擎图片抓取域名,否则图片收录会掉。

Apache 那半段补完:.htaccess 里写 RewriteCond %{HTTP_REFERER} !^$、RewriteCond %{HTTP_REFERER} !^https?://(www\.)?example\.com/ [NC],再配 RewriteRule \.(jpg|jpeg|png|gif|webp)$ - [F,NC]。小皮面板下最常见的“配了完全没反应”是 httpd.conf 或 vhost 里对应 <Directory> 段的 AllowOverride 不是 All,.htaccess 直接失效;其次 mod_rewrite 没开。这两项都要在面板里先确认。

最后提醒误伤面:微信/QQ 内置浏览器、部分安卓 App 请求图片时会带奇怪的 Referer,配置前先开日志看几天真实 Referer 分布再收紧白名单。验证可以直接 curl -e "https://other.com" -I https://你的域名/test.jpg 看返回码,改完务必清一遍 CDN 缓存。很多 CDN 不认 Vary: Referer,一旦把 403 缓存进去,本站用户会集体看到破图。图片量大又上了对象存储的话,其实直接用 OSS/COS 自带的 Referer 黑白名单更省事,不必在源站扛。

lqyyzz
lqyyzz 观望站长 #3216 2楼 2026-10-27 01:49
juming:配置思路没问题,但有两处最容易翻车的地方值得补一下:Nginx 那段 `if` 不用怕“if is evil”,真正的坑在 Apache 侧的 `AllowOv…

补得很到位,尤其 AllowOverride 那条——小皮面板下 .htaccess 配了没反应,九成是它。我再补一个更隐蔽、也更常被人忽略的地方:add_header Vary Referer; 在 403 状态下根本不会下发。

Nginx 的 add_header 和 expires 默认只对 200/201/204/206/301/302/303/304/307/308 这些状态码生效,命中防盗链拦截返回 403 时,这两个响应头都不会被加上。也就是说,CDN 拿到的 403 是一个没有任何 Vary 标记的响应,照样可能被缓存。写法要改成 add_header Vary Referer always;(1.7.5 以后支持 always 参数),或者干脆把防盗链策略放到 CDN 侧做。

如果不想返回 403、想给占位图,error_page 403 /static/nohotlink.png; 比 rewrite 更干净:返回的是 200,缓存头正常下发,占位图又在独立的 /static/ location 里,不会被 valid_referers 再过一遍、也不会自循环。顺便记一下,微信内置浏览器的 Referer 是 servicewechat.com 系,QQ 是 *.qq.com,这两个不放行的话移动端分享出去的图会全变红叉。改之前建议先用 awk -F'"' '{print $4}' access.log | awk -F'/' '{print $3}' | sort | uniq -c | sort -rn | head -20 把真实 Referer 分布拉出来看两天,再按数据收紧白名单。

对象存储那条我完全同意,补一句:OSS/COS 的 Referer 白名单里务必勾上“允许空 Referer”,否则 App 端和直接访问的用户会集体 403,这个比源站配错还常见。

满月居于空
满月居于空 萌新站长 #3219 3楼 2026-10-27 01:55
lqyyzz:补得很到位,尤其 `AllowOverride` 那条——小皮面板下 `.htaccess` 配了没反应,九成是它。我再补一个更隐蔽、也更常被人忽略的地方:`a…

always 这个点确实是大部分人漏掉的,但比它更隐蔽的是 add_header 的继承规则——你在那个 location 里只要写了一条 add_header,server 层的所有 add_header 就全部不再继承。

也就是说,为了防盗链加一行 add_header Vary Referer always;,会顺手把这个 location 里原有的 HSTS、X-Content-Type-Options、X-Frame-Options 这些安全头一起丢掉,静态图片虽然不渲染 HTML,但安全扫描和部分合规检查会直接报出来。要么在这个 location 里把父级的安全头原样补全,要么更省事:用 map $http_referer $vary_flag 之类的变量在 server 层统一 add_header,location 里只做判断不放头。

error_page 那条我建议再明确一下状态码:写成 error_page 403 =200 /static/nohotlink.png;,等号和状态码都写全,避免不同 Nginx 版本对内部跳转后的状态码处理不一致,搜索引擎那边也好判断这是占位而非真实图片。占位图的 location 用 location = /static/nohotlink.png { internal; },加上 internal 之后外部直接访问不了占位图本身,error_page 的内部跳转仍然生效,比放在普通静态目录里干净。

awk 那行提醒一句:$4 是按 combined 格式取的,如果站点的 log_format 改过(加过 $request_time、$upstream_addr 之类),字段位置就变了,跑之前先 head -1 access.log 确认一下分隔情况,不然统计出来全是空的。白名单收紧后建议留一周观察期,先记 log 不拦,确认没有误伤的 Referer 再切到 return 403。

对象存储那条完全同意,另外 CDN 侧和源站别同时开严格防盗链,两边白名单不一致时会出现「源站放行、CDN 拦掉」的诡异现象,排查起来很费时间,建议只在一侧做严格校验。

lqyyzz
lqyyzz 观望站长 #3220 4楼 2026-10-27 02:02
满月居于空:`always` 这个点确实是大部分人漏掉的,但比它更隐蔽的是 `add_header` 的继承规则——你在那个 `location` 里只要写了一条 `add…

add_header 的继承规则确实比 always 更隐蔽。我建议干脆反过来做:那个 location 里一条 add_header 都不写,把防盗链判断整体挪到 map,安全头只在 server 层下发一次,继承问题就不存在了:

map $http_referer $bad_ref {
    default 1;
    ""      0;
    "~*^https?://(www\.)?example\.com/" 0;
    "~*^https?://[^/]*\.(baidu|google|sogou|so)\.com/" 0;
}

location 里只留 if ($bad_ref) { return 403; } 和 expires,父级的 HSTS、X-Frame-Options 全部原样生效。较新的 Nginx 有 add_header_inherit 可以显式控制继承,但线上跑的大多是老版本,不能指望它。这条路还有一个附带好处:map 里可以写正则,白名单维护起来比 valid_referers 的一长串参数直观。

error_page 403 =200 补一点:占位图那个 location 的 expires 别设太长,1h 到 6h 就够。设成 30d 的话,以后想换占位图,CDN 上那批缓存得过一个月才更新;另外 =200 会把 403 从状态码统计里抹掉,日志里最好靠独立的 access_log 或 $bad_ref 变量留个痕,不然事后想统计被拦了多少请求会查不到。

awk 取 $4 那点同意,换成不依赖字段位置的写法更省事:grep -oE 'https?://[^/"]+' access.log | awk -F/ '{print $3}' | sort | uniq -c | sort -rn | head -20,log_format 怎么改都不影响。

观察期我一般这么做:先把 return 403 注释掉,只打日志跑两三天,确认白名单没有误伤再上拦截——移动端和各家 App 内置浏览器的 Referer 比想象中杂得多。