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

How do you set a default root object for subdirectories for a statically hosted website on Cloudfront?

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

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

The behavior of CloudFront default root objects is different from the behavior of Amazon S3 index documents. When you configure an Amazon S3 bucket as a website and specify the index document, Amazon S3 returns the index document even if a user requests a subdirectory in the bucket. (A copy of the index document must appear in every subdirectory.) For more information about configuring Amazon S3 buckets as websites and about index documents, see the Hosting Websites on Amazon S3 chapter in the Amazon Simple Storage Service Developer Guide.

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

Change to edit mode:

Web distributions – Click the Origins tab, click the origin that you want to edit, and click Edit. You can only create an origin access identity for origins for which Origin Type is S3 Origin.

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

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

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

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

很遗憾,您的两个问题的答案都是否定的。

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

没有。如 AWS CloudFront docs...

中所述

... If you define a default root object, an end-user request for a subdirectory of your distribution does not return the default root object. For example, suppose index.html is your default root object and that CloudFront receives an end-user request for the install directory under your CloudFront distribution:

http://d111111abcdef8.cloudfront.net/install/

CloudFront will not return the default root object even if a copy of index.html appears in the install directory.

...

The behavior of CloudFront default root objects is different from the behavior of Amazon S3 index documents. When you configure an Amazon S3 bucket as a website and specify the index document, Amazon S3 returns the index document even if a user requests a subdirectory in the bucket. (A copy of the index document must appear in every subdirectory.)

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

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

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

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

或者您可以编写一个小的网络应用程序来接受这个调用并直接从 S3 获取它。

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

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

感谢This AWS Forum thread

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

我知道这是一个老问题,但我自己也在努力解决这个问题。最终,我的目标不是在目录中设置默认文件,而是获得文件的最终结果,该文件的末尾没有 .html

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

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

激活 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 粘贴到您的新函数中。

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

Of course, it is a bad user experience to expect users to always type index.html at the end of every URL (or even know that it should be there). Until now, there has not been an easy way to provide these simpler URLs (equivalent to the DirectoryIndex Directive in an Apache Web Server configuration) to users through CloudFront. Not if you still want to be able to restrict access to the S3 origin using an OAI. However, with the release of Lambda@Edge, you can use a JavaScript function running on the CloudFront edge nodes to look for these patterns and request the appropriate object key from the S3 origin.

Solution

In this example, you use the compute power at the CloudFront edge to inspect the request as it’s coming in from the client. Then re-write the request so that CloudFront requests a default index object (index.html in this case) for any request URI that ends in ‘/’.

When a request is made against a web server, the client specifies the object to obtain in the request. You can use this URI and apply a regular expression to it so that these URIs get resolved to a default index object before CloudFront requests the object from the origin. Use the following code:

'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 函数创建。

使用 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>");
}

@johan-gorter 上面指出 CloudFront 提供的文件的键以 / 结尾 经过调查,似乎此选项有效,并且可以通过编程方式在 S3 中创建此类文件。因此,我写了一个在 S3 上创建文件时触发的小 lambda,后缀为 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)

Johan Gorter and Jeremie 表示 index.html 可以存储为具有键 subdir/ 的对象。 我验证了这种方法的有效性以及使用 awsclis3api copy-object

的另一种简单方法
aws s3api copy-object --copy-source bucket_name/subdir/index.html --key subdir/ --bucket bucket_name

我完全同意这是一个荒谬的问题! CloudFront 知道将 index.html 服务为 Default Root Object AND STILL they say it doesn't work for subdirectories (source) 的事实真是太奇怪了!

The behavior of CloudFront default root objects is different from the behavior of Amazon S3 index documents. When you configure an Amazon S3 bucket as a website and specify the index document, Amazon S3 returns the index document even if a user requests a subdirectory in the bucket.

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

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


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

1) S3 可以是 Public - 使用自定义来源。

这是最简单的,最初由@JBaczuk as well as in this github gist 发布。由于 S3 已经支持通过 静态网站托管 在子目录中提供服务 index.html,您需要做的就是:

  1. 转到 S3,启用静态网站托管
  2. http://<bucket-name>.s3-website-us-west-2.amazonaws.com
  3. 的形式获取URL
  4. 在 CloudFront 中创建一个新的 Origin 并将其输入为 Custom Origin(并且 NOT S3 ORIGIN),因此 CloudFront 将其视为获取内容时的外部网站。

优点:

  1. 设置非常简单。
  2. 它支持 /about//about/about/index.html 并将最后两个正确重定向到第一个。

缺点:

  1. 如果您在 S3 存储桶中的文件不在 S3 的根目录中(例如 /artifacts/* 然后转到 www.domain.com/about(没有尾随 /)会将您重定向到 www.domain.com/artifacts/about,这是您根本不想要的!基本上,如果您从 CloudFront 和文件路径(从root) 不匹配。

  2. 安全性和功能性:您不能将 S3 设为私有。这是因为 CloudFront 的 Origin Access Identity 将不受支持,很明显,因为 CloudFront 被指示将此 Origin 作为随机网站。这意味着用户可能会直接从 S3 获取文件,由于 security/WAF 的顾虑,这可能不是你想要的,如果你有 JS/html 依赖于路径的网站实际工作仅作为您的域。

  3. [可能是个问题] CloudFront 和 S3 之间的通信不是推荐的优化方式。

  4. [也许吧?] 有人抱怨说它不能顺利地为分布中的多个 Origin 工作(即想要 /blog去某个地方)

  5. [也许?] 有人抱怨它没有按预期保留原始查询参数。

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

它也是the official solution (though the doc is from 2017). There is a ready-to-launch 3rd-party Application (JavaScript source in github) and example Python Lambda function ()。

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

优点:

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

缺点:

  1. 设置又是一回事。
  2. 有眼又是一回事,所以不坏。
  3. 当有东西坏了时检查又是一回事。
  4. 还需要维护一件事 -- 例如这里的第三方自 2021 年 1 月以来有 open PRs(现在是 2021 年 4 月。)
  5. 第 3 方 JS 解决方案不保留查询参数。所以 /about?foo=bar 是 301 重定向到 /about/ 而不是 /about/?foo=bar。您需要更改该 lambda 函数才能使其正常工作。
  6. 第 3 方 JS 解决方案保持 /about/ 作为规范版本。如果您希望 /about 成为规范版本(即其他格式通过 301 重定向到它),您必须对脚本进行更改。
  7. [次要](自 2020 年以来 Github 中未解决的问题,到 2021 年 4 月仍然是未解决的问题)。
  8. [次要]它有自己的成本,尽管考虑到 CloudFront 的缓存,应该不会很重要。

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

它是介于前两者之间的解决方案 -- 它支持 OAI(专用 S3)并且不需要服务器。虽然有点恶心!

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

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

优点:

  1. 安全:支持 S3 Private 和 CloudFront OAI。
  2. No additional live piece: 脚本运行s预上传到S3(或一次性)然后系统保持原样,两片仅限 S3 和 CF。

缺点:

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

PS。其他技巧)

在自定义错误

上使用 Javascript 的肮脏技巧

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

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

优点

  1. 同样,易于设置。

缺点

  1. 它依赖于客户端的JavaScript。
  2. 它会扰乱 SEO——尤其是当爬虫没有 运行 JS 时。
  3. 它扰乱了用户的浏览器历史记录。 (即后退按钮)并可能通过 HTML5 history.replace.
  4. 进行改进(并变得更复杂!)

可以使用发布cloudfront functions and here is sample code.

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

(新功能 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了解更多信息