

# 限制对 AWS Lambda 函数 URL 源的访问
<a name="private-content-restricting-access-to-lambda"></a>

CloudFront 提供*源访问控制*（OAC），以限制对 Lambda 函数 URL 源的访问。

**Topics**
+ [创建新 OAC](#create-oac-overview-lambda)
+ [源访问控制的高级设置](#oac-advanced-settings-lambda)
+ [示例模板代码](#example-template-code-lambda-oac)

## 创建新 OAC
<a name="create-oac-overview-lambda"></a>

完成以下主题中描述的步骤，在 CloudFront 中设置新的 OAC。

**重要**  
如果您在 Lambda 函数 URL 中使用 `PUT` 或 `POST` 方法，则您的用户在向 CloudFront 发送请求时，必须计算正文的 SHA256，并在 `x-amz-content-sha256` 标头中包含请求正文的有效载荷哈希值。Lambda 不支持未签名的有效负载。

**Topics**
+ [先决条件](#oac-prerequisites-lambda)
+ [向 CloudFront 授予访问 Lambda 函数 URL 的权限](#oac-permission-to-access-lambda)
+ [创建 OAC](#create-oac-lambda)

### 先决条件
<a name="oac-prerequisites-lambda"></a>

在创建和设置 OAC 之前，您必须拥有将 Lambda 函数 URL 作为源的 CloudFront 分配。要使用 OAC，您必须将 `AWS_IAM` 指定作为 `AuthType` 参数的值。有关更多信息，请参阅 [使用 Lambda 函数 URL](DownloadDistS3AndCustomOrigins.md#concept_lambda_function_url)。

### 向 CloudFront 授予访问 Lambda 函数 URL 的权限
<a name="oac-permission-to-access-lambda"></a>

在 CloudFront 分配中创建 OAC 或对其设置之前，请确保 CloudFront 具有访问 Lambda 函数 URL 的权限。请在创建 CloudFront 分配后，但在分配配置中将 OAC 添加到 Lambda 函数 URL 之前，执行此操作。

**注意**  
要更新 Lambda 函数 URL 的 IAM 策略，您必须使用 AWS Command Line Interface（AWS CLI）。目前不支持在 Lambda 控制台中编辑 IAM 策略。

以下 AWS CLI 命令授予 CloudFront 服务主体（`cloudfront.amazonaws.com`）访问您的 Lambda 函数 URL 的权限。使用策略中的 `Condition` 元素，*仅* 在该请求代表包含 Lambda 函数 URL 的 CloudFront 分配时，才允许 CloudFront 访问 Lambda。这是您要向其添加 OAC 的具有 Lambda 函数 URL 源的分配。

**Example ：更新策略以支持启用了 OAC 的 CloudFront 分配进行只读访问的 AWS CLI 命令**  
以下 AWS CLI 命令允许 CloudFront 分配（`E1PDK09ESKHJWT`）访问您的 Lambda *`FUNCTION_URL_NAME`*。

```
aws lambda add-permission \
--statement-id "AllowCloudFrontServicePrincipal" \
--action "lambda:InvokeFunctionUrl" \
--principal "cloudfront.amazonaws.com" \
--source-arn "arn:aws:cloudfront::123456789012:distribution/E1PDK09ESKHJWT" \
--function-name FUNCTION_URL_NAME
```

```
aws lambda add-permission \
--statement-id "AllowCloudFrontServicePrincipalInvokeFunction" \
--action "lambda:InvokeFunction" \
--principal "cloudfront.amazonaws.com" \
--source-arn "arn:aws:cloudfront::123456789012:distribution/E1PDK09ESKHJWT" \
--function-name FUNCTION_URL_NAME
```

**注意**  
如果您创建了一个分配，但它没有访问您的 Lambda 函数 URL 的权限，则可以从 CloudFront 控制台中选择**复制 CLI 命令**，然后从命令行终端输入此命令。有关更多信息，请参阅《AWS Lambda 开发人员指南》**中的[向 AWS 服务 授予函数访问权限](https://docs.aws.amazon.com/lambda/latest/dg/access-control-resource-based.html#permissions-resource-serviceinvoke)。

### 创建 OAC
<a name="create-oac-lambda"></a>

要创建 OAC，可以使用 AWS 管理控制台、CloudFormation、AWS CLI 或 CloudFront API。

------
#### [ Console ]

**创建 OAC**

1. 登录 AWS 管理控制台，并通过以下网址打开 CloudFront 控制台：[https://console.aws.amazon.com/cloudfront/v4/home](https://console.aws.amazon.com/cloudfront/v4/home)。

1. 在导航窗格中，选择 **Origin access**（源访问）。

1. 选择 **Create control setting**（创建控制设置）。

1. 在**创建新 OAC** 页面上，执行以下操作：

   1. 输入 OAC 的**名称**和（可选）**描述**。

   1. 在**签名行为**中，建议保留默认设置（**签署请求（推荐）**）。有关更多信息，请参阅 [源访问控制的高级设置](#oac-advanced-settings-lambda)。

1. 对于**源类型**，请选择 **Lambda**。

1. 选择**创建**。
**提示**  
创建 OAC 后，记下**名称**。在以下过程中，您需要此名称。

**向分配中的 Lambda 函数 URL 添加源访问控制**

1. 通过以下网址打开 CloudFront 控制台：[https://console.aws.amazon.com/cloudfront/v4/home](https://console.aws.amazon.com/cloudfront/v4/home)。

1. 选择具有要向其中添加 OAC 的 Lambda 函数 URL 的分配，然后选择**源**选项卡。

1. 选择要向其中添加 OAC 的 Lambda 函数 URL，然后选择**编辑**。

1. 对于源的**协议**，请选择**仅 HTTPS**。

1. 从**源访问控制**下拉菜单中，选择要使用的 OAC。

1. 选择**保存更改**。

分配开始部署到所有 CloudFront 边缘站点。当边缘站点收到新配置时，它会签署自己发送到 Lambda 函数 URL 的所有请求。

------
#### [ CloudFormation ]

要使用 CloudFormation 创建 OAC，请使用 `AWS::CloudFront::OriginAccessControl` 资源类型。以下示例演示了 YAML 格式的 CloudFormation 模板语法，用于创建 OAC。

```
Type: AWS::CloudFront::OriginAccessControl
Properties: 
  OriginAccessControlConfig: 
      Description: An optional description for the origin access control
      Name: ExampleOAC
      OriginAccessControlOriginType: lambda
      SigningBehavior: always
      SigningProtocol: sigv4
```

有关更多信息，请参阅 *AWS CloudFormation 用户指南* 中的 [AWS::CloudFront::OriginAccessControl](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudfront-originaccesscontrol.html)。

------
#### [ CLI ]

要使用 AWS Command Line Interface (AWS CLI) 创建源访问控制，请使用 **aws cloudfront create-origin-access-control** 命令。您可以使用输入文件来提供命令的输入参数，而不是将每个单独的参数指定为命令行输入。

**创建源访问控制（带输入文件的 CLI）**

1. 使用以下命令创建名为 `origin-access-control.yaml` 的文件。此文件包含 **create-origin-access-control** 命令的所有输入参数。

   ```
   aws cloudfront create-origin-access-control --generate-cli-skeleton yaml-input > origin-access-control.yaml
   ```

1. 打开刚创建的 `origin-access-control.yaml` 文件。编辑文件以添加 OAC 的名称、描述（可选），然后将 `SigningBehavior` 更改为 `always`。然后保存文件。

   有关其他 OAC 设置的信息，请参阅[源访问控制的高级设置](#oac-advanced-settings-lambda)。

1. 使用以下命令通过 `origin-access-control.yaml` 文件中的输入参数创建源访问控制。

   ```
   aws cloudfront create-origin-access-control --cli-input-yaml file://origin-access-control.yaml
   ```

   记下命令输出中的 `Id` 值。您需要使用它将 OAC 添加到 CloudFront 分配中的 Lambda 函数 URL。

**将 OAC 附加到现有分配中的 Lambda 函数 URL（带输入文件的 CLI）**

1. 使用以下命令保存要向其添加 OAC 的 CloudFront 分配的分配配置。该分配必须将 Lambda 函数 URL 作为源。

   ```
   aws cloudfront get-distribution-config --id <CloudFront distribution ID> --output yaml > dist-config.yaml
   ```

1. 打开刚创建的名为 `dist-config.yaml` 的文件。编辑文件，进行以下更改：
   + 在 `Origins` 对象中，将 OAC 的 ID 添加到名为 `OriginAccessControlId` 的字段中。
   + 从名为 `OriginAccessIdentity` 的字段中移除值（如果存在）。
   + 将 `ETag` 字段重命名为 `IfMatch`，但不更改字段的值。

   完成后保存该文件。

1. 使用以下命令更新分配以使用源访问控制。

   ```
   aws cloudfront update-distribution --id <CloudFront distribution ID> --cli-input-yaml file://dist-config.yaml
   ```

分配开始部署到所有 CloudFront 边缘站点。当边缘站点收到新配置时，它会签署自己发送到 Lambda 函数 URL 的所有请求。

------
#### [ API ]

要使用 CloudFront API 创建 OAC，请使用 [CreateOriginAccessControl](https://docs.aws.amazon.com/cloudfront/latest/APIReference/API_CreateOriginAccessControl.html)。有关您在此 API 调用中指定的字段的更多信息，请参阅有关 AWS SDK 或其他 API 客户端的 API 参考文档。

创建 OAC 后，可以使用下面的任何一个 API 调用将其附加到分配中的 Lambda 函数 URL：
+ 要将它附加到现有分配，请使用 [UpdateDistribution](https://docs.aws.amazon.com/cloudfront/latest/APIReference/API_UpdateDistribution.html)。
+ 要将它附加到新分配，请使用 [CreateDistribution](https://docs.aws.amazon.com/cloudfront/latest/APIReference/API_CreateDistribution.html)。

对于这两个 API 调用，请在源内的 `OriginAccessControlId` 字段中提供 OAC ID。有关您在这些 API 调用中指定的其他字段的更多信息，请参阅有关 AWS SDK 或其他 API 客户端的 API 参考文档。

------

## 源访问控制的高级设置
<a name="oac-advanced-settings-lambda"></a>

CloudFront OAC 功能包括仅适用于特定使用案例的高级设置。除非您特别需要高级设置，否则请使用推荐的设置。

OAC 包含一个名为**签名行为**（在控制台中）或 `SigningBehavior`（在 API、CLI 和 CloudFormation 中）的设置。此设置提供以下选项：

**始终签署源请求（推荐设置）**  
我们建议使用此设置，此设置在控制台中名为**签署请求（推荐）**，在 API、CLI 和 CloudFormation 中名为 `always`。使用此设置，CloudFront 将始终签署自己发送到 Lambda 函数 URL 的所有请求。

**切勿签署源请求**  
此设置在控制台中名为 **Do not sign requests**（请勿签署请求），在 API、CLI 和 CloudFormation 中名为 `never`。使用此设置关闭所有使用此 OAC 的分配中所有源的 OAC。与从所有使用 OAC 的源和分配中逐一删除 OAC 相比，这可以节省时间和精力。使用此设置，CloudFront 不会签署自己发送到 Lambda 函数 URL 的任何请求。  
要使用此设置，Lambda 函数 URL 必须可供公开访问。如果您将此设置用于不可公开访问的 Lambda 函数 URL，则 CloudFront 无法访问该源。Lambda 函数 URL 源向 CloudFront 返回错误，而 CloudFront 会将这些错误传递给查看器。有关更多信息，请参阅**《AWS Lambda 用户指南》中的 [Lambda 函数 URL 的安全性和身份验证模型](https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html)。

**不要改写查看器（客户端）`Authorization` 标头**  
此设置在控制台中名为 **Do not override authorization header**（请勿改写授权标头），在 API、CLI 和 CloudFormation 中名为 `no-override`。如果您希望 CloudFront 仅在相应的查看器请求不包含 `Authorization` 标头时签署源请求，请使用此设置。使用此设置，CloudFront 会在查看器请求中存在 `Authorization` 标头时传递该标头，但在查看器请求不包含 `Authorization` 标头时对源请求进行签名（添加自己的 `Authorization` 标头）。  
+ 如果您使用此设置，则必须为 Lambda 函数 URL 指定签名版本 4 签名，而不是您的 CloudFront 分配的名称或 CNAME。当 CloudFront 将查看器请求中的 `Authorization` 标头转发到 Lambda 函数 URL 时，Lambda 会针对 Lambda URL 域的主机验证签名。如果签名不是基于 Lambda URL 域，签名中的主机将与 Lambda URL 源使用的主机不匹配。这意味着请求将失败，从而导致签名验证错误。
+ 要传递查看器请求的 `Authorization` 标头，您*必须* 针对所有使用与此源访问控制关联的 Lambda 函数 URL 的缓存行为，将 `Authorization` 标头添加到[缓存策略](controlling-the-cache-key.md)中。

## 示例模板代码
<a name="example-template-code-lambda-oac"></a>

如果您的 CloudFront 源是与 OAC 关联的 Lambda 函数 URL，则可以使用以下 Python 脚本，通过 `POST` 方法将文件上传到 Lambda 函数。

此代码假设您对 OAC 进行了配置，将默认签名行为设置为**始终签署源请求**，并且没有选择**请勿覆盖授权标头**设置。

通过此配置，OAC 可以使用 Lambda 主机名正确管理 Lambda 的 SigV4 授权。有效载荷使用来自 IAM 身份的 SigV4 进行签名，该身份已获得 Lambda 函数 URL 的授权，指定为 `IAM_AUTH` 类型。

模板针对来自客户端的 `POST` 请求，演示如何处理 x-amz-content-sha256 标头中已签名的有效载荷哈希值。具体而言，此模板设计用于管理表单数据有效载荷。该模板可以实现通过 CloudFront 将文件安全上传到 Lambda 函数 URL，并使用 AWS 身份验证机制确保只有经过授权的请求才能访问 Lambda 函数。

**代码包括以下功能：**  
满足在 x-amz-content-sha256 标头中包含有效载荷哈希的要求
使用 SigV4 身份验证安全地访问 AWS 服务
支持使用分段表单数据来上传文件
包括请求异常的错误处理

```
import boto3
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest
import requests
import hashlib
import os


def calculate_body_hash(body):
    return hashlib.sha256(body).hexdigest()


def sign_request(request, credentials, region, service):
    sigv4 = SigV4Auth(credentials, service, region)
    sigv4.add_auth(request)


def upload_file_to_lambda(cloudfront_url, file_path, region):
    # AWS credentials
    session = boto3.Session()
    credentials = session.get_credentials()

    # Prepare the multipart form-data
    boundary = "------------------------boundary"

    # Read file content
    with open(file_path, 'rb') as file:
        file_content = file.read()

    # Get the filename from the path
    filename = os.path.basename(file_path)

    # Prepare the multipart body
    body = (
        f'--{boundary}\r\n'
        f'Content-Disposition: form-data; name="file"; filename="{filename}"\r\n'
        f'Content-Type: application/octet-stream\r\n\r\n'
    ).encode('utf-8')
    body += file_content
    body += f'\r\n--{boundary}--\r\n'.encode('utf-8')

    # Calculate SHA256 hash of the entire body
    body_hash = calculate_body_hash(body)

    # Prepare headers
    headers = {
        'Content-Type': f'multipart/form-data; boundary={boundary}',
        'x-amz-content-sha256': body_hash
    }

    # Create the request
    request = AWSRequest(
        method='POST',
        url=cloudfront_url,
        data=body,
        headers=headers
    )

    # Sign the request
    sign_request(request, credentials, region, 'lambda')

    # Get the signed headers
    signed_headers = dict(request.headers)

    # Print request headers before sending
    print("Request Headers:")
    for header, value in signed_headers.items():
        print(f"{header}: {value}")

    try:
        # Send POST request with signed headers
        response = requests.post(
            cloudfront_url,
            data=body,
            headers=signed_headers
        )

        # Print response status and content
        print(f"\nStatus code: {response.status_code}")
        print("Response:", response.text)

        # Print response headers
        print("\nResponse Headers:")
        for header, value in response.headers.items():
            print(f"{header}: {value}")

    except requests.exceptions.RequestException as e:
        print(f"An error occurred: {e}")


# Usage
cloudfront_url = "https://d111111abcdef8.cloudfront.net"
file_path = r"filepath"
region = "us-east-1"  # example: "us-west-2"

upload_file_to_lambda(cloudfront_url, file_path, region)
```