nginx

关注公众号 jb51net

关闭
首页 > 网站技巧 > 服务器 > nginx > Nginx referer静态资源防盗链

Nginx中referer模块的静态资源防盗链配置教学

作者:知远漫谈

本文将带你深入 Nginx 的 referer 模块,系统性地掌握防盗链技术的底层原理、实战配置、边界场景处理、Java 后端协同策略,以及如何构建一个安全、高效、可扩展的静态资源防护体系

在当今互联网高速发展的时代,静态资源(如图片、视频、CSS、JS 文件)已成为网站性能优化的核心组成部分。它们承载着视觉表达、交互体验和用户留存的关键任务。然而,随着内容分发网络的普及与资源复用的便捷性,一个令人头疼的问题悄然浮现——静态资源被盗链

你是否曾见过这样的场景?一个小型博客网站,服务器带宽每月仅 50GB,却因为一张热门图片被多个大型论坛、公众号、甚至电商平台直接嵌入,导致月流量飙升至 500GB,服务器崩溃,账单暴增。这并非科幻小说,而是无数中小站长的真实噩梦。而这一切的根源,正是HTTP Referer 头的滥用

本文将带你深入 Nginx 的 referer 模块,系统性地掌握防盗链技术的底层原理、实战配置、边界场景处理、Java 后端协同策略,以及如何构建一个安全、高效、可扩展的静态资源防护体系。我们将通过真实代码示例、可视化流程图、多维度测试方案,让你不仅“会配置”,更能“懂原理”、“能优化”、“可扩展”。

什么是盗链?为什么它如此致命?

盗链(Hotlinking),又称“外链引用”或“带宽窃取”,是指第三方网站在未经许可的情况下,直接引用你服务器上的静态资源(如图片、音频、视频)到自己的网页中。

盗链的典型表现

假设你有一个图片资源:https://your-website.com/images/logo.png

某论坛的管理员在帖子中直接写入:<img src="https://your-website.com/images/logo.png" alt="Logo">

当用户访问该论坛时,浏览器会向你的服务器发起请求,加载这张图片。你的服务器承担了:

而论坛主呢?他零成本获得了高质量视觉内容,提升了页面美观度,还可能因此获得更多点击与广告收入。

盗链的危害

危害类型说明
成本飙升带宽费用呈指数级增长,尤其在云服务商(如阿里云、腾讯云)按量计费下,月支出可能从 50 元暴涨至 5000 元
性能下降你的服务器资源被大量盗链请求占用,导致合法用户访问变慢,甚至超时
SEO 受损搜索引擎可能认为你的内容被大量复制,降低权重
安全风险某些盗链者会伪装 Referer,结合 XSS 或 CSRF 攻击,利用你的资源作为跳板

Nginx Referer 模块:防盗链的基石

Nginx 本身不直接提供“防盗链”功能,但其强大的 ngx_http_referer_module 模块,允许我们基于 HTTP 请求头中的 Referer 字段进行灵活的访问控制。

什么是 Referer?

Referer 是 HTTP 请求头中的一个字段,用于标识当前请求是从哪个页面跳转而来的。例如:

注意:Referer 字段不是强制的,浏览器可以不发送,或被用户代理(如隐私模式、插件)屏蔽。但它在绝大多数正常场景下是可靠的。

Nginx 防盗链核心指令

指令说明
valid_referers定义合法的 Referer 来源,支持域名、IP、空值等
invalid_referer可选,用于标记非法 Referer(配合 set 使用)
return / rewrite配合 valid_referers 实现拒绝、重定向、返回占位图等策略

工作原理图解(Mermaid)

这个流程图清晰展示了 Nginx 如何基于 Referer 做出决策。每一个判断点都是可配置的,灵活性极高。

防盗链实战配置:从基础到高阶

我们以一个典型电商网站为例,域名:shop.example.com,静态资源部署在 /static/ 路径下,包含图片、CSS、JS、字体等。

基础配置:仅允许本站访问

server {
    listen 80;
    server_name shop.example.com;

    location ~* \.(jpg|jpeg|png|gif|webp|ico|bmp|svg)$ {
        valid_referers none blocked shop.example.com www.shop.example.com;
        
        if ($invalid_referer) {
            return 403;
        }

        root /var/www/html;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}

配置详解:

valid_referers none blocked shop.example.com www.shop.example.com;

if ($invalid_referer)valid_referers 会自动设置 $invalid_referer 变量,若 Referer 不合法,则值为 1

return 403;:直接拒绝访问,返回 HTTP 403 Forbidden

重要提示valid_referers 不支持通配符(如 *.example.com),如需泛域名支持,需使用正则表达式(见下文)。

高阶配置:支持泛域名 + 第三方合作站

假设你的品牌授权了 3 家合作伙伴网站:

你希望允许这些站点引用你的图片,但拒绝其他所有来源。

location ~* \.(jpg|jpeg|png|gif|webp|ico|bmp|svg)$ {
    valid_referers none blocked server_names
                   *.partner1.com
                   *.partner2.net
                   mall.example.org;

    if ($invalid_referer) {
        return 403;
    }

    root /var/www/html;
    expires 30d;
    add_header Cache-Control "public, immutable";
}

解析:

如果你需要更复杂的匹配(如多个顶级域名),请使用正则表达式。

正则表达式:灵活匹配多域名

假设你希望允许:

location ~* \.(jpg|jpeg|png|gif|webp|ico|bmp|svg)$ {
    valid_referers none blocked server_names
                   ~^https?://(www\.)?(partner1|partner2)\.com(/.*)?$
                   ~^https?://cdn\.partner3\.org(/.*)?$
                   ~^https?://mybrand-[^/]+\.com(/.*)?$;

    if ($invalid_referer) {
        return 403;
    }

    root /var/www/html;
    expires 30d;
    add_header Cache-Control "public, immutable";
}

正则说明:

表达式说明
^https?://匹配 http:// 或 https://
(www\.)?可选的 www 子域名
`(partner1partner2).com`
(/.*)?可选路径(避免因路径不同导致匹配失败)
cdn\.partner3\.org精确匹配,注意转义点
mybrand-[^/]+\.com匹配 mybrand- 开头 + 任意非斜杠字符 + .com

正则表达式是 Nginx 防盗链的终极武器,但需注意性能开销。建议仅在必要时使用。

替代方案:返回占位图 vs 重定向 vs 444 关闭连接

方案一:返回 403 Forbidden(最严格)

if ($invalid_referer) {
    return 403;
}

方案二:返回占位图(推荐!)

location ~* \.(jpg|jpeg|png|gif|webp|ico|bmp|svg)$ {
    valid_referers none blocked server_names
                   ~^https?://(www\.)?(partner1|partner2)\.com(/.*)?$;

    if ($invalid_referer) {
        rewrite ^/(.*)$ /static/hotlink-blocked.png last;
    }

    root /var/www/html;
    expires 30d;
    add_header Cache-Control "public, immutable";
}

需提前准备一张占位图:/var/www/html/static/hotlink-blocked.png

优势

方案三:重定向到首页(温和策略)

if ($invalid_referer) {
    return 302 https://shop.example.com;
}

方案四:返回 444(最隐蔽)

if ($invalid_referer) {
    return 444;
}

注意:444 会导致浏览器控制台报错 “ERR_EMPTY_RESPONSE”,调试时需谨慎。

防盗链测试:如何验证你的配置是否生效?

方法一:使用 curl 模拟盗链请求

# 正常访问(无 Referer)
curl -I https://shop.example.com/images/logo.png

# 模拟来自百度的盗链
curl -H "Referer: https://www.baidu.com" -I https://shop.example.com/images/logo.png

# 模拟来自自家网站
curl -H "Referer: https://shop.example.com/page.html" -I https://shop.example.com/images/logo.png

观察响应头:

方法二:使用 Postman / Thunder Client(VSCode 插件)

  1. 创建 GET 请求:https://shop.example.com/images/logo.png
  2. 在 Headers 中添加:
    • Key: Referer,Value: https://evil-site.com
  3. 发送请求,观察状态码

方法三:浏览器开发者工具

  1. 打开 Chrome DevTools → Network
  2. 刷新页面,找到一张图片请求
  3. 右键 → “Copy as cURL”
  4. 在终端中执行,观察响应

方法四:构造 HTML 测试页(本地)

创建一个本地 HTML 文件 test.html

<!DOCTYPE html>
<html>
<head>
    <title>盗链测试页</title>
</head>
<body>
    <h1>测试是否被防盗链</h1>
    <img src="https://shop.example.com/images/logo.png" alt="测试图" width="300">
</body>
</html>

用浏览器打开此文件,观察图片是否加载成功。

建议:将此测试页部署在你的测试域名(如 test.yourdomain.com)上,模拟真实盗链场景。

Java 后端协同:动态生成 Referer 白名单

虽然 Nginx 防盗链强大,但静态配置无法应对动态合作方变更。比如:

解决方案:Java + Redis + Nginx 动态白名单

我们采用以下架构:

[Java 服务] ←→ [Redis] ←→ [Nginx]
    ↑              ↑
  动态配置       Nginx 读取白名单

Step 1:Java 服务管理白名单(Spring Boot 示例)

@RestController
@RequestMapping("/api/referer")
public class RefererWhitelistController {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    // 添加合法域名
    @PostMapping("/add")
    public ResponseEntity<String> addReferer(@RequestBody String domain) {
        if (domain == null || domain.trim().isEmpty()) {
            return ResponseEntity.badRequest().body("域名不能为空");
        }
        // 去除协议头和路径,只保留域名
        String cleanDomain = domain.replaceAll("^(https?://)?(www\\.)?", "").split("/")[0];
        redisTemplate.opsForSet().add("referer_whitelist", cleanDomain);
        return ResponseEntity.ok("已添加: " + cleanDomain);
    }
    // 获取所有白名单
    @GetMapping("/list")
    public ResponseEntity<List<String>> getWhitelist() {
        Set<String> domains = redisTemplate.opsForSet().members("referer_whitelist");
        return ResponseEntity.ok(new ArrayList<>(domains));
    }
    // 删除域名
    @DeleteMapping("/remove")
    public ResponseEntity<String> removeReferer(@RequestBody String domain) {
        String cleanDomain = domain.replaceAll("^(https?://)?(www\\.)?", "").split("/")[0];
        Long removed = redisTemplate.opsForSet().remove("referer_whitelist", cleanDomain);
        if (removed > 0) {
            return ResponseEntity.ok("已移除: " + cleanDomain);
        } else {
            return ResponseEntity.notFound().build();
        }
    }
}

Step 2:Nginx 读取 Redis 白名单(需安装 ngx_http_lua_module)

此方案需要 Nginx 编译时启用 lua 模块(如 OpenResty),非标准 Nginx 默认支持。

# nginx.conf

http {
    lua_package_path "/usr/local/openresty/lualib/?.lua;;";

    init_by_lua_block {
        local redis = require "resty.redis"
        local red = redis:new()
        red:set_timeouts(1000, 1000, 1000)
        local ok, err = red:connect("127.0.0.1", 6379)
        if not ok then
            ngx.log(ngx.ERR, "failed to connect to redis: ", err)
        end
        -- 预加载白名单
        local whitelist, err = red:smembers("referer_whitelist")
        if whitelist then
            ngx.var.referer_whitelist = table.concat(whitelist, " ")
        end
    }

    server {
        listen 80;
        server_name shop.example.com;

        location ~* \.(jpg|jpeg|png|gif|webp|ico|bmp|svg)$ {
            # 动态从 Lua 变量读取白名单
            set $valid_referers_list "none blocked server_names $referer_whitelist";
            
            # 由于 Nginx 不支持变量在 valid_referers 中,需使用更高级的 lua 实现
            access_by_lua_block {
                local referer = ngx.var.http_referer
                local whitelist = ngx.var.referer_whitelist

                if not referer then
                    return  -- 允许空 Referer
                end

                -- 解析域名
                local domain = string.match(referer, "^[^/]+//([^/]+)")
                if not domain then
                    ngx.exit(403)
                end

                -- 检查是否在白名单中
                local found = false
                for d in string.gmatch(whitelist, "[^%s]+") do
                    if domain == d or string.match(domain, "^%." .. d .. "$") then
                        found = true
                        break
                    end
                end

                if not found then
                    ngx.log(ngx.WARN, "Blocked hotlink from: ", domain)
                    ngx.exit(403)
                end
            }

            root /var/www/html;
            expires 30d;
            add_header Cache-Control "public, immutable";
        }
    }
}

适用场景对比表

方案适用场景是否需重启 Nginx维护成本推荐指数
静态配置合作方固定(<5 个)⭐⭐⭐⭐
正则表达式多域名、子域名⭐⭐⭐⭐
占位图 + 静态通用网站、电商⭐⭐⭐⭐⭐
Redis + Lua动态合作、SaaS 平台⭐⭐⭐⭐

防盗链 + CDN 混合部署:最佳实践

大多数企业都会使用 CDN(如阿里云 CDN、Cloudflare)加速静态资源。但 CDN 会改变 Referer 的来源!

错误做法

valid_referers shop.example.com;  # 仅允许本站

当你把图片托管在 CDN(如 https://cdn.shop.example.com)时,用户从 shop.example.com 访问,请求的 Referer 是 shop.example.com,但CDN 回源时的 Referer 是 CDN 的域名

结果:CDN 回源失败 → 图片无法加载!

正确做法:CDN 防盗链 + 源站双重防护

方案:源站只允许 CDN 回源,CDN 层做用户访问控制

用户 → CDN → (回源) → Nginx 源站

Nginx 源站配置(仅允许 CDN IP):

# 限制仅允许 CDN 回源访问
location ~* \.(jpg|jpeg|png|gif|webp|ico|bmp|svg)$ {
    # 仅允许阿里云 CDN IP 段(示例)
    allow 101.226.0.0/16;
    allow 101.227.0.0/16;
    deny all;

    # 不检查 Referer,因为回源是 CDN 发起的
    root /var/www/html;
    expires 30d;
}

CDN 防盗链配置(以阿里云 CDN 为例):

这样,只有从你官网跳转的请求才能通过 CDN 加载图片,而 CDN 回源不受限制。

实际 CDN 防盗链配置示例(阿里云)

项目配置值
防盗链类型白名单
白名单内容https://shop.example.com/*
是否允许空 Referer✅ 是
是否忽略查询参数✅ 是
是否开启 Referer 优先级✅ 是

重要:CDN 防盗链规则比 Nginx 更高效,因为它在边缘节点就拦截,节省了回源带宽!

特殊场景处理:微信、抖音、App 内嵌浏览器

微信公众号、抖音、快手、微博等 App 内嵌浏览器,默认不发送 Referer,或发送 https://mp.weixin.qq.com

问题:你的防盗链配置阻止了微信公众号中的图片显示!

valid_referers shop.example.com www.shop.example.com;

微信公众号访问你的图片 → Referer: https://mp.weixin.qq.com → 被拦截 → 图片空白!

解决方案:

location ~* \.(jpg|jpeg|png|gif|webp|ico|bmp|svg)$ {
    valid_referers none blocked
                   shop.example.com www.shop.example.com
                   mp.weixin.qq.com
                   douyin.com
                   kuaishou.com
                   weibo.com;

    if ($invalid_referer) {
        return 403;
    }

    root /var/www/html;
    expires 30d;
    add_header Cache-Control "public, immutable";
}

微信公众号的 Referer 是 https://mp.weixin.qq.com,不是 mp.weixin.qq.com,请确保包含协议头!

更安全的做法:使用正则匹配微信域名

valid_referers none blocked server_names
               ~^https?://(www\.)?(shop\.example\.com|mp\.weixin\.qq\.com|douyin\.com|kuaishou\.com|weibo\.com)(/|$);

小贴士:如何获取真实 Referer?

在微信公众号文章中插入一张图片,然后:

  1. 手机微信 → 长按图片 → “在浏览器中打开”
  2. 打开开发者工具 → Network → 查看图片请求的 Referer
  3. 将其加入白名单

防盗链的边界与陷阱

陷阱一:误封搜索引擎爬虫

百度、Google 爬虫抓取你的图片用于图片搜索,它们的 Referer 是空的或伪装的。

valid_referers none blocked server_names;

正确做法:允许空 Referer (none),这是搜索引擎的默认行为。

陷阱二:移动端 App 无 Referer

许多原生 App(如 iOS/Android)使用 URLConnectionOkHttp 请求图片,默认不带 Referer

解决方案:

location ~* ^/app-images/ {
    # 不做防盗链,由 App 通过 Token 鉴权
    root /var/www/html;
    expires 30d;
}

location ~* \.(jpg|jpeg|png|gif|webp)$ {
    # 仅对网站图片启用防盗链
    valid_referers none blocked server_names;
    if ($invalid_referer) { return 403; }
    root /var/www/html;
    expires 30d;
}

陷阱三:HTTPS 跨域 Referer 丢失

当你的网站是 https://shop.example.com,而引用者是 http://blog.com(HTTP),浏览器出于安全策略,不会发送 Referer

解决方案:

<meta name="referrer" content="strict-origin-when-cross-origin">

推荐策略:strict-origin-when-cross-origin,在跨域时只发送协议+域名,不暴露路径

性能与安全权衡:防盗链的代价

性能影响分析

配置方式CPU 开销内存开销响应延迟
静态域名极低0ms
正则表达式1~5ms
Lua + Redis5~20ms
CDN 防盗链极低0ms(边缘节点)

推荐组合CDN 防盗链(主) + Nginx 静态白名单(辅) + 占位图兜底

安全性对比

方案抗伪造能力抗绕过能力推荐度
静态域名⭐⭐⭐
正则表达式⭐⭐⭐⭐
Lua + Redis极高⭐⭐⭐⭐⭐
444 关闭极高极高⭐⭐⭐⭐

防盗链不是万能的:恶意用户可通过代理、伪造 Referer(如使用 curl -H "Referer: shop.example.com")、使用爬虫工具(如 Puppeteer)绕过。
真正的安全应是:防盗链 + Token 鉴权 + IP 限速 + 日志监控

监控与日志:如何发现盗链行为?

Nginx 日志增强

修改 nginx.conflog_format

log_format hotlink '$remote_addr - $remote_user [$time_local] "$request" '
                   '$status $body_bytes_sent "$http_referer" "$http_user_agent" '
                   '$invalid_referer';

access_log /var/log/nginx/access.log hotlink;

日志分析脚本(Python)

import re
from collections import Counter
with open('/var/log/nginx/access.log') as f:
    lines = f.readlines()
hotlink_count = Counter()
for line in lines:
    if '"-" ' in line or ' 403 ' in line:
        match = re.search(r'(\S+) - - \[.*\] "GET .*" 403 (\d+) "([^"]+)"', line)
        if match:
            referer = match.group(3)
            if referer != '-' and not referer.startswith('https://shop.example.com'):
                hotlink_count[referer] += 1
print("Top 10盗链来源:")
for ref, count in hotlink_count.most_common(10):
    print(f"{count:4d} 次 | {ref}")

输出示例:

Top 10盗链来源:
  2845 次 | https://evil-site.com
  1987 次 | https://forum.example.org
   892 次 | https://t.me
   345 次 | https://twitter.com

每周运行一次,发现异常来源,及时加入黑名单!

防盗链之外:更高级的资源保护方案

方案一:Token 鉴权(适用于敏感资源)

location ~* ^/private/ {
    # 生成临时 token,有效期 5 分钟
    set $token $arg_token;
    if ($token = "") {
        return 403;
    }
    # 校验 token 是否合法(可结合 Lua + Redis)
    access_by_lua_file /etc/nginx/check_token.lua;
    root /var/www/html;
}

Java 生成 Token:

@GetMapping("/image/{id}/token")
public ResponseEntity<String> generateImageToken(@PathVariable String id) {
    String token = UUID.randomUUID().toString().replace("-", "");
    redisTemplate.opsForValue().set("token:" + token, id, Duration.ofMinutes(5));
    return ResponseEntity.ok("https://shop.example.com/private/" + id + "?token=" + token);
}

方案二:签名 URL(类似阿里云 OSS)

https://shop.example.com/images/photo.jpg?Expires=1728000000&Signature=abc123&OSSAccessKeyId=key123

Java 签名逻辑(伪代码):

String sign = HMACSHA256(secretKey, "GET&/images/photo.jpg&Expires=1728000000");

Nginx 验证签名(需 Lua):

-- 检查 Expires 是否过期
-- 检查 Signature 是否匹配
-- 检查 OSSAccessKeyId 是否合法

方案三:WebP + 自适应格式 + 懒加载

<picture>
  <source srcset="/images/photo.webp" type="image/webp">
  <source srcset="/images/photo.jpg" type="image/jpeg">
  <img src="/images/photo.jpg" alt="Photo" loading="lazy">
</picture>

既能提升体验,又能减少盗链价值(盗链者拿到的是 WebP,兼容性差)

最佳实践总结

最终推荐配置模板(开箱即用)

server {
    listen 80;
    server_name shop.example.com;

    # 静态资源防盗链配置
    location ~* \.(jpg|jpeg|png|gif|webp|ico|bmp|svg|css|js|woff|woff2|ttf|eot)$ {
        valid_referers none blocked server_names
                       shop.example.com www.shop.example.com
                       mp.weixin.qq.com douyin.com kuaishou.com weibo.com;

        if ($invalid_referer) {
            rewrite ^/ /static/hotlink-blocked.png last;
        }

        root /var/www/html;
        expires 365d;
        add_header Cache-Control "public, immutable";
        add_header Vary Accept-Encoding;
    }

    # 占位图单独放行(避免循环重定向)
    location = /static/hotlink-blocked.png {
        root /var/www/html;
        expires 7d;
        add_header Content-Type image/png;
    }

    # 其他静态资源
    location / {
        root /var/www/html;
        try_files $uri $uri/ =404;
    }
}

附录:防盗链配置自查清单

检查项是否完成
已允许空 Referer (none)☑️
已允许被屏蔽 Referer (blocked)☑️
已包含自有域名☑️
已包含微信、抖音等平台域名☑️
已测试本地 HTML 引用☑️
已测试 curl 模拟盗链☑️
已配置占位图而非 403☑️
已启用缓存头 expiresCache-Control☑️
已排除 App 资源路径☑️
已部署日志监控脚本☑️

到此这篇关于Nginx中referer模块的静态资源防盗链配置教学的文章就介绍到这了,更多相关Nginx referer静态资源防盗链内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文