简介

本文主要通过演示 Amazon S3 如何防盗链(Hotlinking)来解释 Amazon S3 的访问权限以及存储桶策略和策略评估逻辑。

使用 Amazon S3 作为网站的外链资源存储服务器是有诸多好处的,但是也有不好的方面,比如很容易因为请求数和带宽使用过大而导致账单猛增,这就不得不提“防盗链”。

策略的写法

网上搜索一下,有很多 Amazon S3 “防盗链”的办法(本文以“图片”来代指存储桶中的对象),不过基本上都是靠增加存储桶策略(bucket policy)。最典型的一个存储桶策略代码如下:

{
  "Version":"2012-10-17",
  "Id":"PreventHotLinking",
  "Statement":[
    {
      "Sid":"Allow get requests originated from www.example.com and example.com",
      "Effect":"Allow",
      "Principal":"*",
      "Action":"s3:GetObject",
      "Resource":"arn:aws:s3:::examplebucket/*",
      "Condition":{
        "StringLike":{"aws:Referer":["http://www.example.com/*","http://example.com/*"]}
      }
    }
  ]
}

它还有一个相反写法的,如下,上面的使用的是 “Allow”,下面这个使用的 “Deny”,这两个策略乍一看效果好像应该一样,其实他们的效果是不一样的,通俗的说,下面这个策略 “更严格”,或者理解成它的权限设定 “更窄”。具体后面说明。

{
  "Version":"2012-10-17",
  "Id":"PreventHotLinking",
  "Statement":[
    {
      "Sid":"Allow get requests originated from www.example.com and example.com",
      "Effect":"Allow",
      "Principal":"*",
      "Action":"s3:GetObject",
      "Resource":"arn:aws:s3:::examplebucket/*",
      "Condition":{
        "StringLike":{"aws:Referer":["http://www.example.com/*","http://example.com/*"]}
      }
    }
  ]
}

策略(Policy)的写法,可以参考 Amazon 官方文档: IAM Policy Elements Reference ,文档讲的非常详细,而且有多种语言的版本。在此,只放一张简单的策略结构演示图:

AccessPolicyLanguage_General_Policy_Structure.diagram

多数情况下,上面这个策略就足够了,可以起到防盗链的作用。如果别人把你的图片盗链到他的网站,那么网页请求的 Referer 就会是他的网址,而我们的策略是只允许自己的网址,所以除了自己网址外的所有请求都会是“403 Forbidden”。另外需要注意一点,策略的 “Version” 部分,网上的多数教程都是 “2008-10-17”,这是旧版本的,目前最新的是 “2012-10-17”

上面这个策略多数情况是可以的,但是还有不管用的时候。如果存储桶中的某个图片有它自己的 ACL (Access Control List) 设置,允许外部打开或下载这个图片,如下图所示,那么虽然上面的策略设置了只允许指定域名,但是外界依然可以随意访问这个图片。

单独的 ACL (Access Control List) 设置

这也就是网上好多人提问的“为什么我设置了存储桶策略但是依然无法防盗链”的原因。

ACL

ACL 是 IAM (Identity and Access Management) 还没出来之前 Amazon S3 所使用的一种访问控制机制,它可以针对存储桶中个别的对象来单独设置不一样的权限,这点是比较有用的。

如果你的存储桶里的所有图片都像上面这样,有其自己的 ACL 并且设置成了所有人可以打开和下载,那要怎么更改策略才能把这个所有人可以访问的设定覆盖掉呢?(当然也可以手动或者使用脚本或程序来自动批量修改图片的 ACL,但是无论手动或者自动修改,在图片数量比较多的情况下都很劳民伤财

前面提到了,策略一个是 “Allow” 写法,一个是 “Deny” 写法,如果我们使用 “Deny” 写法的话,因为 “Deny” 的优先级,可以达到覆盖 ACL 设定的的效果。

策略评估逻辑

这里提到了“优先级”,是不太正规的说法,官方说法是 IAM 策略评估逻辑,具体可以参考 Amazon 官方文档: IAM Policy Evaluation Logic

官方文档里说的非常详细,在此我着重说两点。

  1. 策略评估逻辑所遵循的规则:

    • 默认的,所有请求都被拒绝。(通常,始终允许使用账户证书创建的访问该账户资源的请求。)
    • 显式允许(explicit allow)可以覆盖默认设置。(官方翻译成“显式”,我感觉还是使用“明确的”这个意思好理解)
    • 显式拒绝(explicit deny)将覆盖任何允许。

    策略的评估(evaluate)顺序对评估结果有任何影响。所有策略被评估后的最终结果是,这个请求要么被允许要么被拒绝。

    下面这个流程图比较有助于理解:

    AccessPolicyLanguageEvaluationFlow.diagram

  2. 默认情况下的“拒绝”和“显式拒绝”(明确的“拒绝”)的区别

    默认的“拒绝”是可以被“显式允许”(明确的“允许”)所覆盖的,但是“显示拒绝”(明确的“拒绝”)是无法被覆盖掉的。

    看图最好理解了:

    AccessPolicyLanguageAllowOverride.diagram

    情景1里的请求因为对不上 Policy A1,所以被默认拒绝了,但是因为 Policy B 有一个明确的允许,所以这个允许把默认的拒绝覆盖掉了,最终结果是允许。

    情景2里的请求因为有一个明确的拒绝 Policy A2,所以即使再有允许或者其他情况都不管用了,这一个明确的拒绝直接可以导致结果也是拒绝。

更加安全的防盗链策略

理解了策略评估逻辑,前面的问题就很容易解决了,我们只需要添加一个 “显示拒绝”的策略就可以把图片的ACL覆盖掉(官方说法是“取代”) 了,这个官方文档有明确的说明,下面是官方文档的截图:

Explicit deny supersedes ACL

所以,我们把“显示允许”和“显示拒绝”两个声明合并到一块,最后的策略如下:

{
  "Version": "2012-10-17",
  "Id": "PreventHotLinking",
  "Statement": [
    {
      "Sid": "Allow get requests referred by www.mysite.com and mysite.com",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::examplebucket/*",
      "Condition": {
        "StringLike": {"aws:Referer": ["http://www.example.com/*","http://example.com/*"]}
      }
    },
    {
      "Sid": "Explicit deny to ensure requests are allowed only from specific referer.",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": "arn:aws:s3:::examplebucket/*",
      "Condition": {
        "StringNotLike": {"aws:Referer": ["http://www.example.com/*","http://example.com/*"]}
      }
    }
  ]
}

这种 “Allow” 加 “Deny” 的策略与前面那个只有 “Allow” 的相比更加安全,所以推荐这种写法,防盗链而且更安全。

遇到的问题

但是我在使用过程中还遇到两个问题,在这里记录下,如果有网友也遇到过类似问题,说不定可以参考。

问题一

由于我使用的网站程序需要在没有 HTTP referer 的情况下也可以访问和下载图片,所以上面这个策略是不行的,因为它同样会禁掉 HTTP referer 为空值的访问。怎么办呢,我试过好些个的声明(statement)写法,都不行,最后甚至绝望了想干脆列举一些常见的域名后缀,把这些后缀禁掉,至于 referer 为空的就不管了,全部允许。

后来在查找资料的时候恍然大悟,既然一个策略可以并列放着多个声明(statement),那是不是条件(condition)也可以包含多个判断呢,再搜索一下,果然可以,官网示例就有这么写的。

根据官方文档,条件元素(condition)下面可以包括多个判断(可以理解为子条件),这些个子条件合并到一起类似一个“逻辑与”(logic AND),只有当请求符合每个子条件,这个总的条件才为真,才会执行声明中的 “Allow” 或者 “Deny”。

所以就有了下面的这种策略写法:

{
  "Version": "2012-10-17",
  "Id": "PreventHotLinking",
  "Statement": [
    {
      "Sid": "Explicit deny",
      "Effect": "Deny",
      "Principal": {
        "AWS": "*"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::examplebucket/*",
      "Condition": {
        "StringNotLike": {
          "aws:Referer": [
            "http://www.example.com/*",
            "http://example.com/*",
          ]
        },
        "NotIpAddress": {"aws:SourceIp": "133.166.199.211/32"}
      }
    }
  ]
}

其实就是多加了一条 IP 地址的判断,配合 referer,以缩小范围,因为声明是 “Deny”,满足条件的范围小了,相当于禁掉的范围更大了,正好解决问题一。

问题二

使用了上面的策略,可以满足我的网站的要求了,测试了一下(Chrome 浏览器下)没有问题。可是我在 Firefox 浏览器下测试的时候又发现了一个问题,Firefox 的 HTTP referer 里的网址是不带斜杠(“/”)的,不知道是不是特殊情况,如果这样的话,前面这些策略里网址的写法就需要修改,比如把 "http://blog.zmr.xyz/*" 改成 "http://blog.zmr.xyz*" ,否则在 Firefox 下可能该被拒绝的请求却被允许了,反之亦是。

Firefox 浏览器下的 Referer 值