基于 referer 限制对 AWS S3 存储桶的访问

Restricting access to AWS S3 bucket based on referer

我正在尝试限制对 S3 存储桶的访问,并且只允许基于引用者的列表中的某些域。

存储桶策略基本上是:

{
"Version": "2012-10-17",
"Id": "http referer domain lock",
"Statement": [
    {
        "Sid": "Allow get requests originating from specific domains",
        "Effect": "Allow",
        "Principal": "*",
        "Action": "s3:GetObject",
        "Resource": "arn:aws:s3:::example.com/*",
        "Condition": {
            "StringLike": {
                "aws:Referer":  [ 
                    "*othersite1.com/*",
                    "*othersite2.com/*",
                    "*othersite3.com/*"
                ]
            }
        }
    }
 ]
}

其他站点 1、2 和 3 调用了一个我存储在域 example.com 下的 s3 存储桶中的对象。 我还有一个附加到存储桶的云端分发。我在字符串条件前后使用 * 通配符。 referer 可以是 othersite1.com/folder/another-folder/page.html。推荐人也可以使用 http 或 https。

我不知道为什么会收到 403 禁止错误。

我这样做主要是因为我不希望其他站点调用该对象。

如有任何帮助,我们将不胜感激。

作为正确的缓存行为所必需的,CloudFront 在将请求转发到源服务器之前从请求中删除几乎所有请求 header。

Referer | CloudFront removes the header.

http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/RequestAndResponseBehaviorCustomOrigin.html#request-custom-headers-behavior

因此,如果您的存储桶试图阻止基于引用页面的请求(有时是为了防止盗链),S3 默认情况下将无法看到 Referer header,因为 CloudFront 不转发它。

而且,这很好地说明了 为什么 CloudFront 不转发它。如果 CloudFront 转发 header 然后盲目地缓存结果,则存储桶策略是否具有预期效果将取决于第一个请求是来自其中一个预期站点还是来自其他地方——其他请求者将获得缓存的响应,可能是 错误的 响应。

(tl;dr) 将 Referer header 列入白名单以转发到源(在 CloudFront 缓存行为设置中)解决了这个问题。

但是,有一点问题。

现在您将 Referer header 转发到 S3,您已经扩展了 缓存键 -- CloudFront 所针对的事物列表缓存响应——包括 Referer header.

因此,现在,对于每个 object,CloudFront 将不会提供来自缓存的响应,除非传入请求的 Referer header 与 完全匹配 一个来自 already-cached 请求...否则请求必须转到 S3。而且,关于引用者 header,它是引用 页面 ,而不是引用 站点 ,所以每个 来自授权站点的页面 将在 CloudFront 中拥有这些资产的自己的缓存副本。

这本身不是问题。这些额外的 object 副本是免费的,这就是 CloudFront 设计的工作方式......问题是,它降低了给定 object 在给定边缘缓存中的可能性,因为每个 object 必然会被引用得更少。如果您的流量很大,这将变得不那么重要——甚至可以忽略不计;如果您的流量较小,它就会变得更重要。更少的缓存命中意味着更慢的页面加载和更多的请求到 S3。

对于这是否适合您,没有正确的答案,因为它非常具体到您使用 CloudFront 和 S3 的方式。

但是,这是另一种选择:

您可以从 header 的白名单中删除 Referer header 以转发到 S3,并通过配置 CloudFront 触发 Lambda@Edge Viewer Request trigger 将在每个请求进入前门时对其进行检查,并阻止那些不是来自您要允许的引用页面的请求。

查看器请求触发器在特定缓存行为匹配后触发,但在检查实际缓存之前,并且大多数传入 header 仍然完好无损。您可以允许请求继续进行(可选择进行修改),或者您可以生成响应并取消其余的 CloudFront 处理。这就是我在下面说明的内容——如果 Referer header 的主机部分不在可接受值的数组中,我们将生成 403 响应;否则,请求继续,检查缓存,并仅在需要时查询来源。

触发此触发器会为每个请求增加少量开销,但该开销可能会分摊到比降低缓存命中率更可取的程度。因此,以下不是 "better" 解决方案 -- 只是替代解决方案。

这是用 Node.js 6.10 编写的 Lambda 函数。

'use strict';

const allow_empty_referer = true;

const allowed_referers = ['example.com', 'example.net'];

exports.handler = (event, context, callback) => {

    // extract the original request, and the headers from the request
    const request = event.Records[0].cf.request;
    const headers = request.headers;

    // find the first referer header if present, and extract its value;
    // then take http[s]://<--this-part-->/only/not/the/path.
    // the || [])[0]) || {'value' : ''} construct is optimizing away some if(){ if(){ if(){ } } } validation

    const referer_host = (((headers.referer || [])[0]) || {'value' : ''})['value'].split('/')[2];

    // compare to the list, and immediately allow the request to proceed through CloudFront 
    // if we find a match

    for(var i = allowed_referers.length; i--;)
    {
        if(referer_host == allowed_referers[i])
        {
            return callback(null,request);
        }
    }

    // also test for no referer header value if we allowed that, above
    // usually, you do want to allow this

    if(allow_empty_referer && referer_host === "")
    {
        return callback(null,request);
    }

    // we did not find a reason to allow the request, so we deny it.

    const response = {
        status: '403',
        statusDescription: 'Forbidden',
        headers: {
            'vary':          [{ key: 'Vary',          value: '*' }], // hint, but not too obvious
            'cache-control': [{ key: 'Cache-Control', value: 'max-age=60' }], // browser-caching timer
            'content-type':  [{ key: 'Content-Type',  value: 'text/plain' }], // can't return binary (yet?)
        },
        body: 'Access Denied\n',
    };

    callback(null, response);
};