aws-sdk-go 实战:使用 CloudFront CookieSigner 生成签名 Cookie 保护私有内容

原创2026-10-13 19:55:361,949 阅读
文章标签:后端云原生开发工具

aws-sdk-go 实战:使用 CloudFront CookieSigner 生成签名 Cookie 保护私有内容

导读

本文以 example/service/cloudfront/signCookies 示例为核心,讲解如何使用 aws-sdk-go 中 service/cloudfront/sign 包的 CookieSigner 为 CloudFront 私有内容生成签名 Cookie(Signed Cookies),并通过 http.Client 携带这些 Cookie 请求受限资源。读完本文,你将掌握 CloudFront 签名 Cookie 的完整流程:私钥加载、CookieSigner 构造、策略(Policy)生成与签名原理、以及客户端与服务端两种使用方式,并能将示例改造成自己的生产代码。

一、示例概览:README 说了什么

关联文档 example/service/cloudfront/signCookies/README.md 篇幅极短,核心内容有两块:

  • 示例用途:演示如何使用 CloudFront 的 CookieSigner 生成签名 Cookie,为 CloudFront 背后的受限资源提供短时访问权限;
  • 运行方式:发起一个使用 CloudFront Cookie 签名的对象请求,并把对象内容输出到 stdout:
go run -tags example signCookies.go -file <privkey file>  -id <keyId> -r <resource pattern> -g <object to get>

示例程序本体位于 example/service/cloudfront/signCookies/signCookies.go。注意文件开头的构建标签 //go:build example,因此必须带上 -tags example 才能编译运行(Makefile 中的测试/示例 tag 定义见 Makefile)。下面我们逐段深入。

二、四个命令行参数解析

示例通过标准库 flag 包定义四个参数(signCookies.go):

参数 变量 含义 示例值
-file keyFile 私钥 PEM 文件路径 /path/to/private-key.pem
-id keyID CloudFront 密钥对 ID(Key Pair ID) APKA... 形式的 ID
-r resource CloudFront 资源匹配模式 http*://d111111abcdef8.cloudfront.net/images/*
-g object 需要 GET 的对象地址(S3 对象经 CloudFront 加速后的 URL) https://d111111abcdef8.cloudfront.net/images/horizon.jpg

其中 -r 的资源模式决定 Cookie 可访问的范围,支持通配符(如 *、http*),与 policy.go 中 CreateResource 对 scheme 的处理逻辑对应:http、https、http*、* 直接透传,rtmp 则剥离协议前缀转换为 RTMP 资源路径。

三、核心流程逐步拆解

3.1 加载 PEM 私钥

privKey, err := sign.LoadPEMPrivKeyFile(keyFile)
if err != nil {
    fmt.Println("failed to load key,", err)
    return
}

LoadPEMPrivKeyFile 位于 privkey.go,其内部先 os.Open 读取文件,再委托给 LoadPEMPrivKey 完成 pem.Decode 与 x509.ParsePKCS1PrivateKey 解析。这意味着私钥文件必须是 PKCS#1 格式的 PEM 块(RSA PRIVATE KEY)。同文件还提供:

  • LoadEncryptedPEMPrivKey(reader, password):解密带密码的 PEM 私钥(privkey.go),可通过 x509.EncryptPEMBlock 配合密码生成(参考 privkey_test.go);
  • 若输入不含合法 PEM 数据,会返回 no valid PEM data provided 错误,对应测试见 privkey_test.go。

3.2 构造 CookieSigner

signer := sign.NewCookieSigner(keyID, privKey)

NewCookieSigner 的签名是 NewCookieSigner(keyID string, privKey *rsa.PrivateKey, opts ...func(*CookieOptions))(sign_cookie.go)。设计上,每个密钥对 ID + 私钥组合只需创建一次,且 CookieSigner 可安全并发使用;CookieOptions 本身不推荐并发修改(源码注释明确说明,见 sign_cookie.go)。

CookieOptions(sign_cookie.go)包含三个字段,会原样写入生成的 http.Cookie:

  • Path:Cookie 生效路径,如 / 或 /mypath/;
  • Domain:Cookie 生效域名,如 .cNameAssociatedWithMyDistribution.com;
  • Secure:是否仅限 HTTPS 传输。

3.3 生成签名 Cookie

cookies, err := signer.Sign(resource, time.Now().Add(1*time.Hour))
if err != nil {
    fmt.Println("failed to sign cookies", err)
    return
}

Sign(u string, expires time.Time, opts ...func(*CookieOptions))(sign_cookie.go)执行三步:

  1. 校验 URL scheme:只允许 http、https、http*,否则返回 invalid cookie URL, missing scheme(sign_cookie.go);
  2. 用 CreateResource 构造资源字符串;
  3. 通过 NewCannedPolicy(resource, expires)(policy.go)生成仅含 DateLessThan(过期时间)的 Canned Policy,再签名出三枚 Cookie。

3.4 携带 Cookie 请求对象

req, err := http.NewRequest("GET", object, nil)
for _, c := range cookies {
    fmt.Printf("%s=%s;\n", c.Name, c.Value)
    req.AddCookie(c)
}
resp, err := http.DefaultClient.Do(req)

示例把三枚 Cookie 逐一打印并 AddCookie 到请求中,随后 http.DefaultClient.Do(req) 发起 GET,读取响应体并输出到 stdout(signCookies.go)。成功时对象内容直接打印;示例注释还给出了服务端场景:如果自己实现 Web 服务,应改用 http.SetCookie(w, c) 写入 ResponseWriter,把签名 Cookie 下发给浏览器,而不是在客户端手动拼接。

四、签名 Cookie 的组成:三枚固定命名的 Cookie

createCookies(sign_cookie.go)始终生成三枚 Cookie,名称由包级常量定义(sign_cookie.go):

Cookie 名称 内容 说明
CloudFront-Policy Base64 编码的策略 JSON 记录资源模式与生效条件
CloudFront-Signature 策略的 RSA 签名 由私钥对策略签名产生
CloudFront-Key-Pair-Id 密钥对 ID 与私钥对应的公钥 ID,供 CloudFront 验签

三枚 Cookie 均设置 HttpOnly: true,并统一套用 CookieOptions 中的 Path / Domain / Secure。单元测试 sign_cookie_test.go 验证了这三个名字与返回顺序,sign_cookie_test.go 则验证了 Path、Domain、Secure 三个选项确实生效。

五、底层原理:Policy 如何被签名

5.1 策略结构

policy.go 定义了策略 JSON 模型:

  • Condition:包含三个可空字段——IPAddress(可选,限定来源 IP 段,如 192.0.2.0/24)、DateGreaterThan(可选,生效起始时间)、DateLessThan(必填,过期时间);
  • Statement:资源 + 条件;
  • Policy:语句列表(签名 Cookie 只应包含单条 statement,见 sign_cookie_example_test.go 注释)。

时间字段由 AWSEpochTime 包装,JSON 序列化为 {"AWS:EpochTime":<unix秒>}(policy.go),测试用例见 policy_test.go。

5.2 签名与转义流程

Policy.Sign(policy.go)的核心步骤:

  1. Validate 校验:至少一条 statement、资源非空、且资源只能包含 ASCII 字符(Unicode 主机名需转 Punycode、路径需 URL 转义,见 policy.go);
  2. 策略 JSON 编码为不转义 HTML 的紧凑形式(Go 1.7+ 走 encoder.SetEscapeHTML(false),见 policy_json_1_7.go;Go 1.6 及以下在 policy_json_1_6.go 中把 \u0026 还原为 &);
  3. 对 JSON 做标准 Base64 编码得到 CloudFront-Policy 的 Value;
  4. 对 JSON 计算 SHA-1 摘要,再用 RSA 私钥执行 rsa.SignPKCS1v15(..., crypto.SHA1, ...) 签名(policy.go),Base64 编码后作为 CloudFront-Signature;
  5. 将 Base64 中的 +、=、/ 分别替换为 -、_、~,得到 URL 安全的编码(policy.go),测试见 policy_test.go。

TestSignEncodedPolicy(policy_test.go)还演示了如何用 rsa.VerifyPKCS1v15 反向验签,验证签名过程符合 PKCS#1 v1.5 + SHA-1 的约定。

5.3 Canned Policy 与自定义策略

Sign 只能表达"资源 + 过期时间"这种最简单的 Canned Policy。需要更多条件(来源 IP、生效时间)时,应使用 SignWithPolicy(sign_cookie.go),并自行构造 Policy,示例见 sign_cookie_example_test.go:

p := &Policy{
    Statements: []Statement{{
        Resource: "http://sub.cloudfront.com",
        Condition: Condition{
            IPAddress:       &IPAddress{SourceIP: "192.0.2.0/24"},
            DateGreaterThan: &AWSEpochTime{time.Now().Add(30 * time.Minute)},
            DateLessThan:    &AWSEpochTime{time.Now().Add(1 * time.Hour)},
        },
    }},
}
cookies, err := cookieSigner.SignWithPolicy(p)

该测试还提醒:不要给签名 Cookie 额外设置 Expire 或 MaxAge 字段(见 sign_cookie_example_test.go 注释),过期控制完全交给策略中的 DateLessThan。

5.4 附带说明:URLSigner 与 CookieSigner 的关系

service/cloudfront/sign 包同时提供 URLSigner(sign_url.go),用于生成签名 URL(Expires、Signature、Key-Pair-Id 三个查询参数),两者共享同一套 Policy 与签名实现。区别在于:签名 URL 把凭证放进 URL 查询串,而签名 Cookie 把凭证放进 Set-Cookie 响应头,更利于配合 CloudFront 默认缓存行为和浏览器自动携带。Cookie 方案尤其适合不希望在 URL 中暴露签名信息、或需要覆盖整个站点资源目录的场景。

六、在仓库中验证:测试与可运行示例

仓库为 sign 包提供了完整测试,可直接在本地运行验证:

# 在仓库根目录执行
go test -tags example ./service/cloudfront/sign/
  • sign_cookie_test.go:覆盖 NewCookieSigner、Sign、SignWithPolicy、Cookie 选项;
  • policy_test.go:覆盖策略编码、资源构造、签名验签、AWS 转义;
  • privkey_test.go:覆盖明文/加密 PEM 私钥加载与错误路径;
  • sign_cookie_example_test.go:Go Example 测试,Output 注释中给出了真实的 CloudFront-Policy、CloudFront-Signature、CloudFront-Key-Pair-Id 输出样例,可作为对照检查自己的实现。

运行示例本身需要真实 CloudFront 密钥对与已配置的私有内容,命令为:

cd example/service/cloudfront/signCookies
go run -tags example signCookies.go \
  -file /path/to/private-key.pem \
  -id APKAXXXXXXXXXXXXXXXXX \
  -r 'http*://d111111abcdef8.cloudfront.net/images/*' \
  -g 'https://d111111abcdef8.cloudfront.net/images/horizon.jpg'

七、使用前提与注意事项

  1. 密钥对来源:keyID 是 AWS 账户中 CloudFront 密钥对的 ID(非 Access Key),私钥为对应的 PEM 编码 RSA 私钥,需先创建 CloudFront Key Pair;
  2. 资源模式:-r 的模式应与分发行为匹配,示例中 http*:// 同时覆盖 http 与 https;资源必须为纯 ASCII(见 policy.go);
  3. 构建标签:示例带 //go:build example,务必使用 -tags example 运行或编译;
  4. 时间同步:Cookie 的生效与过期基于 Unix 时间戳(AWS:EpochTime),客户端时钟偏差过大可能导致请求被拒;
  5. SDK 维护状态:本仓库 aws-sdk-go(v1)已进入停止维护阶段(go.mod 顶部注释及 sign_url.go 均声明 Deprecated),新项目应迁移到 AWS SDK for Go v2;本文涉及的签名算法与策略格式本身是 CloudFront 的稳定协议,理解这些原理对使用 v2 或其他语言的 SDK 同样有直接参考价值。

结语

从 README 的三行说明出发,本文还原了签名 Cookie 示例的完整面貌:四个命令行参数的语义、私钥加载到 Cookie 下发的每一步、三枚 Cookie 的名称与含义、Canned/自定义策略的构造与 SHA-1 + RSA 签名细节,以及客户端与服务端两种接入方式。借助 signCookies.go 与 sign 包 的源码和测试,你可以快速把示例改造为可投入生产的短时授权方案。

登录后查看全文
aws-sdk-go