直链别裸奔:用 Python 给下载地址加时效签名 token
前面讲防盗链靠 Referer,但 Referer 能被伪造,真要防泄露得靠签名 URL——给直链加一个带过期时间的 token,服务端校验通过才放行。这篇用一个最小可用的 Python 实现讲清楚,以及几个别踩的坑。
思路
- 服务端有个只有自己知道的
SECRET; - 生成下载链接时,把
过期时间 exp和路径 path拼上,用 HMAC 算个签名sig; - 把
exp和sig附到 URL 参数; - CDN/后端收到请求,用同样的
SECRET重算签名比对,且检查exp没过期,才返回文件,否则 403。
生成签名(Python)
import hmac, hashlib, time, base64, urllib.parse
SECRET = b'your-secret-key'
def sign(path: str, ttl=600):
exp = int(time.time()) + ttl
raw = f"{path}:{exp}".encode()
sig = hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
return exp, sig
exp, sig = sign("/videos/a.mp4")
url = f"https://cdn.example.com/videos/a.mp4?exp={exp}&sig={sig}"
print(url)
ttl=600 表示 10 分钟有效。
校验(服务端/中间件)
def verify(path: str, exp: str, sig: str):
if int(exp) < time.time():
return False # 过期
raw = f"{path}:{exp}".encode()
expect = hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
return hmac.compare_digest(expect, sig)
一定用 hmac.compare_digest 比较,别用 ==,后者有时序攻击风险。
坑一:path 必须规范化和双方一致
你签名用 /videos/a.mp4,CDN 转发后可能变成 //videos//a.mp4 或带 query 的,签名就对不上。校验端先把 path 用 urllib.parse 规范化(unquote、去多余斜杠)再算。
坑二:CDN 会改写 URL
Nginx/CDN 可能把 ?exp=&sig= 透传也可能吞掉。配置上要确认参数被原样传给后端校验逻辑。我踩过 CDN 把 sig 里的 +/= 转义的坑,校验端 unquote 一下。
坑三:SECRET 泄露等于没签
签名的安全性全在 SECRET。别硬编码在前端、别进 git、用环境变量或 KMS。前端只能拿到"已签好的 URL",绝不能拿到 SECRET。
和 Referer 的关系
签名解决"链接本身能不能用",Referer 解决"从哪来的"。两者叠加:签名防泄露、Referer 防盗链。前面那篇防盗链是客户端视角,这篇是服务端视角,配合着用。