跳到主要内容

为API添加七牛云私有化防护的踩坑记录

· 阅读需 4 分钟

起因:一个简单的防盗需求

我的数据库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是:

  1. 在原URL后面追加 ?e=deadline&e=deadline
  2. 对整个URL字符串进行签名(不是只对path!)
  3. 使用 &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;
}

测试通过!图片终于能正常显示了!🎉

经验总结

给想要实现同样功能的伙伴们:

  1. 先跑通官方SDK 不要自己写实现,先用Python或PHP官方SDK测试,确保密钥、空间配置都没问题。

  2. 仔细对比实现差异 特别是:

    • 签名的对象是什么(path还是完整URL?)
    • Base64编码是否保留填充字符
    • URL连接符是 ? 还是 &
  3. 官方文档要看源码 有时候文档描述不够清楚,直接看官方SDK的源码最可靠。

  4. 分步调试很重要 我创建了很多调试工具,逐步验证每个环节,最后才定位到问题所在。

关于Token有效期:

我设置的是3600秒(1小时),这个可以根据实际需求调整:

  • 安全要求高:设置短一点(如1800秒)
  • 用户体验优先:设置长一点(如7200秒)

每次API请求都会生成新的token,所以过期后会自动刷新。


参考资料: