坑爹的 NGINX Secure Link Module:入坑到出坑全流程详细解析及认证无效解决

优快云 @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

然后将该字符串:

  1. 用 MD5 算法,得到二进制输出(不是 hex)

  2. 再进行 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)


💬 欢迎留言讨论。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值