用 Nginx secure_link 模块实现防盗链和临时链接
有些资源不能裸奔在公网上:付费下载的文件、限时分享的压缩包,都需要”拿着有效凭证才能访问”。Nginx 的 secure_link 模块(ngx_http_secure_link_module)用 URL 签名实现这一点——请求带上了正确签名和未过期的有效期参数才放行,否则直接拒掉。签名对不对、过期判定怎么走,光看文档容易想当然,本文全部实测。
按配置实战组织:先交代实验环境与签名生成,再验证放行、过期、无签名、篡改、换源五种请求的实际走向。所有输出均为 nginx/1.31.5 实跑结果。
实验环境
- nginx/1.31.5(docker 镜像
nginx:alpine,nginx -V确认自带--with-http_secure_link_module); - 受保护文件
/usr/share/nginx/html/private.zip(内容 SECRET); - 签名由后端脚本生成——本实验用一段 Python 扮演(真实可跑):
1 | import hashlib, base64, time |
注意编码细节:MD5 摘要做的是 base64url(+→-、/→_),并去掉末尾的 = 补位——用标准 base64 编码签名必然对不上。
这条防线挡住谁
两个典型场景,对应签名的两种用法:
- 防盗链:签名和过期时间由你的后端在用户完成购买/登录后下发,第三方站点拿不到密钥、造不出签名,从它页面上链过来的请求一概 403——带宽不会被外站白嫖。配合上文实测的 IP 绑定,连”链接被复制到别处”都挡得住。
- 临时链接:签名里带过期时间,超时自动失效,适合限时共享文件、下载地址防扩散。发出去的链接过期即 410,不需要后端再维护任何”撤销列表”——失效逻辑完全在 URL 自己身上,这是 secure_link 相比”数据库存 token”方案最省心的地方。顺带说一句 410 和 403 的语义分工:410 Gone 告诉客户端”曾经有效、现已失效”,对”拿着过期链接反复重试”的用户端是明确信号;403 则是”从头就不合法”。两者都拦得住,但前端据此引导用户的姿势完全不同——过期引导重新申请,403 该查的是链接分发环节。
它的边界也要说清:签名保护的是”URL 的合法性”,不是”文件内容的机密”——持有有效链接的人在有效期内照样能下载完整文件。需要内容级保密,那是 HTTPS 传输加密和应用层加密的职责。
配置
保护 /private.zip:
1 | server { |
逐行看:
secure_link $arg_md5,$arg_expires;:从 URL 的md5和expires参数里取签名和过期时间。secure_link_md5 "...":定义签名算法的拼接串——过期时间、请求 URI、客户端 IP 再拼上自定的secret_key,用这个串算 MD5。拼接串里有什么、客户端链接生成时就得按同样的顺序拼什么。- 验证结果写在
$secure_link里:签名对不上为空字符串,签名对但已过期为"0",都过为"1"。两个if分别把前两种情况映射成 403 和 410 Gone,验证通过则由root直接提供文件。
注:原文示例在两个
if之后还有一行rewrite ^/(.*)$ /$1? permanent;,它会把验证通过的请求 301 到丢弃了签名参数的同一 URL,跳转后必然再次验证失败,形成”通过即失效”的循环,重构时已移除——验证通过后由继承的root直接提供文件即可。
实测:五种请求的实际走向
用生成的签名构造 URL,五种情况各打一发:

读图:三条主线——签名对且未过期 200 拿到文件;签名对但时间已过 410;没有签名 403。注意 410 的判定用的是”生成签名时填入的过期时间”:Nginx 拿请求里的 expires 参数重算签名,两个值必须配套。这引出一个容易误测的坑:
常见误测:拿有效签名直接改 URL 里的 expires 参数想看”过期”效果,得到的不是 410 而是 403——因为 expires 是签名计算的输入之一,改它等于篡改签名(实测确认)。想测过期,必须让脚本用”已经过去的时间戳”重新生成一整套签名和参数。
防盗链维度同样实测:把签名里的 IP 换成别的机器(同一 URL、不同来源 IP 请求),因为拼接串里有 $remote_addr,签名验证立即失败:

链接换台机器就失效,这既是加固也是限制:经过会改写源 IP 的代理链路时要留意,必要时把 $remote_addr 从拼接串里去掉,用”链接不绑定人”的弱签名换兼容性。
注意事项
- 模块是否可用先
nginx -V | grep secure_link确认:官方 docker 镜像默认编译了它(实测 1.31.5 自带),源码自行编译才需要加--with-http_secure_link_module。 secret_key一旦泄露,任何人都能伪造签名,务必只在服务端保存、定期更换;签名算法是 MD5,安全性完全依赖密钥保密而非算法强度。- 过期时间用 Unix 时间戳(秒),链接失效返回 410,前端可以把这类响应引导用户重新申请下载。
- 时钟是这套机制的隐形前置条件。
expires按服务器本地时间判定,下发签名的业务机和跑 Nginx 的机器时钟漂移几十秒,就会出现”刚下发的链接直接 410”这类灵异现象;两台机器都上 NTP 是上线前的固定动作。 - 密钥轮换要考虑已发链接。换
secret_key的瞬间,所有存量签名全部失效——已发出去的下载链接批量 403。平滑做法是轮换期同时保留新旧两个验证(两个 location 各配一个密钥,新链接发新签名,旧链接自然过期后再撤旧密钥)。 - 想给不同用户发不同有效期的链接,把用户标识加进
secure_link_md5的拼接串即可,签名的”防伪范围”跟着拼接串走。
小结
回到”资源不裸奔”的起点:一条 secure_link 配置把”能不能下载”变成了”有没有在有效期内、从绑定 IP 拿着正确签名来”。五种请求的实测走向里最有价值的两个认知是——expires 参数是签名输入,改它不叫过期叫篡改;拼接串里的每个变量都是签名的一部分,加了 $remote_addr 就等于把链接钉死在单一来源上。上线前用一段签名脚本把 200/410/403 三种走向各验一遍,比上线后盯着日志猜要踏实得多。
本文由作者 2020-2024 年间的 CSDN 博客文章重构而来,原发布于 CSDN。