ChatGPT解决这个技术问题 Extra ChatGPT

如何为 Cloudfront 上的静态托管网站的子目录设置默认根对象?

如何在 Cloudfront 上的静态托管网站上为子目录设置默认根对象?具体来说,我希望在用户请求 www.example.com/subdir 时提供 www.example.com/subdir/index.html。请注意,这是为了交付保存在 S3 存储桶中的静态网站。此外,我想使用源访问身份将 S3 存储桶的访问权限限制为仅限 Cloudfront。

现在,我知道 Cloudfront 的工作方式与 S3 和亚马逊状态 specifically 不同:

CloudFront 默认根对象的行为不同于 Amazon S3 索引文档的行为。当您将 Amazon S3 存储桶配置为网站并指定索引文档时,即使用户请求存储桶中的子目录,Amazon S3 也会返回索引文档。 (索引文档的副本必须出现在每个子目录中。)有关将 Amazon S3 存储桶配置为网站和索引文档的更多信息,请参阅 Amazon Simple Storage Service 开发人员指南中的在 Amazon S3 上托管网站一章。

因此,尽管 Cloudfront 允许我们指定默认根对象,但这仅适用于 www.example.com 而不适用于 www.example.com/subdir。为了绕过这个困难,我们可以将源域名更改为指向 S3 给定的网站端点。这很好用,并允许统一指定根对象。不幸的是,这似乎与 origin access identities 不兼容。具体来说,上述链接指出:

更改为编辑模式:Web 分发 - 单击“源”选项卡,单击要编辑的源,然后单击“编辑”。您只能为 Origin Type 为 S3 Origin 的源创建源访问身份。

基本上,为了设置正确的默认根对象,我们使用 S3 网站端点而不是网站存储桶本身。这与使用原始访问身份不兼容。因此,我的问题归结为

是否可以为 Cloudfront 上的静态托管网站的所有子目录指定默认根对象?是否可以为从 Cloudfront 提供的内容设置源访问身份,其中源是 S3 网站端点而不是 S3 存储桶?

我认为现在使用 Lambda@edge 可以做到这一点,使用一个将所有以 / 结尾的 URL 重定向到 /index.html 的函数我将在我的网站上尝试它并报告结果并发布详细配置作为答案。
22 年 1 月 - @ktutnik 在此处的回答 stackoverflow.com/a/69157535/216695 似乎是最干净和最简单的方法 - 仍然支持 OAI 并且可以完全锁定存储桶。

J
JBaczuk

有办法做到这一点。不要通过在下拉列表中选择它来将其指向您的存储桶 (www.example.com.s3.amazonaws.com),而是将其指向存储桶的静态域(例如 www.example.com.s3-website-us -west-2.amazonaws.com):

https://i.stack.imgur.com/cEqeo.png

感谢This AWS Forum thread


任何人都知道当拥有 s3 来源和 web 来源时,这是否会有所不同?
如果我只想通过 HTTPS 提供我的整个网站和文件,这是否可以正常工作?
这是否意味着必须将 S3 启用为 Web 服务器?
OP 明确表示这种方法对他不起作用:“为了解决这个困难,我们可以将源域名更改为指向 S3 给出的网站端点。这很好,并且允许统一指定根对象。不幸的是,这似乎与原始访问身份不兼容”。 AWS 自己似乎为此推荐 lamda@edge - aws.amazon.com/blogs/compute/…
这与 Cloud Front - Origin Access Identity 不兼容。您将无法通过这种方式限制对 S3 存储桶的访问。
k
kenske

激活 S3 托管意味着您必须向世界开放存储桶。就我而言,我需要将存储桶保持私有并使用原始访问身份功能来限制仅访问 Cloudfront。就像@Juissi 建议的那样,Lambda 函数可以修复重定向:

'use strict';

/**
 * Redirects URLs to default document. Examples:
 *
 * /blog            -> /blog/index.html
 * /blog/july/      -> /blog/july/index.html
 * /blog/header.png -> /blog/header.png
 *
 */

let defaultDocument = 'index.html';

exports.handler = (event, context, callback) => {
    const request = event.Records[0].cf.request;

    if(request.uri != "/") {
        let paths = request.uri.split('/');
        let lastPath = paths[paths.length - 1];
        let isFile = lastPath.split('.').length > 1;

        if(!isFile) {
            if(lastPath != "") {
                request.uri += "/";
            }

            request.uri += defaultDocument;
        }

        console.log(request.uri);
    }

    callback(null, request);
};

发布函数后,转到 AWS 控制台中的云端分发。转到 Behaviors,然后选择 Lambda Function Associations 下的Origin Request,最后将 ARN 粘贴到您的新函数中。


有一个类似于该函数的准备部署 lambda 函数:serverlessrepo.aws.amazon.com/applications/…
这里的问题是这个功能需要部署到 us-east-1,所以如果你有一家公司受到严格的 GDPR 监管,不允许在德国以外的地方有任何一点,那么这不适合你。
k
ktutnik

(2021 年 5 月新功能)CloudFront 功能

在下面创建一个简单的 JavaScript 函数

function handler(event) {
    var request = event.request;
    var uri = request.uri;
    
    // Check whether the URI is missing a file name.
    if (uri.endsWith('/')) {
        request.uri += 'index.html';
    } 
    // Check whether the URI is missing a file extension.
    else if (!uri.includes('.')) {
        request.uri += '/index.html';
    }

    return request;
}

阅读here了解更多信息


这非常有效,而且比使用 Lambda 便宜得多。 Here's an example 如何在 Serverless Framework 部署脚本中设置 CF 函数(只需将函数代码替换为上述答案中的代码/链接)。
这完美无瑕 - 只需在 CF 的相关部分(左侧菜单)中创建一个函数,然后将其与默认行为相关联 -> 我的发行版的查看器请求。雨果网站现在按预期工作!
我已经尝试过这个解决方案,但没有喜悦。你能看出我做错了吗?stackoverflow.com/questions/70717168/…
M
Max Desiatov

有一个 "official" guide published on AWS blog 建议设置由您的 CloudFront 分配触发的 Lambda@Edge 函数:

当然,期望用户总是在每个 URL 的末尾键入 index.html(甚至知道它应该在那里)是一种糟糕的用户体验。到目前为止,还没有一种简单的方法可以通过 CloudFront 向用户提供这些更简单的 URL(相当于 Apache Web 服务器配置中的 DirectoryIndex 指令)。如果您仍然希望能够使用 OAI 限制对 S3 源的访问,则不会。但是,随着 Lambda@Edge 的发布,您可以使用在 CloudFront 边缘节点上运行的 JavaScript 函数来查找这些模式并从 S3 源请求适当的对象密钥。解决方案 在此示例中,您使用 CloudFront 边缘的计算能力检查来自客户端的请求。然后重新编写请求,以便 CloudFront 为任何以“/”结尾的请求 URI 请求默认索引对象(在本例中为 index.html)。当对 Web 服务器发出请求时,客户端会在请求中指定要获取的对象。您可以使用此 URI 并对其应用正则表达式,以便在 CloudFront 从源请求对象之前将这些 URI 解析为默认索引对象。使用以下代码:

'use strict';
exports.handler = (event, context, callback) => {

    // Extract the request from the CloudFront event that is sent to Lambda@Edge
    var request = event.Records[0].cf.request;

    // Extract the URI from the request
    var olduri = request.uri;

    // Match any '/' that occurs at the end of a URI. Replace it with a default index
    var newuri = olduri.replace(/\/$/, '\/index.html');

    // Log the URI as received by CloudFront and the new URI to be used to fetch from origin
    console.log("Old URI: " + olduri);
    console.log("New URI: " + newuri);

    // Replace the received URI with the URI that includes the index page
    request.uri = newuri;

    // Return to CloudFront
    return callback(null, request);

};

按照上面链接的指南查看设置所需的所有步骤,包括 S3 存储桶、CloudFront 分配和 Lambda@Edge 函数创建。


J
Johan Gorter

还有另一种方法可以获取在子目录中提供的默认文件,例如 example.com/subdir/。您实际上可以(以编程方式)将带有密钥 subdir/ 的文件存储在存储桶中。此文件将不会显示在 S3 管理控制台中,但它确实存在,CloudFront 将提供它。


S3 将 subdir/ 转换为 subdir;当您尝试上传 HTML 时。此外,当您尝试访问 example.com/subdir/ 时,它会失败,如果您尝试访问 example.com/subdir;它下载 HTML 文件而不是渲染它。
这实际上是我发现的最佳方法之一,因为它适用于所有 S3 发行版,无需特定配置,并且确实需要使用 lambda@edge(在执行 lambda 时会产生额外的重定向并减慢页面服务速度)。我不同意@jacobfogg 的评论。当以编程方式使用时,它工作得很好。我做了一个由 S3 事件触发的小型 lambda 函数。请看我下面的回复。
感谢 Johan 和@Jeremie,这种方法有效!您也可以使用 awscli 执行此操作。
啊,我错过了这只能以编程方式工作的联系。我已经通过具有我指出的行为的 Web 界面测试了这个解决方案。下次遇到这种问题时,我会把它放在后兜里。
A
Aidin

我完全同意这是一个荒谬的问题! CloudFront 知道将 index.html 作为 Default Root Object 提供服务,但他们仍然说它不适用于子目录 (source),这一事实完全奇怪!

CloudFront 默认根对象的行为不同于 Amazon S3 索引文档的行为。当您将 Amazon S3 存储桶配置为网站并指定索引文档时,即使用户请求存储桶中的子目录,Amazon S3 也会返回索引文档。

我个人认为 AWS 已经做到了这一点,因此 CloudFront 仅成为 CDN(加载资产,其中没有任何逻辑),并且对您网站中路径的每个请求都应该从“服务器”(例如 EC2 节点/Php 服务器或 Lambda 函数。)

这种限制的存在是为了增强安全性,还是将事物分开(即逻辑和存储分离),还是为了赚更多的钱(强制人们拥有专用服务器,即使是静态内容)还有待商榷。

无论如何,我在这里总结了可能的解决方案,以及它们的优缺点。

1) S3 可以是公共的 - 使用自定义来源。

这是最简单的一个,最初由 @JBaczuk answerthis github gist 发布。由于 S3 已经支持通过 Static Website Hosting 在子目录中提供 index.html,您只需:

转到 S3,启用静态网站托管 以 http://.s3-website-us-west-2.amazonaws.com 的形式获取 URL 在 CloudFront 中创建一个新的 Origin 并将其作为自定义输入Origin(而不是 S3 ORIGIN),因此 CloudFront 在获取内容时将其视为外部网站。

优点:

很容易设置。它支持 /about/、/about 和 /about/index.html 并将最后两个重定向到第一个,正确。

缺点:

如果您在 S3 存储桶中的文件不在 S3 的根目录中(比如在 /artifacts/* 中,那么转到 www.domain.com/about(没有尾随 /)会将您重定向到 www.domain.com/artifacts/about这是您根本不想要的东西!基本上,如果您从 CloudFront 提供服务并且文件路径(从根目录)不匹配,则 S3 中的 /about 到 /about/ 重定向会中断。安全性和功能性:您不能S3 Private。这是因为 CloudFront 的 Origin Access Identity 将不被支持,很明显,因为 CloudFront 被指示将此 Origin 作为随机网站。这意味着用户可以直接从 S3 获取文件,这可能不是你的由于安全/WAF 问题,以及网站实际工作,如果您的 JS/html 仅依赖于您的域的路径。[可能是一个问题] CloudFront 和 S3 之间的通信不是推荐的方式优化东西。[也许?]有人抱怨它不能顺利运行y 对于分发中的多个来源(即希望 /blog 去某个地方)[也许?]有人抱怨它没有按预期保留原始查询参数。

2)官方解决方案 - 使用 Lambda 函数。

它是 the official solution(尽管文档来自 2017 年)。还有一个 ready-to-launch 3rd-party Application (JavaScript source in github) 和 Python Lambda 函数示例 (this answer)。

从技术上讲,通过这样做,您创建了一个微型服务器(他们称之为无服务器!),它只为 CloudFront 的 S3 原始请求提供服务(因此,它基本上位于 CloudFront 和 S3 之间。)

优点:

嘿,这是官方的解决方案,所以可能持续时间更长,是最优化的解决方案。您可以根据需要自定义 Lambda 函数并对其进行控制。您可以在其中支持进一步的重定向。如果实施正确,(如第 3 方 JS 之一,我不认为是官方的)它支持 /about/ 和 /about 两者(从后者重定向而不尾随 / 到前者)。

缺点:

设置是另一回事。有眼睛是另一回事,所以它不会坏掉。检查何时发生故障是另一回事。还有一件事需要维护——例如,这里的第三方自 2021 年 1 月(现在是 2021 年 4 月)以来就已开放 PR。第 3 方 JS 解决方案不保留查询参数。所以 /about?foo=bar 是 301 重定向到 /about/ 而不是 /about/?foo=bar。您需要对该 lambda 函数进行更改以使其工作。第 3 方 JS 解决方案将 /about/ 保留为规范版本。如果您希望 /about 成为规范版本(即其他格式通过 301 重定向到它),您必须对脚本进行更改。 [次要] 它仅在 us-east-1 中有效(自 2020 年以来在 Github 中开放问题,2021 年 4 月仍然开放和实际问题)。 [次要] 尽管考虑到 CloudFront 的缓存,但它有其自身的成本,但应该不会很重要。

3) 在 S3 中创建假的“文件夹文件” - 使用手动脚本。

这是前两者之间的一种解决方案——它支持 OAI(私有 S3)并且不需要服务器。虽然有点恶心!

您在这里所做的是,您运行一个脚本,为 /about/index.html 的每个子目录在 S3 中创建一个名为 /about 的对象(具有 key)并将该 HTML 文件(内容和 content-type)复制到这个对象。

使用 AWS CLI 在 this Reddit answerthis answer 中可以找到示例脚本。

优点:

安全:支持 S3 Private 和 CloudFront OAI。没有额外的 live 片段:脚本运行预上传到 S3(或一次性),然后系统保持不变,只有 S3 和 CF 两个片段。

缺点:

[需要确认] 它支持 /about 但不支持 /about/ 尾随 / 我相信。从技术上讲,您存储了两个不同的文件。如果有大量 HTML 文件,可能看起来令人困惑,并使您的部署变得昂贵。您的脚本必须手动查找所有子目录并在 S3 中从中创建一个虚拟对象。这有可能在未来打破。

PS。其他技巧)

在自定义错误上使用 Javascript 的肮脏技巧

虽然它看起来不像真的,但 this answer 值得称赞,IMO!

您让拒绝访问(404 变成 403)通过,然后捕获它们,并通过 JS 手动将它们重定向到正确的位置。

优点

再次,易于设置。

缺点

它依赖于客户端的 JavaScript。它会与 SEO 混淆——尤其是在爬虫不运行 JS 的情况下。它弄乱了用户的浏览器历史记录。 (即后退按钮)并且可能可以通过 HTML5 history.replace 进行改进(并且变得更复杂!)。


J
Juissi

该问题的解决方法是利用 lambda@edge 重写请求。只需为 CloudFront 分发的查看器请求事件设置 lambda,并使用默认根文档(例如 index.html)重写以“/”结尾且不等于“/”的所有内容。


此处有关此方法的更多详细信息:aws.amazon.com/blogs/compute/…
不幸的是,Lambda@Edge 仅适用于 us-east-1 区域,来源:github.com/awslabs/serverless-application-model/issues/635
Lambda@Edge 函数仅部署在 us-east-1 上,该函数被复制并在全球边缘位置运行,其运行位置取决于离用户最近的边缘位置。
u
user1333371

使用 lambda@edge 的另一个替代方法是使用 CloudFront 的错误页面。设置 Custom Error Response 以将所有 403 发送到特定文件。然后将 javascript 添加到该文件以将 index.html 附加到以 / 结尾的 url。示例代码:

if ((window.location.href.endsWith("/") && !window.location.href.endsWith(".com/"))) {
    window.location.href = window.location.href + "index.html";
}
else {
    document.write("<Your 403 error message here>");
}

r
runwuf

Johan GorterJeremie 表示 index.html 可以存储为具有键 subdir/ 的对象。我用 awsclis3api copy-object 验证了这种方法有效,以及另一种简单的方法来做到这一点

aws s3api copy-object --copy-source bucket_name/subdir/index.html --key subdir/ --bucket bucket_name

C
Community

更新:看来我错了!请参阅 JBaczuk 的答案,这应该是该线程上公认的答案。

不幸的是,您的两个问题的答案都是否定的。

1. 是否可以为 Cloudfront 上的静态托管网站的所有子目录指定默认根对象?

不。如 AWS CloudFront docs 中所述...

...如果您定义默认根对象,最终用户对您的分发的子目录的请求不会返回默认根对象。例如,假设 index.html 是您的默认根对象,并且 CloudFront 收到最终用户对 CloudFront 分配下的安装目录的请求:http://d111111abcdef8.cloudfront.net/install/ CloudFront 不会返回默认根对象即使 index.html 的副本出现在安装目录中。 ... CloudFront 默认根对象的行为不同于 Amazon S3 索引文档的行为。当您将 Amazon S3 存储桶配置为网站并指定索引文档时,即使用户请求存储桶中的子目录,Amazon S3 也会返回索引文档。 (索引文档的副本必须出现在每个子目录中。)

2. 是否可以为从 Cloudfront 提供的内容设置源访问身份,其中源是 S3 网站端点而不是 S3 存储桶?

不是直接的。您使用 CloudFront 的来源选项是 S3 存储桶或您自己的服务器。

不过,第二个选项确实开辟了一些有趣的可能性。这可能会破坏您尝试做的事情的目的,但您可以设置自己的服务器,其唯一工作是成为 CloudFront 原始服务器。

当收到对 http://d111111abcdef8.cloudfront.net/install/ 的请求时,CloudFront 会将此请求转发到您的源服务器,请求 /install。您可以根据需要配置源服务器,包括在这种情况下提供 index.html

或者您可以编写一个小 Web 应用程序,它只接受这个调用并直接从 S3 获取它。

但是我意识到设置自己的服务器并担心扩展它可能会破坏您首先尝试做的事情的目的。


我遇到的一个问题是,让它工作意味着你将有两 (2) 个 URL 能够在 s3 上访问你的网站。您的云端 URL 和 s3 url (bucket_name.s3-website-us-east-1.amazonaws.com)
R
Rishikesh Darandale

可以使用发布的cloudfront functions,这里是sample code

注意:如果您使用的是static website hosting,那么您不需要任何功能!


w
whtevn

我知道这是一个老问题,但我自己只是在努力解决这个问题。最终,我的目标不是在目录中设置默认文件,而是更多地获得在没有 .html 的情况下提供的文件的最终结果

我最终从文件名中删除了 .html,并以编程方式/手动将 mime 类型设置为 text/html。这不是传统的方式,但它似乎确实有效,并且在不牺牲 cloudformation 的好处的情况下满足了我对漂亮 url 的要求。设置 mime 类型很烦人,但在我看来要付出很小的代价才能获得好处


J
Jeremie

@johan-gorter 上面指出,CloudFront 提供的文件的密钥以 / 结尾。经过调查,此选项似乎有效,并且可以以编程方式在 S3 中创建此类文件。因此,我写了一个小 lambda,它在 S3 上创建文件时触发,后缀为 index.html 或 index.htm

它的作用是将对象 dir/subdir/index.html 复制到对象 dir/subdir/

import json
import boto3

s3_client = boto3.client("s3")

def lambda_handler(event, context):

    for f in event['Records']:

        bucket_name = f['s3']['bucket']['name']
        key_name = f['s3']['object']['key']
        source_object = {'Bucket': bucket_name, 'Key': key_name}

        file_key_name = False

        if key_name[-10:].lower() == "index.html" and key_name.lower() != "index.html":
            file_key_name = key_name[0:-10]
        elif key_name[-9:].lower() == "index.htm" and key_name.lower() != "index.htm":
            file_key_name = key_name[0:-9]
        
        if file_key_name:
            s3_client.copy_object(CopySource=source_object, Bucket=bucket_name, Key=file_key_name)