概述
本章,您将学习到 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 为例)生成签名的步骤:
- 拼接字符串 - 必须按照
secure_link_md5指令定义的顺序拼接字符串,也就是 "密钥 + URL 路径 + 过期时间戳" - 计算 MD5 值 - 获取该字符串的 MD5 二进制原始值(16 bytes)
- Base 64 编码 - 对二进制 MD5 值进行标准 Base64 编码
-
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










