内容提要
使用归因短链分发CloudFront签名URL时,短链追加的归因参数会导致签名校验失败、下载返回403,原因是canned policy按实际请求URL现场拼接Resource进行校验,多一个参数即失败。解法是改用custom policy,将Resource写为“精确路径?*”,仅放开query段而锁定路径,并关闭query string转发,以避免影响缓存命中率和源站安全。
延伸解读
403 的根源:签名覆盖整个 base URL
CloudFront 签名校验的对象不只是文件路径,而是协议、域名、路径和自定义 query 参数的整体。归因短链在跳转时追加参数,相当于改变了签名时的 base URL,因此 canned policy 会直接返回 403。这也解释了为什么直接打开原始签名 URL 正常,而经过短链就失败。排查时不能只看错误码,因为过期、参数不符、路径不匹配都会返回同样的 AccessDenied。
custom policy 的 ?* 是精确放行 query 段
custom policy 允许在 Resource 中使用通配符,把 Resource 写成「精确路径?*」后,路径仍被锁死,只有 query 段被放开。文章实测表明,这种写法在不带参数时也返回 200,因此同一条签名 URL 加不加归因参数都能用。关键是不能写成「路径*」,否则路径也会被通配,导致 app.apk.bak 等非目标文件也能通过签名访问,权限被意外放大。
放开 query 后必须管住缓存与回源
允许任意 query 参数通过签名后,风险转移到缓存和源站。如果 query string 进入 cache key,每个渠道的参数组合都会成为独立缓存对象,APK 这类大文件会显著拉低命中率并增加回源流量。若 query string 被转发到 S3,拿到 URL 的人还可能追加 response-content-type 等参数改变响应行为。因此应确认 query string 既不进入缓存键,也不转发给源站。
签名有效期要覆盖短链投放周期
示例代码的 TTL 是 72 小时,但归因短链往往要投放数周。签名过期后短链同样返回 403,很容易与参数问题混淆。实际部署时应按投放周期设置过期时间,或定期重新签发并更新短链目标地址。此外,若源站本身依赖 query string 返回不同内容,放开 query 段的影响需要单独评估,不能直接套用静态文件下载场景的结论。
Q&A
为什么用归因短链分发 CloudFront 签名 URL 后,下载 APK 会返回 403?
归因短链(如 AppsFlyer OneLink)在跳转时会追加归因参数,而 CloudFront 的 canned policy 签名校验时,Resource 由 CloudFront 根据实际请求 URL(路径加上除保留参数外的全部 query)现场拼接,多一个参数就会导致签名校验失败,从而返回 403。
CloudFront 的 canned policy 和 custom policy 在签名校验上有什么关键区别?
canned policy 的 Resource 由 CloudFront 根据请求 URL 现场拼接,签发方无法写通配符,URL 多一个字节都会校验失败;custom policy 的 policy 原文随 URL 传递,CloudFront 先校验 policy 和签名是否匹配,再用 policy 中的 Resource(支持通配符)匹配实际请求 URL。
如何确认 403 是归因短链追加参数导致的?
用与客户端一致的 GET 请求追踪短链跳转,拿到最终 URL,与原始签名 URL 逐字符对比,检查是否多了参数、混入空格或换行、少了参数。例如使用 curl 命令:curl -sS -L -r 0-0 -A "<Android 设备的 User-Agent>" -o /dev/null -w "%{url_effective}\n" "<短链>"。注意不要仅凭 403 AccessDenied 错误码判断,签名过期、路径不匹配等也会返回相同错误。
解决 CloudFront 签名 URL 因归因参数返回 403 的推荐方案是什么?
将签发方式从 canned policy 改为 custom policy,Resource 写成「https://<域名>/<精确路径>?*」(注意是「路径?*」,不是「路径*」),并关闭 query string 转发(确保 query string 不在 cache key 中,也不转发给源站)。这样归因短链追加任意非保留 query 参数都能通过,路径仍锁定在具体文件上。
为什么 custom policy 的 Resource 要写成「路径?*」而不是「路径*」?
因为 custom policy 的通配规则中,路径末尾的星号会隐含匹配 query string 段,即「路径*」等价于「路径*?*」。如果写成「路径*」,路径也会被通配,导致 app.apk.bak、app.apk/anything 等都能用同一签名访问,权限被意外放大。正确写法是「路径?*」,只放开 query 段而锁定路径。
改用 custom policy 并放开 query 段后,需要配套调整哪些 CloudFront 配置?
需要检查 cache policy 和 origin request policy,确保 query string 既不在 cache key 中,也不转发给源站(例如使用托管缓存策略 CachingOptimized,且不配置转发 query string 的 origin request policy;若使用 legacy cache settings,则确认 Query string forwarding and caching 设为 None)。这样做可以避免缓存命中率下降和源站安全风险(如 S3 处理 response-content-type 等参数)。