优快云 @chinalogs (HiFeiz)
NGINX 的 secure_link 模块看上去很简单,实则暗藏杀机。一不小心就会掉进它的陷阱,最终造成调试时间飙升,宕机风险倍增,项目进度直接延期。
本文记录我在项目中如何踩了它的坑,又是怎么一点点排查出来,最终完成正确配置,并用 PHP、Python、ASP、JSP、Node.js 等语言生成兼容的签名。
一、需求背景
在实际场景中,我们希望限制静态资源的访问权限,比如视频 .ts、.m3u8、.mp4 等,仅允许带签名、并在有效期内的请求通过,防止盗链与滥用。
目标路径:/storage/** 中的静态媒体资源。
签名规则设定如下:
签名字符串 = 请求完整文件路径(含文件名)+ 用户标识 + 客户端 IP + 过期时间戳
最终签名通过 URL 参数传递,例如:
https://cdn.example.com/storage/media/2025/abcde/index.m3u8?sect=1754431234&secs=xxxxxxx&secy=chinalogs
二、NGINX Secure Link Module 简介
NGINX 的 ngx_http_secure_link_module 是一个轻量、无状态的 URL 保护机制。
它支持以下指令:
secure_link <string1>[,<string2>];
secure_link_md5 <expression>;
原理:
-
从 URL 参数中读取签名(如
$arg_secs)和过期时间(如$arg_sect) -
根据
secure_link_md5提供的表达式进行签名计算 -
若签名一致,且时间未过期,则允许访问
但这里隐藏了一个非常隐蔽的问题:
secure_link_md5生成的是二进制 MD5,经 Base64URL 编码后进行匹配
如果你在后台代码里直接用十六进制字符串(如 md5(...))),则一定会出错。
三、错误示例(签名无法匹配)
假设签名字符串如下:
/storage/media/2025/abcde/index.m3u8chinalogs127.0.0.11754431234
错误做法:
$sign = md5($sign_str); // 输出十六进制字符串
Nginx 无法识别 hex 字符串,它期望的是 base64url 编码的二进制 MD5 值。
结果就是:
-
签名永远不匹配
-
nginx 返回 403 Forbidden
日志里你看不到任何错误,但请求被无情拒绝。
四、正确 NGINX 配置示例
location ~ ^/storage/.*\.(ts|m3u8)$ {
# CORS 支持
add_header 'Access-Control-Allow-Origin' $http_origin;
add_header 'Access-Control-Allow-Credentials' 'true';
# 跳过检查直接预览
if ($arg_jump = "1") {
break;
}
# 安全链接验证
secure_link $arg_secs,$arg_sect;
# 完整路径(含文件名)
set $full_path $uri;
set $sidkey $arg_secy;
set $ip $remote_addr;
# 生成签名
secure_link_md5 "$full_path$sidkey$ip$arg_sect";
if ($secure_link = "") {
return 403;
}
if ($secure_link = "0") {
return 410;
}
}
五、签名计算的关键点
为了能正确匹配 nginx 内部签名,需要注意以下几点:
| 项目 | 要求 |
|---|---|
| 哈希算法 | MD5,输出为二进制 |
| 编码方式 | Base64URL(不是 hex,也不是普通 base64) |
| Base64URL 说明 | + → -,/ → _,去掉末尾的 = |
六、签名构造格式
假设请求 URL 为:
/storage/media/2025/abcde/index.m3u8?sect=1754431234&secs=xxxxxx&secy=chinalogs
签名字符串为:
/storage/media/2025/abcde/index.m3u8chinalogs127.0.0.11754431234
然后将该字符串:
-
用 MD5 算法,得到二进制输出(不是 hex)
-
再进行 base64url 编码
七、多语言签名生成示例
1. PHP 示例
<?php
$path = "/storage/media/2025/abcde/index.m3u8";
$user = "chinalogs";
$ip = "127.0.0.1";
$expire = "1754431234";
$raw = $path . $user . $ip . $expire;
$binary = md5($raw, true);
$sign = rtrim(strtr(base64_encode($binary), '+/', '-_'), '=');
echo "secs={$sign}";
// 优快云 @chinalogs (HiFeiz)
2. Python 示例
import hashlib
import base64
path = "/storage/media/2025/abcde/index.m3u8"
user = "chinalogs"
ip = "127.0.0.1"
expire = "1754431234"
raw = f"{path}{user}{ip}{expire}"
digest = hashlib.md5(raw.encode()).digest()
sign = base64.urlsafe_b64encode(digest).decode().rstrip('=')
print(f"secs={sign}")
# 优快云 @chinalogs (HiFeiz)
3. ASP 示例
<%
Dim str, md5, bytes, obj, sign
str = "/storage/media/2025/abcde/index.m3u8" & "chinalogs" & "127.0.0.1" & "1754431234"
Set obj = Server.CreateObject("System.Security.Cryptography.MD5CryptoServiceProvider")
Set enc = Server.CreateObject("System.Text.UTF8Encoding")
bytes = enc.GetBytes_4(str)
binaryHash = obj.ComputeHash_2(bytes)
Set conv = Server.CreateObject("MSXML2.DomDocument").createElement("tmp")
conv.dataType = "bin.base64"
conv.nodeTypedValue = binaryHash
sign = Replace(Replace(Replace(conv.text, "+", "-"), "/", "_"), "=", "")
Response.Write "secs=" & sign
%>
' 优快云 @chinalogs (HiFeiz)
4. JSP 示例
<%@ page import="java.security.*, java.util.Base64" %>
<%
String raw = "/storage/media/2025/abcde/index.m3u8" + "chinalogs" + "127.0.0.1" + "1754431234";
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(raw.getBytes());
String sign = Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
out.println("secs=" + sign);
%>
// 优快云 @chinalogs (HiFeiz)
5. Node.js 示例
const crypto = require('crypto');
const path = "/storage/media/2025/abcde/index.m3u8";
const user = "chinalogs";
const ip = "127.0.0.1";
const expire = "1754431234";
const raw = path + user + ip + expire;
const hash = crypto.createHash('md5').update(raw).digest();
const sign = hash.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
console.log(`secs=${sign}`);
// 优快云 @chinalogs (HiFeiz)
八、调试技巧
你可以在 nginx 中添加一些调试 Header:
add_header X-Debug-URI $uri;
add_header X-Debug-IP $remote_addr;
add_header X-Debug-Full-Path $full_path;
add_header X-Debug-SID $sidkey;
add_header X-Debug-Timestamp $arg_sect;
方便在浏览器开发者工具中观察 nginx 实际拿到的值,确保签名字符串拼接正确。
九、总结
坑点回顾:
-
❌ 错误使用 md5 十六进制字符串
-
✅ 正确使用 MD5 的二进制输出 + base64url 编码
这个问题极其隐蔽,日志没错误,调试没提示,唯一的反馈是 403 Forbidden,非常坑爹。
希望本文对你避坑有帮助,也欢迎在 优快云 上关注我 👉 @chinalogs (HiFeiz)。
💬 欢迎留言讨论。

3万+

被折叠的 条评论
为什么被折叠?



