使用 cdk 部署静态网站 - 直接 link 到页面显示访问被拒绝

deployed static website using cdk - direct link to pages shows access denied

我已经在这里部署了我的网站:

https://curlycactus.com/

如果您遍历 link,所有页面都可以正常工作。但是当我复制并粘贴直接 link 例如:

https://curlycactus.com/work/1

我收到这个错误:

<Error>
<Code>AccessDenied</Code>
<Message>Access Denied</Message>
<RequestId>40EFGXY32PKPH10V</RequestId>
<HostId>NSWqXYQGVXuN39bP9DEyqkJ8tjIDDH2xpv08l/CUwcEVUKeoRcKNnwrDm0V/eENkLczmF8935OY=</HostId>
</Error>

知道为什么会这样吗?这是我的 CDK 设置:

import * as path from "path";
import { Aws, CfnOutput, RemovalPolicy, Stack, StackProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as s3Deployment from "aws-cdk-lib/aws-s3-deployment";
import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
import * as targets from "aws-cdk-lib/aws-route53-targets";
import * as cloudwatch from "aws-cdk-lib/aws-cloudwatch";
import * as iam from "aws-cdk-lib/aws-iam";
import * as route53 from "aws-cdk-lib/aws-route53";
import * as acm from "aws-cdk-lib/aws-certificatemanager";
import { S3Origin } from "aws-cdk-lib/aws-cloudfront-origins";
import { CONSTRUCT_NAMES } from "./ConstructNames";

export interface IStaticWebsiteConstruct extends StackProps {
  domainName: string;
}

export class StaticWebsiteConstruct extends Construct {
  websiteBucket: s3.Bucket;
  deploy: s3Deployment.BucketDeployment;
  cloudFront: cloudfront.CloudFrontWebDistribution;

  constructor(parent: Stack, id: string, props: IStaticWebsiteConstruct) {
    super(parent, id);
    // create bucket which holds the website data

    const zone = route53.HostedZone.fromLookup(this, "Zone", {
      domainName: props.domainName,
    });

    const siteDomain = props.domainName;
    const cloudfrontOAI = new cloudfront.OriginAccessIdentity(
      this,
      "cloudfront-OAI",
      {
        comment: `OAI for ${id}`,
      }
    );

    this.websiteBucket = new s3.Bucket(this, CONSTRUCT_NAMES.bucket.name, {
      bucketName: CONSTRUCT_NAMES.bucket.name,
      websiteIndexDocument: "index.html",
      publicReadAccess: true,
      removalPolicy: RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
    });

    // Grant access to cloudfront
    this.websiteBucket.addToResourcePolicy(
      new iam.PolicyStatement({
        actions: ["s3:GetObject"],
        resources: [this.websiteBucket.arnForObjects("*")],
        principals: [
          new iam.CanonicalUserPrincipal(
            cloudfrontOAI.cloudFrontOriginAccessIdentityS3CanonicalUserId
          ),
        ],
      })
    );

    // TLS certificate
    const certificateArn = new acm.DnsValidatedCertificate(
      this,
      "SiteCertificate",
      {
        domainName: siteDomain,
        hostedZone: zone,
        region: "us-east-1", // Cloudfront only checks this region for certificates.
      }
    ).certificateArn;

    // Specifies you want viewers to use HTTPS & TLS v1.1 to request your objects
    const viewerCertificate = cloudfront.ViewerCertificate.fromAcmCertificate(
      {
        certificateArn: certificateArn,
        env: {
          region: Aws.REGION,
          account: Aws.ACCOUNT_ID,
        },
        node: this.node,
        stack: parent,
        metricDaysToExpiry: () =>
          new cloudwatch.Metric({
            namespace: "TLS Viewer Certificate Validity",
            metricName: "TLS Viewer Certificate Expired",
          }),
      },
      {
        sslMethod: cloudfront.SSLMethod.SNI,
        securityPolicy: cloudfront.SecurityPolicyProtocol.TLS_V1_1_2016,
        aliases: [siteDomain],
      }
    );

    // CloudFront distribution
    const distribution = new cloudfront.CloudFrontWebDistribution(
      this,
      "SiteDistribution",
      {
        viewerCertificate,
        originConfigs: [
          {
            s3OriginSource: {
              s3BucketSource: this.websiteBucket,
              originAccessIdentity: cloudfrontOAI,
            },
            behaviors: [
              {
                isDefaultBehavior: true,
                compress: true,
                allowedMethods:
                  cloudfront.CloudFrontAllowedMethods.GET_HEAD_OPTIONS,
              },
            ],
          },
        ],
      }
    );

    // Route53 alias record for the CloudFront distribution
    new route53.ARecord(this, "SiteAliasRecord", {
      recordName: siteDomain,
      target: route53.RecordTarget.fromAlias(
        new targets.CloudFrontTarget(distribution)
      ),
      zone,
    });

    // deploy/copy the website built website to s3 bucket
    this.deploy = new s3Deployment.BucketDeployment(
      this,
      CONSTRUCT_NAMES.bucket.deployment,
      {
        sources: [
          s3Deployment.Source.asset(
            path.join(__dirname, "..", "..", "frontend", "out")
          ),
        ],
        destinationBucket: this.websiteBucket,
        // distribution: this.cloudFront,
        distribution,
        distributionPaths: ["/*"],
      }
    );
  }
}

您的网站感觉不像是 100%(客户端)静态网站。我的意思是每个 HTML 页面都是预先生成的,并且客户端的所有内容都是静态的。如果是这种情况,那么 /work/1 不应加载任何 html 页面,因为它不是 html 资源。要成为 HTML 资源,它应该像 /work/1.html

话虽这么说,但您似乎正在使用 React 或其他一些技术,这些技术可以在已知上一页时转换路由。 / -> /work/1

因为您的堆栈中已有 CloudFront。只需将错误页面设置为重定向回主页,然后它就可以正常工作。附加我在 S3+CloudFront 上托管的 React 应用程序的解决方案。

发现问题。 Apache、nginx 等 HTTP 服务器会自动为没有后缀的 URL 请求寻找 .html 后缀文件。 我们显然可以使用云端 lambda 来做同样的事情,这些 lambda 被称为 lambda@edge。

https://jarredkenny.com/cloudfront-pretty-urls/

请记住,lambda@edge 功能仅适用于 us-east-1,因此您必须部署两个堆栈,除非您将整个堆栈移至 us-east-1