- 后端
- 数据可视化
- 运维
【免费下载链接】Tautulli
A Python based monitoring and tracking tool for Plex Media Server.
导读
本文讲解 Cloudinary 的"签名浏览器直传"模式:由你的服务器为上传参数签发签名,浏览器或移动端客户端携带该签名把文件直接 POST 到api.cloudinary.com,文件不经由你的应用服务器,而api_secret也始终不出服务器。Tautulli 仓库在 lib/cloudinary 目录下完整打包了 Cloudinary Python SDK(含本主题对应的官方文档与全部实现源码),文章将结合该 SDK 的 utils.py、uploader.py、api.py 源码,从签名算法、端点构造、上传预置(preset)到响应校验与排错,给你一套可直接落地的直传方案。
适用场景:什么时候需要"签名直传"
常规的服务端上传(cloudinary.uploader.upload(...))是文件先到你的应用、再由应用转发给 Cloudinary;而浏览器直传解决的是另一类需求:
- 文件不经过你的应用服务器,直接由浏览器/移动端上传到 Cloudinary,节省服务器带宽与中转时间;
- 但上传授权由你的服务器控制——
api_secret只存在于服务器端,永不随请求下发到客户端。
实现方式即本文主题:服务器为一批上传参数(如timestamp、folder)计算签名,客户端将"文件 + 这批参数 + 签名"一起 POST 给 Cloudinary。若你不需要此模式,服务端上传请参阅 Upload an image;在 Django 中还可以使用CloudinaryJsFileField直接渲染一个已签名的直传组件,见 Use with Django。
完整流程:从签名端点到客户端上传
第一步:服务器签发签名参数
签名由服务器端完成。核心思路是:把所有"将要随上传发送给 Cloudinary 的参数"收集起来,连同当前 Unix 时间戳一起签名。下面的示例代码与官方文档一致,直接可以运行:
import time import cloudinary import cloudinary.utils # 导入 cloudinary 之后即自动可用 # 配置自动从 CLOUDINARY_URL 环境变量读取 def signature_payload(): config = cloudinary.config() # 这里签名的每一个参数,客户端都必须原样(逐字节)提交 params_to_sign = { "timestamp": int(time.time()), "folder": "user-uploads", } signature = cloudinary.utils.api_sign_request(params_to_sign, config.api_secret) return { "signature": signature, # 40 位十六进制字符串(SHA-1) "api_key": config.api_key, # 公开信息,可以安全下发 "cloud_name": config.cloud_name, **params_to_sign, } if __name__ == "__main__": print(signature_payload())几点关键说明:
api_key与cloud_name是公开信息,随载荷一起返回给客户端无安全风险;只有api_secret不能离开服务器。- 签名结果默认是 SHA-1 的 40 位十六进制串;若配置了
signature_algorithm="sha256",则为 64 位十六进制串。 cloudinary.config()返回的是进程级全局配置对象。SDK 在首次使用时自动从CLOUDINARY_URL(形如cloudinary://<api_key>:<api_secret>@<cloud_name>)解析出cloud_name、api_key、api_secret,解析逻辑见 lib/cloudinary/init.py;也可调用cloudinary.config(cloud_name=..., api_key=..., api_secret=...)显式配置。
第二步:客户端 POST 到上传端点
客户端拿到上述载荷后,向如下端点发起 POST:
https://api.cloudinary.com/v1_1/<cloud_name>/image/upload请求包含:签名过的全部参数(timestamp、folder)+file+api_key+signature。
不要手写这个 URL,而应调用 SDK 动态构建,避免 cloud name 变化或版本前缀调整时失效:
endpoint = cloudinary.utils.cloudinary_api_url("upload", resource_type="image") # 结果形如 https://api.cloudinary.com/v1_1/<cloud>/image/upload源码佐证:cloudinary_api_url的实现位于 lib/cloudinary/utils.py,它最终调用base_api_url(utils.py),由upload_prefix(默认https://api.cloudinary.com)、API_VERSION(即v1_1,定义于 lib/cloudinary/init.py)和cloud_name拼装而成;缺cloud_name时直接抛出ValueError("Must supply cloud_name")。
签名算法底层原理(源码级解析)
理解api_sign_request的内部实现,对排查签名失败至关重要。其定义在 lib/cloudinary/utils.py,签名流程如下:
- 组装待签字符串:调用
api_string_to_sign(utils.py),对参数字典做规范化:- 列表值用
,连接(如tags=[a,b]→tags=a,b); - 布尔值转为小写字符串(
True→true); - 空值(
None或"")直接跳过、不参与签名; - 每个键值对格式化为
key=value; - 按参数名排序后用
&连接成key1=value1&key2=value2形式的字符串。
- 列表值用
- 拼接
api_secret并哈希:compute_hex_hash(to_sign + api_secret, algorithm)(utils.py)对"待签字符串 + api_secret"计算哈希,默认算法是sha1(常量定义于 utils.py)。 - 签名版本差异:
signature_version默认取配置值2(见 lib/cloudinary/init.py)。版本 2 会对每个key=value中的&做百分号编码(替换为%26,见_encode_param,utils.py),用于防止参数走私(parameter smuggling);版本 1 不做编码,仅用于向后兼容(如响应签名校验,见后文)。
这也解释了官方文档中的一条排错线索:当 Cloudinary 返回Invalid Signature <hash>. String to sign - '<params>'时,错误信息里打印的正是服务端实际参与签名的规范化字符串——把它与客户端实际提交的参数逐一比对,几乎总能发现是多签或漏签了一个参数,而不是密钥配错。
最容易导致失败的一条规则
签名参数与提交参数必须完全一致。如果客户端额外加了一个服务器没签过的tags,或者服务器签了folder而客户端没发,Cloudinary 都会返回Invalid Signature。原则是:
- 客户端将要发送的每一个参数,都必须被服务器签名;
- 客户端不要发送任何未被签名的额外参数。
另外,签名与时间戳绑定,有效期为一小时。因此正确的做法是"每次上传签发一次签名",不要缓存签名复用在多个上传上。
源码印证:服务端 SDK 自身在上传时会通过
sign_request(utils.py)把参数清理(cleanup_params,过滤空值)后统一签名,并顺带写入signature与api_key;其中non_signable指定的键会照常发送但被排除在签名之外。可见"签什么就发什么"是整个 SDK 与 Cloudinary 服务端的共同约定。
用上传预置(Upload Preset)限制客户端行为
签名只代表"这个上传是经过授权的",它本身不限制客户端能传什么。要约束格式、大小或目标目录,应当签发一个签名预置(signed preset),而不是签发裸参数:
params_to_sign = {"timestamp": int(time.time()), "upload_preset": "user-avatars"}预置只需在管理端创建一次,把目标文件夹、allowed_formats和所需的转换(transformation)固化进去:
import cloudinary.api cloudinary.api.create_upload_preset( name="user-avatars", unsigned=False, folder="avatars", allowed_formats="jpg,png,webp", transformation=[{"width": 512, "height": 512, "crop": "fill", "gravity": "face"}], )该例中的预置会把用户头像统一裁剪为 512×512、以人脸为重心填充(gravity: "face"),且只接受 jpg/png/webp,杜绝了客户端自定义上传目标或格式的余地。
源码佐证:create_upload_preset是 Admin API 的封装,实现在 lib/cloudinary/api.py,通过POST upload_presets创建,支持unsigned、disallow_public_id、name、live等选项(unsigned=False即为签名预置)。
无签名上传(Unsigned Upload):彻底放弃控制
如果使用unsigned=True的预置,客户端上传时连签名都不需要——服务器往返彻底消失,但同时你也失去了对"谁能上传"的控制。因此无签名模式只应服务于真正公开的上传场景,并配合一个刻意收紧的预置:固定 folder、allowed_formats、以及大小上限。
服务端 SDK 对应的调用是cloudinary.uploader.unsigned_upload:
cloudinary.uploader.unsigned_upload("photo.jpg", "public-drop-box")源码佐证:unsigned_upload定义于 lib/cloudinary/uploader.py,其本质是调用upload(file, upload_preset=upload_preset, unsigned=True, **options)——在call_api(uploader.py)中,unsigned=True会跳过utils.sign_request签名步骤(见 uploader.py),仅依赖预置本身的约束。
校验返回结果:信任来自 Cloudinary 的响应
浏览器直传之后,你的服务器可能收到来自 Cloudinary 的响应和webhook 通知。由于它们可能经过不可信网络,SDK 提供了对应的签名校验工具,确保其确实来自 Cloudinary:
cloudinary.utils.verify_api_response_signature( result["public_id"], result["version"], result["signature"] )对于 webhook 负载则使用:
cloudinary.utils.verify_notification_signature(body, timestamp, signature)源码佐证:
verify_api_response_signature(utils.py)对public_id与version两个字段按签名版本 1重新计算签名并比对(版本 1 用于向后兼容);CloudinaryResource.validate()方法(lib/cloudinary/init.py)正是调用它来做对象级校验。verify_notification_signature(utils.py)计算body + timestamp + api_secret的哈希并与签名比对,且默认valid_for=7200秒(2 小时)——超过该窗口的timestamp直接返回False,防止重放攻击。注意:此处的 2 小时是针对 webhook 通知的时效;上传签名的一小时时效由 Cloudinary 服务端执行。
疑难排查清单
Invalid Signature <hash>. String to sign - '<params>'—— 签名参数与提交参数不一致。错误信息打印了服务端实际签名的字符串,与客户端发送的内容逐项比对即可定位。这几乎总是"多一个参数或漏一个参数"导致,而非密钥错误。Must supply api_secret—— 签名流程缺少凭据配置。只配置了cloud_name通常足够生成 URL,但上传签名与 Admin API 都需要api_secret;请重新从控制台复制完整的三元组(cloud_name、api_key、api_secret)。Stale request——timestamp超过一小时,或服务器时钟不准。检查服务器时间同步,并在每次上传时重新签发。Upload preset not found—— 该预置名在当前的 cloud 中不存在;或预置是unsigned=True,而你在对请求做签名(反之亦然)。- 客户端报 CORS 错误—— 上传地址发错了主机。浏览器直传必须发往
api.cloudinary.com,而不是用于交付图片的res.cloudinary.com。
若问题源于基础配置(例如Must supply cloud_name / api_key或Invalid CLOUDINARY_URL scheme),可进一步参阅配置文档 configure-cloudinary.md 中的排查小节——该文档还明确指出:一旦设置了任一CLOUDINARY_*独立环境变量,SDK 会优先读取它们并完全忽略CLOUDINARY_URL,两种配置风格不要混用。
相关文档
- Upload an image —— 服务端直传方式。
- Use with Django ——
CloudinaryJsFileField为你渲染一个已签名的浏览器直传组件。 - configure-cloudinary.md ——
CLOUDINARY_URL等基础配置与常见配置错误排查。 - Cloudinary 官方还提供完整的浏览器内上传 UI(Upload Widget),其消费的正是本文所述的签名协议;签名生成的完整参数规则可参考 Cloudinary 官方签名生成文档(
signatures.md)。
- 后端
- 数据可视化
- 运维
【免费下载链接】Tautulli
A Python based monitoring and tracking tool for Plex Media Server.
相关推荐
Supabase Storage 签名可续传上传(Signed Resumable Upload):基于 Uppy + TUS 与 createSignedUploadUrl 的完整实战
Supabase Storage 签名可续传上传(Signed Resumable Upload):基于 Uppy + TUS 与 createSignedUp
后端前端数据库Teleport `tsh` 浏览器 MFA(Browser MFA)完整指南:原理、流程、配置与源码实现
Teleport tsh 浏览器 MFA(Browser MFA)完整指南:原理、流程、配置与源码实现 浏览器 MFA(Browser MFA)是 Telepo
网络安全认证鉴权运维后端Crawl4AI 内置浏览器(Builtin Browser)实战指南:browser_mode="builtin" 与 crwl browser 全解析
Crawl4AI 内置浏览器(Builtin Browser)实战指南:browser_mode="builtin" 与 crwl browser 全解析 本文
网页爬虫RAG人工智能AI 应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考