为API添加七牛云私有化防护的踩坑记录
起因:一个简单的防盗需求
我的数据库API托管了不少图片资源,之前用的是七牛云的公开空间,配合Referer防盗链。说实话,一开始觉得挺简单,配置一下白名单就行了,能防止别人盗链嘛。
但很快就遇到了问题:
本地开发的时候图片全挂了
因为本地开发用的是 localhost,Referer是 http://localhost,根本不在白名单里。每次要测试图片显示,都得先上传到服务器或者临时关闭防盗链,说实话是比较麻烦的。
Referer防盗链只是君子协议,技术上很容易被伪造,安全性也不够高。所以我就想,要不干脆上私有空间算了?
第一坑:私有空间的认知误区
一开始我天真的以为,只要把七牛云的空间设置为"私有",然后按照文档生成带token的URL就行了。结果:
{"error":"download token not specified"}
我反复检查:
- Bucket是不是私有?
- AK/SK对不对?
- URL格式对不对?
就是不行!
第二坑:自己实现签名算法的"正确"方式
我按照网上找到的教程,用PHP实现了七牛云的签名算法:
// 我的实现
$path = parse_url($url, PHP_URL_PATH);
$baseStr = $path . '?e=' . $deadline;
$sign = hash_hmac('sha1', $baseStr, $secretKey, true);
$encodedSign = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($sign));
$token = $accessKey . ':' . $encodedSign;
看起来很完美对吧?完全不对!
第三坑:Base64编码的填充字符问题
后来我用Python官方SDK生成了一个测试URL,发现了一个关键差异:
Python生成的token末尾有 = 号
我的实现把 = 全移除了
这就是问题所在!七牛云的签名需要保留Base64末尾的填充字符。
// ❌ 错误:移除了 =
$find = ['+', '/', '='];
$replace = ['-', '_', ''];
// ✅ 正确:只替换+和/,保留=
return strtr(base64_encode($data), '+/', '-_');
修复后,还是403...
第四坑:签名对象的差异(最关键的一步)
这个坑踩得最深。我以为只对URL的path部分签名就行了,结果发现官方SDK的实现完全不同:
我的实现:
// 只对path签名
$baseStr = '/static/232.jpg?e=1772817970';
$token = sign($baseStr);
// 最终URL:/static/232.jpg?e=xxx&token=xxx ❌
官方SDK的实现:
// 对整个URL签名!
$urlWithDeadline = 'https://open.xinyuu.cn/static/232.jpg?e=1772817970';
$token = sign($urlWithDeadline);
// 最终URL:...jpg?e=xxx&token=xxx ✅
区别大了去了!官方SDK是:
- 在原URL后面追加
?e=deadline或&e=deadline - 对整个URL字符串进行签名(不是只对path!)
- 使用
&token=连接(不是?token=)
这就是为什么一直403的原因!
修复后的实现完全按照官方SDK的逻辑:
public static function privateDownloadUrl($url, $expires = null) {
$deadline = time() + $expires;
// 在URL后添加 e 参数
$pos = strpos($url, '?');
if ($pos !== false) {
$url .= '&e=';
} else {
$url .= '?e=';
}
$url .= $deadline;
// 对整个URL进行签名
$signData = self::signData($url);
// 用 & 连接token
return $url . '&token=' . $signData;
}
测试通过!图片终于能正常显示了!🎉
经验总结
给想要实现同样功能的伙伴们:
-
先跑通官方SDK 不要自己写实现,先用Python或PHP官方SDK测试,确保密钥、空间配置都没问题。
-
仔细对比实现差异 特别是:
- 签名的对象是什么(path还是完整URL?)
- Base64编码是否保留填充字符
- URL连接符是
?还是&
-
官方文档要看源码 有时候文档描述不够清楚,直接看官方SDK的源码最可靠。
-
分步调试很重要 我创建了很多调试工具,逐步验证每个环节,最后才定位到问题所在。
关于Token有效期:
我设置的是3600秒(1小时),这个可以根据实际需求调整:
- 安全要求高:设置短一点(如1800秒)
- 用户体验优先:设置长一点(如7200秒)
每次API请求都会生成新的token,所以过期后会自动刷新。
参考资料:
