第3章 桶策略的JSON语法结构

桶策略说白了就是一个JSON文件。你把它挂到存储桶上,云服务就按这个规则来判定谁可以做什么。我刚开始接触的时候觉得这东西不就是一段JSON嘛,写几条规则就行了……结果第一次上线就出问题了,一条错误的条件直接让整个团队的构建流程炸了。后来我花了整整一下午查文档,才把语法搞明白。

嗯,这一章我就带你彻底捋一遍桶策略的JSON骨架。你看懂了这块,后面写策略的时候心里就有底了。

3.1 桶策略的整体框架

每个桶策略JSON只有三个顶层字段:

  • Id(可选)——策略的唯一标识,建议写上,方便审计时追溯
  • Version(必选)——策略语法版本,当前固定用 "2012-10-17"
  • Statement(必选)——规则数组,至少一条,每条就是一个独立的权限声明

核心要点: Statement 是一个数组。哪怕你只写一条规则,也要用方括号包起来。我见过有人图省事直接写对象,结果策略被当成了非法JSON,整个桶变公开了——差点出大事。

3.2 Statement 的六要素

每条 Statement 里最多可以出现6个字段,我按常用顺序给你排:

字段必填说明
Sid可选规则的别名,用来做日志关联。我习惯每条都写上,比如“AllowGetObjectFromDevIP”
Effect必选就两个值:Allow 或 Deny。注意 Deny 优先级高于一切 Allow
Principal必选谁可以被影响。可以是 AWS 账号、IAM 用户、匿名用户("*")或者联合身份
Action必选允许或拒绝的操作列表,比如 s3:GetObject
Resource必选操作针对的具体资源,通常是桶 ARN 或对象 ARN
Condition可选附加条件,比如来源IP、时间范围、是否使用HTTPS等

为什么我把 Condition 放最后?因为这是最容易出坑的地方。我个人建议,除非你真的明白条件的工作原理,否则不要轻易加——我曾因为写错条件键名,导致所有人都不能下载文件,排查了半天才发现多打了个冒号。

3.3 完整的结构示例

下面是一段真实项目中我常用的模板。你注意看缩进和引号:

{
  "Id": "BucketPolicy-2024",
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowGetForDevTeam",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:root"
      },
      "Action": [
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::my-bucket",
        "arn:aws:s3:::my-bucket/*"
      ],
      "Condition": {
        "IpAddress": {
          "aws:SourceIp": "10.0.0.0/8"
        }
      }
    }
  ]
}

💡 小技巧: 如果你写多条规则,注意 Effect: Deny 要放在 Effect: Allow 之前。因为云服务逻辑是先扫描所有 Deny,只要有一条命中就直接拒绝,不会再看后面的 Allow。反过来如果你把 Allow 写前面,Deny 写后面,效果一样(Deny 权重最高),但可读性差。我团队内部约定:Deny 统一放第一个 Statement。

3.4 Principal 的三种常见写法

  • 匿名用户:"Principal": "*" 或 "Principal": { "AWS": "*" }。注意:"*" 不带 AWS 键时,表示所有身份(包括未认证用户)。我曾经把它用在公开读取的桶上,结果被爬虫扫了一遍,流量费暴增。后来我加了 Referer 条件才挡住。
  • 指定账号:"Principal": { "AWS": "arn:aws:iam::123456789012:root" }。可以给多个账号,写成数组。
  • 服务主体:比如给 CloudFront 写 "Principal": { "Service": "cloudfront.amazonaws.com" }。这个在 CDN 回源场景很常见。

3.5 Action 和 Resource 的配对关系

Action 必须和 Resource 对应。比如 s3:GetObject 只能作用在对象级别(arn:aws:s3:::bucket/*),不能作用在桶级别。反过来 s3:ListBucket 只能作用在桶级别(arn:aws:s3:::bucket)。你如果写错了 Resource 范围,策略会直接保存失败。

⚠️ 常见坑: 很多人写 Resource 时只写一条 "arn:aws:s3:::bucket/*",结果 ListBucket 操作被隐式拒绝了。因为 ListBucket 需要的 resource 是 "arn:aws:s3:::bucket"(不带 /*)。我建议你在 Resource 里同时写两条:一条带 /*,一条不带。这样不管是读对象还是列举桶,都能覆盖到。

3.6 Condition 的核心逻辑

Condition 本质是布尔表达式:只有条件为真时,这条规则才生效。常用的条件键包括:

  • IpAddress / NotIpAddress:限制来源 IP
  • StringEquals / StringNotEquals:比如限制请求必须来自某个 Referer
  • Bool:比如 aws:SecureTransport 是否为 true(强制 HTTPS)
  • NumericLessThanEquals:限对象大小

记得这点: Condition 可以写在 Allow 语句里,也可以写在 Deny 语句里。如果你想让某个用户只能在特定 IP 段内操作,就在 Allow 里加 IpAddress 条件。如果你想让所有人强制使用 HTTPS,就在 Deny 里写 aws:SecureTransport 等于 false 时拒绝。我更喜欢后一种方式——它像一道防火墙:先拒绝所有不安全请求,再给正常用户开绿灯。

3.7 为什么说 JSON 结构是策略的“骨架”?

你看看下面这张图就明白了。整个桶策略从根节点往下分叉,每一条 Statement 都是独立的分支。理解这个树状结构,你才能写出没有逻辑漏洞的策略。

桶策略 JSON 结构树 桶策略 JSON Id (可选) Version (必选) Statement (必选) [ 数组... ] Statement[0] Sid Effect + Principal Action / Resource / Condition 每条 Statement 独立生效,Condition 是可选的“开关” 多个 Statement 按顺序评估,Deny 优先于 Allow

看清这个结构了吗?核心就是 Statement 数组里的每一条规则都包含那几个字段。你写策略时,脑子里要先画这个树,再填参数,基本上就不会漏字段了。

💡 个人习惯: 我每次写好桶策略,都会用 AWS Policy Simulator 模拟一遍。虽然那是后话了,但你先记住——别直接往生产桶上怼。我吃过一次亏,写了一条有漏洞的 Condition,结果内网的人反而比外网权限小。模拟器能帮你把所有条件组合跑一遍,省得到时候半夜被报警电话叫醒。

3.8 我再强调几个容易忽略的细节

  • 引号必须是英文双引号,不能用单引号。我在帮同事修bug时发现他把整个策略写成了单引号,s3直接报MalformedPolicy,完全看不出来。
  • 数组内最后一个元素不要加逗号。虽然部分编辑器容错,但s3严格解析。我曾经因为多打了一个逗号,策略保存失败,CI/CD卡了半小时。
  • Resource 中的 ARN 区分大小写。桶名一般全小写,但如果你写错了大小写,策略会作用于不存在的资源,等于白写。

⚠️ 重要提醒: 如果你给匿名用户("Principal": "*")配了写操作(比如 s3:PutObject),相当于你的桶变成了公共上传区。我曾经在测试环境忘了一条这样的策略,结果被机器人上传了几百个加密文件,存储费翻了三倍。所以强烈建议:凡是涉及匿名用户的策略,一定要用 Condition 把来源IP、HTTPS 这些硬条件卡死。

好,这一章就到这里。你只要把那棵 JSON 结构树刻在脑子里,桶策略就没什么神秘的了。接下来写策略的时候,每写一行都问自己:这条 Statement 的 Effect 是什么?Principal 是谁?Action 和 Resource 配对了没有?Condition 加得对不对?问完这四步,基本不会出错。