基于引用者限制访问AWS S3存储桶

8

我将尝试限制对 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/*"
                ]
            }
        }
    }
 ]
}

这里有三个网站othersite1,2和3,它们调用了我在example.com域名下存储在S3存储桶中的一个对象。我还将CloudFront分发附加到了该存储桶上。我在字符串条件前后使用通配符*。引用者可能是othersite1.com/folder/another-folder/page.html,也可能使用http或https。
我不知道为什么会出现403禁止错误。
我这样做主要是因为我不希望其他网站调用该对象。
非常感谢您的帮助。

我不确定是否支持前导通配符。我建议尝试去掉它们,然后再次测试。 - Çağatay Gürtürk
在CloudFront缓存行为中,您是否配置了“Referer”标头以进行白名单处理,以便将其转发到S3?未转发的任何标头均无法由存储桶策略使用,这将是您遇到的第一个问题……尽管问题比这更复杂,因为如果您转发“Referer”,则缓存命中率将不会很高。 - Michael - sqlbot
@ÇağatayGürtürk 谢谢您的评论,我尝试过删除,甚至按照我在 Chrome 检查器中看到的确切引用者编写,但仍然无法正常工作。 - esdrayker
@Michael-sqlbot 我没有配置“referer”头以进行白名单操作。我会去做的!谢谢!希望这样会有效。 - esdrayker
@Michael 我确实这样做了,效果非常好。在测试之前我一直遇到这种问题,即使创建了无效ation也是如此。感谢你的回答。我不知道该如何解决,但你的方法是解决方案。 - esdrayker
显示剩余4条评论
1个回答

16

为了正确地进行缓存,CloudFront在将请求转发到源服务器之前几乎会删除所有的请求头。

Referer | CloudFront将删除该标头。

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

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

这非常好地说明了CloudFront不转发它的原因。如果CloudFront转发了标头,然后盲目地缓存结果,存储桶策略是否具有预期效果将取决于第一个请求是来自预期站点中的一个还是其他地方 - 其他请求者将获得缓存响应,这可能是错误的响应。

(tl;dr) 在 CloudFront 缓存行为设置中,将 Referer 标头加入白名单以转发到源站 (origin),可以解决此问题。

但是,有一个小问题。

现在你将 Referer 标头转发到 S3,已经扩展了缓存键 (cache key) -- 即 CloudFront 用于缓存响应的内容列表 -- 包括了 Referer 标头。

因此,现在对于每个对象,只有当传入请求的 Referer 标头与已缓存请求的标头完全匹配时,CloudFront 才不会从缓存中提供响应...否则该请求必须转到 S3。并且,关于 referer 标头,它是指引用的页面,而不是引用的网站,因此来自授权网站的每个页面都将在 CloudFront 中拥有自己的这些资产的缓存副本。

这本身并不是问题。这些额外对象的副本没有费用,而且这就是CloudFront的设计原理...问题在于,它降低了特定对象在特定边缘缓存中的可能性,因为每个对象都必然被引用更少。如果您有大量的流量,则这变得不那么重要--甚至可以忽略不计--如果您的流量较小,则变得更加重要。较少的缓存命中意味着页面加载速度变慢,需要更多请求发送到S3。
是否理想完全取决于您使用CloudFront和S3的方式,没有正确答案。
但是,以下是替代方案:
您可以从转发到S3的标头白名单中删除Referer标头,并通过配置CloudFront触发Lambda@Edge Viewer Request trigger来检查每个请求,阻止那些不来自您允许的引荐页面的请求,从而消除对缓存命中率的负面影响。
一个Viewer Request 触发器会在匹配特定缓存行为之后触发,但在实际缓存检查之前,大部分传入的标头仍然完整。您可以允许请求继续进行,可选地进行修改,或者您可以生成响应并取消其余的CloudFront处理。下面我将进行说明--如果Referer标头的主机部分不在可接受值的数组中,我们将生成403响应;否则,请求将继续进行,缓存将被检查,并且只在需要时咨询源。
触发此触发器会为每个请求添加一小部分开销,但这种开销可能会摊销为比减少缓存命中率更理想的结果。因此,以下不是“更好”的解决方案--只是另一种解决方案。
这是一个使用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);
};

谢谢@Micheal,这非常有帮助。正如你所说,解决方案取决于使用情况。我们确实有很多流量,但也有很多不同的引荐者。因此,现在我们必须权衡成本和性能。从Lambda价格来看,即使对于高流量,它似乎是一个不错的选择,但它确实增加了一些开销。另一方面,如果目标是不断添加域并增加复杂策略,则桶策略似乎不太可扩展。这确实非常有趣。再次感谢! - esdrayker

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接