secure link模块仅校验不生成签名,后端须用md5($uri+$arg_e+密钥)生成base64url编码签名并注入url,nginx通过secure_link与secure_link_md5配合验证,$secure_link为"1"才放行。

Secure-Link 模块不能直接生成带时效的下载链接——它只做校验,签名和过期逻辑必须由后端(如 PHP/Python)生成并注入到 URL 中,Nginx 仅负责验证。
secure_link 指令只校验,不生成签名
很多人误以为 secure_link 能自动签发带时间戳的链接,其实它只是从请求中提取 $arg_st(签名)和 $arg_e(过期时间),然后用密钥重新计算比对。Nginx 不生成、不管理密钥、不构造 URL——这些全得你手写后端代码完成。
常见错误现象:403 Forbidden 且 error log 出现 secure link does not match 或 expired,大概率是后端算错签名,或时间戳单位不一致(比如用了秒但 Nginx 期望毫秒,或时区偏差)。
- 签名算法固定为 MD5(
$uri+$arg_e+密钥),base64url 编码(去掉+//,替换=为空) -
$arg_e必须是 Unix 时间戳(秒级整数),不是相对时间(如 “+300”),也不是毫秒 - 密钥在 Nginx 配置里用
secure_link_secret指令声明,前后端必须完全一致(包括空格、换行)
location 块里必须用 secure_link 和 secure_link_md5 配合校验
单独写 secure_link $arg_st; 不够,Nginx 还需要知道怎么还原原始签名。实际要配两行:
location /dl/ {
secure_link $arg_st,$arg_e;
secure_link_md5 "$uri$arg_e$secure_link_secret";
if ($secure_link = "") { return 403; }
if ($secure_link = "0") { return 410; }
# 后续 proxy_pass 或 alias 到文件
}
注意:$secure_link_secret 是变量名,不是字面量;它由 secure_link_secret "your-key"; 全局或 location 级定义。
-
$secure_link变量值为""表示签名字段缺失或格式错误(如 base64 解码失败) - 值为
"0"表示签名正确但已过期($arg_e ) - 值为
"1"才代表校验通过,可放行
后端生成 URL 时容易漏掉的三个细节
PHP 示例(其他语言同理,但别直接复制 base64 编码逻辑):
$uri = '/dl/report.pdf';
$expires = time() + 300; // 5 分钟后过期
$secret = 'my-secret-key';
$hash = md5($uri . $expires . $secret, true);
$st = rtrim(strtr(base64_encode($hash), '+/', '-_'), '=');
$url = "https://example.com{$uri}?st={$st}&e={$expires}";
这里踩坑最多的是:
- 没用
raw_output = true(PHP)或digest=True(Python hashlib)导致 MD5 输出十六进制字符串而非二进制字节,签名必然错 - base64 编码后没做 URL 安全转换(
+/→-_,去=),浏览器会把+当空格解码 -
$uri必须和 Nginx 收到的原始 URI 完全一致:包括开头的/、大小写、编码(如空格是%20还是+),建议后端用rawurlencode()处理路径部分
调试时优先检查 Nginx error log 和 $secure_link 变量值
别只看返回码。在 location 块里临时加一行:
add_header X-Secure-Link-Status $secure_link; add_header X-Arg-St $arg_st; add_header X-Arg-E $arg_e;
再 curl 请求,看响应头输出什么。如果 X-Secure-Link-Status 是空,说明 $arg_st 根本没传进来(可能被 rewrite 清掉了,或前端 JS 拼 URL 时漏了参数);如果是 0 但 X-Arg-E 显示的时间明显未过期,大概率是 Nginx 服务器时间不准,或后端用的是本地时区时间戳。
模块本身不支持自定义过期提示页,想返回 JSON 错误?只能靠 error_page 410 /410.json; 配静态文件,或者用 return 410 '{"error":"expired"}';(需 Nginx ≥ 1.13.10)。
最常被忽略的一点:Secure-Link 模块默认不启用,编译时得带 --with-http_secure_link_module,OpenResty 默认带,但很多 Alpine 小镜像或自编译包会漏掉——先 nginx -V 2>&1 | grep secure_link 确认模块存在再说。











