Nginx功能篇14—防盗链

概述

本章,您将学习到 Nginx 中的防盗链功能。

防盗链:专门用于限制其他网站未经授权直接引用本站静态资源的技术手段,核心目的是降低服务器的无效带宽消耗、保护原创内容版权、降低运营成本。静态资源(音频、视频、图片、CSS、js等)是 Web 网站的重要组成部分,然而,这些资源可能会被其他网站引用,进而消耗服务器的带宽,当引用过多时会严重降低网站的性能,因此需要通过一些技术手段进行限制。在全球大多数国家和地区,盗链行为首先被认定为侵犯著作权的民事侵权行为,情节特别严重、满足法定立案标准的,还会被追究刑事责任。

防盗链的实现

主要通过这两种方式实现防盗链的拦截效果:

  • Referer 字段校验 - 浏览器加载资源时会自动在 HTTP 请求头中携带 Referer 字段,记录当前请求是从哪个页面跳转而来。Web 服务器会将该字段值与预设的合法域名白名单进行比对,仅放行匹配的请求,非法请求直接返回 403 状态码或自定义提示内容。这种拦截方式在中小网站中使用较多,可以拦截绝大多数普通盗链请求

    • 在 Nginx 中由 ngx_http_referer_module 模块提供
  • Token 签名验证 - 不依赖浏览器请求头,由服务端为合法资源请求生成带加密签名的临时链接,链接中通常附带时间戳参数限制有效期。浏览器访问时必须携带该 Token,Web 服务器会重新计算签名校验,通过之后才返回资源,安全性远高于 Referer 校验。

    • 在 Nginx 中由 ngx_http_secure_link_module 模块提供

Referer 字段校验

在 Nginx 中,与之相关的指令有:

  • valid_referers 指令 - 指定合法请求的白名单。该简单指令并不直接拦截请求,而是通过与内置变量 $invalid_referer 进行比对完成对普通盗链请求的放行或拦截,常搭配 if 指令或 return 指令 或 rewrite 指令一起使用,见如下示例:

    ...
    http {
        ...
        server {
            ...
            location /images/ {
                valid_referers none blocked games.com *.google.com;
                if ($invalid_referer) {
                    return 403;
                }
            }
        }
    }

    valid_referers none blocked games.com *.google.com; 中,none 表示允许请求头中缺失 Referer 字段的情况;blocked 表示允许 Referer 字段存在,但其值不以 http://https:// 开头。

    此处使用了 valid_referers 指令定义了白名单。$invalid_referer 值为 1 (真/True),表示请求头中的 Referer 字段‌不在‌白名单内;值为 ""(空/False),表示请求合法,在白名单范围内。使用 if 指令进行条件判断,为真则执行大括号内的指令。

Token 签名验证

这种方式在某些高价值资源场景中使用广泛,如:

  • 付费音频和视频
  • 付费图片或素材
  • 付费文档或软件
  • 教育平台
  • 私密资源

需要注意的是,这种方式并不是所有网站的标准配置。

由于我当前 Nginx 在源码编译时没有使用 --with-http_secure_link_module 编译选项,导致 Nginx 没有包含对应的 ngx_http_secure_link_module 模块,解决办法为重新编译安装。

# 当前 Nginx 的编译选项信息
Shell > /usr/local/nginx/sbin/nginx -V
nginx version: nginx/1.31.5
built by gcc 8.5.0 20210514 (Red Hat 8.5.0-28) (GCC)
built with OpenSSL 1.1.1k  FIPS 25 Mar 2021
TLS SNI support enabled
configure arguments: --prefix=/usr/local/nginx --sbin-path=/usr/local/nginx/sbin/nginx --modules-path=/usr/local/nginx/modules --user=nginx --group=nginx --with-http_ssl_module --with-http_v2_module --with-http_v3_module --with-http_realip_module --with-http_gzip_static_module --with-http_dav_module --with-stream --with-threads --with-file-aio --with-compat

# 在 Windows 10/11 的 PowerShell 交互终端中完成对主配置文件 nginx.conf 的备份
PS > cd E:\
PS > scp -P 22 root@192.168.100.20:/usr/local/nginx/conf/nginx.conf  .

# 删除已存在的 Nginx
Shell > rm -rf /usr/local/nginx

# 进入到解压提取后的目录位置
Shell > cd /usr/local/src/nginx-1.31.5/

Shell > make clean

# 开始编译安装
Shell > ./configure \
--prefix=/usr/local/nginx \
--sbin-path=/usr/local/nginx/sbin/nginx \
--modules-path=/usr/local/nginx/modules \
--user=nginx \
--group=nginx \
--with-http_ssl_module \
--with-http_v2_module \
--with-http_v3_module \
--with-http_realip_module \
--with-http_gzip_static_module \
--with-http_dav_module \
--with-stream \
--with-threads \
--with-file-aio \
--with-http_secure_link_module \
--with-compat \
&& make && make install

# 再次查看
Shell > /usr/local/nginx/sbin/nginx -V
nginx version: nginx/1.31.5
built by gcc 8.5.0 20210514 (Red Hat 8.5.0-28) (GCC)
built with OpenSSL 1.1.1k  FIPS 25 Mar 2021
TLS SNI support enabled
configure arguments: ... --with-http_secure_link_module ...

# 将备份的主配置文件放入到 conf 目录中
Shell > rm -rf /usr/local/nginx/conf/nginx.conf
PS > cd E:\
PS > scp -P 22 ./nginx.conf root@192.168.100.20:/usr/local/nginx/conf/

Nginx 的 ngx_http_secure_link_module 模块提供两种工作模式:

  • 不需要后端程序参与的纯 Nginx 配置(模式一) - 简单的轻量配置,链接格式固定且规则简单,完全不需要后端程序生成签名
  • 需要后端程序生成签名(模式二) - 在生产环境中最常使用

模式二通过校验 URL 中的两个关键参数来实现安全访问:

  • ‌st (Secure Token)‌:签名字符串,由后端根据密钥、资源路径、过期时间等计算得出
  • e (Expires)‌:过期时间戳(Unix Timestamp,秒级)

Nginx 的处理逻辑:

  • 提取 URL 中的 $arg_st$arg_e
  • 检查当前时间是否超过 $arg_e。如果超时,$secure_link 变量值为 "0"。
  • 如果未超时,Nginx 按照配置的公式(如 md5(secret + uri + expires))重新计算 MD5 值
  • 将计算出的 MD5 值进行 URL-safe Base64 编码,并与 $arg_st 比对

    • $secure_link = "":签名校验失败
    • $secure_link = "0":签名正确,但已过期
    • $secure_link = "1":签名正确且未过期

Nginx 中的配置示例:

...
http {
    ...
    server {
        ...
        location /images/ {
            ...
            # 1. 指定从 URL 中提取签名(st)和过期时间(e)
            # 注意:参数名可以自定义,但必须与后端生成时一致
            secure_link $arg_st,$arg_e;

            # 2. 定义签名计算规则
            # $uri: 请求的资源路径(不包含查询参数)
            # $arg_e: URL 中的过期时间参数
            # mysecretkey: 只有后端和 Nginx 知道的密钥
            # 注意:secure_link_md5 不会自动添加分隔符,Nginx 会原样拼接字符串,因此后端必须与 Nginx 的拼接规则完全一致,否则签名必然失败。
            secure_link_md5 "mysecretkey$uri$arg_e";

            # 3. 根据校验结果执行操作
            if ($secure_link = "") {
                # 签名不匹配(伪造或密钥错误)
                return 403; 
            }

            if ($secure_link = "0") {
                # 签名匹配但已过期
                return 410; 
            }

            # 4. 签名有效,正常提供文件
            alias /var/www/attachment/;
        }
    }
}

后端程序(以 Golang 为例)生成签名的步骤:

  1. 拼接字符串 - 必须按照 secure_link_md5 指令定义的顺序拼接字符串,也就是 "密钥 + URL 路径 + 过期时间戳"
  2. 计算 MD5 值 - 获取该字符串的 ‌MD5 二进制原始值‌(16 bytes)
  3. Base 64 编码 - 对二进制 MD5 值进行标准 Base64 编码
  4. ‌URL-safe 转换

    • + 替换为 -
    • / 替换为 _
    • 去除末尾的 = 填充符

Golang 代码如下:

package main

import (
    "crypto/md5"
    "encoding/base64"
    "fmt"
    "strings"
    "time"
)

// GenerateSecureLinkToken 生成 Nginx secure_link 签名 Token
// secret: Nginx 配置的密钥 (例如: "mysecretkey")
// uri: 请求的资源路径 (例如: "/images/file.zip"),必须与用户最终访问的 URI 完全一致
// expireTime: 过期时间戳 (Unix Timestamp, 秒级)
func GenerateSecureLinkToken(secret string, uri string, expireTime int64) string {
    // 1. 拼接签名字符串
    // 注意:这里的拼接顺序必须与 Nginx 配置中的 secure_link_md5 指令完全一致
    signString := fmt.Sprintf("%s%s%d", secret, uri, expireTime)

    // 2. 计算 MD5 (获取二进制原始值)
    hash := md5.Sum([]byte(signString))

    // 3. Base64 编码
    // 使用标准 Base64 编码
    base64Hash := base64.StdEncoding.EncodeToString(hash[:])

    // 4. 转换为 URL-safe 格式
    // Nginx 期望: '+' -> '-', '/' -> '_', 去除末尾 '='
    token := strings.ReplaceAll(base64Hash, "+", "-")
    token = strings.ReplaceAll(token, "/", "_")
    token = strings.TrimRight(token, "=")

    return token
}

// BuildSecureURL 构建完整的防盗链 URL
func BuildSecureURL(baseUrl string, uri string, secret string, validitySeconds int) string {
    expireTime := time.Now().Unix() + int64(validitySeconds)
    token := GenerateSecureLinkToken(secret, uri, expireTime)

    baseUrl = strings.TrimRight(baseUrl, "/")
    // URL 参数名 st / e 需与 Nginx 配置中的 $arg_st、$arg_e 对应
    return fmt.Sprintf("%s%s?st=%s&e=%d", baseUrl, uri, token, expireTime)
}

func main() {
    secret := "mysecretkey"
    uri := "/images/test.zip"
    validity := 3600 // 1小时有效期
    baseUrl := "http://example.com"

    url := BuildSecureURL(baseUrl, uri, secret, validity)
    fmt.Println("Generated URL:", url)
}

这段 Golang 代码运行的结果如下所示:

go run main.go
Generated URL: http://example.com/images/test.zip?st=MhAxbKgxnkeGuF9x1-YDhQ&e=1789896353
进程已结束,退出代码 0
Avatar photo

关于 陸風睿

GNU/Linux 从业者、开源爱好者、技术钻研者,撰写文档既是兴趣也是工作内容之一。Q - "281957576";WeChat - "jiulongxiaotianci",Github - https://github.com/jimcat8
用一杯咖啡支持我们,我们的每一篇[文档]都经过实际操作和精心打磨,而不是简单地从网上复制粘贴。期间投入了大量心血,只为能够真正帮助到您。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇