【问题标题】:Invoking AWS Lambda via Unsigned POST to REST API通过未签名的 POST 到 REST API 调用 AWS Lambda
【发布时间】:2021-01-01 15:47:56
【问题描述】:

我想在我的 Jekyll 网站上有一个表单供访问者填写,form action 应该 POST 到 AWS Lambda 函数。网站上不允许使用 JavaScript,因此POST 不得要求签名。

我想要尽可能简单的设置,并且不需要高安全性。如果有办法避免使用 AWS API Gateway 创建 HTTP API,并以某种方式让 Lambda 函数直接从用户的 Web 浏览器接收POST,那将是完美的。如果需要 API 网关,那么最简单的解决方案将是最好的。

我想专门使用命令行命令(不是 Web 浏览器)来使用 AWS API。这允许使用脚本解决方案。

我在这个问题上花了一些时间,这就是我所拥有的。我在deploy 脚本中用TODO 标记了问题。该脚本中有一些可能不需要的额外代码。问题是,我不确定要删除什么,因为我不知道如何将POST 提供给 lambda。

脚本使用jqyq,因此 bash 脚本可以分别解析 JSON 和 YAML。

_config.yml

aws:
  cloudfront:
    distributionId: "" # Provide value if CloudFront is used on this site
  lambda:
    addSubscriber:
      custom: # TODO change these values to suit your website
        iamRoleName: lambda-ex
        name: addSubscriberAwsLambdaSample
        handler: addSubscriberAwsLambda.lambda_handler
        runtime: python3.8
      computed: # These values are computed by the _bin/awsLambda setup and deploy scripts
        arn: arn:aws:lambda:us-east-1:031372724784:function:addSubscriberAwsLambdaSample:3
        iamRoleArn: arn:aws:iam::031372724784:role/lambda-ex

utils 源 bash 脚本

#!/bin/bash

function readYaml {
  # $1 - path
  yq r _config.yml "$1"
}

function writeYaml {
  # $1 - path
  # $2 - value
  yq w -i _config.yml "$1" "$2"
}


# AWS Lambda values
export LAMBDA_IAM_ROLE_ARN="$(  readYaml aws.lambda.addSubscriber.computed.iamRoleArn )"
export LAMBDA_NAME="$(          readYaml aws.lambda.addSubscriber.custom.name         )"
export LAMBDA_RUNTIME="$(       readYaml aws.lambda.addSubscriber.custom.runtime      )"
export LAMBDA_HANDLER="$(       readYaml aws.lambda.addSubscriber.custom.handler      )"
export LAMBDA_IAM_ROLE_NAME="$( readYaml aws.lambda.addSubscriber.custom.iamRoleName  )"

export PACKAGE_DIR="${GIT_ROOT}/_package"
export LAMBDA_ZIP="${PACKAGE_DIR}/function.zip"

# Misc values
export TITLE="$( readYaml title )"
export URL="$( readYaml url )"
export DOMAIN="$( echo "$URL" | sed -n -e 's,^https\?://,,p' )"

设置 bash 脚本

#!/bin/bash

# Inspired by https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-awscli.html

SOURCE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"

GIT_ROOT="$( git rev-parse --show-toplevel )"
cd "${GIT_ROOT}"
source _bin/utils

# Define the execution role that gives an AWS Lambda function permission to access AWS resources.
read -r -d '' ROLE_POLICY_JSON <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF

# If a role named $LAMBDA_IAM_ROLE_NAME is already defined then use it
ROLE_RESULT="$( aws iam get-role --role-name "$LAMBDA_IAM_ROLE_NAME" 2> /dev/null )"
if [ $? -ne 0 ]; then
  ROLE_RESULT="$( aws iam create-role \
    --role-name "$LAMBDA_IAM_ROLE_NAME" \
    --assume-role-policy-document "$ROLE_POLICY_JSON"
  )"
fi
LAMBDA_IAM_ROLE_ARN="$( jq -r .Role.Arn <<< "$ROLE_RESULT" )"
writeYaml aws.lambda.addSubscriber.computed.iamRoleArn "$LAMBDA_IAM_ROLE_ARN"

部署 bash 脚本

# Call this script after the setup script has created the IAM role
# that gives the addSubscriber AWS Lambda function permission to access AWS resources
#
# 1) This script builds the AWS Lambda package and deploys it, with permissions.
#    Any previous version of the AWS Lambda is deleted.
#
# 2) The newly (re)created AWS Lambda ARN is stored in _config.yml
#
# 3) An AWS Gateway HTTP API is created so static web pages can POST subscriber information to the AWS Lambda function.
#    Because the web page is not allowed to have JavaScript, the POST is unsigned.
#    *** The API must allow for an unsigned POST!!! ***

# Set cwd to the git project root
GIT_ROOT="$( git rev-parse --show-toplevel )"
cd "${GIT_ROOT}"

# Load configuration environment variables from _bin/utils:
# DOMAIN, LAMBDA_IAM_ROLE_ARN, LAMBDA_IAM_ROLE_NAME, LAMBDA_HANDLER, LAMBDA_NAME, LAMBDA_RUNTIME, LAMBDA_ZIP, PACKAGE_DIR, and URL
source _bin/utils

# Directory that this script resides in
SOURCE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"

echo "Building the AWS Lambda and packaging it into a zip file"
"$SOURCE_DIR/package" "$PACKAGE_DIR" > /dev/null

# Check to see if the Lambda function already exists.
LAMBDA="$( aws lambda list-functions | jq ".Functions[] | select(.FunctionName | contains(\"$LAMBDA_NAME\"))" )"

if [ -z "$LAMBDA" ]; then
  echo "The AWS Lambda function '$LAMBDA_NAME' does not exist yet, so create it"
  LAMBDA_METADATA="$( aws lambda create-function \
    --description "Add subscriber to the MailChimp list with ID '$MC_LIST_ID_MSLINN' for the '$DOMAIN' website" \
    --environment "{
      \"Variables\": {
        \"MC_API_KEY_MSLINN\": \"$MC_API_KEY_MSLINN\",
        \"MC_LIST_ID_MSLINN\": \"$MC_LIST_ID_MSLINN\",
        \"MC_USER_NAME_MSLINN\": \"$MC_USER_NAME_MSLINN\"
      }
    }" \
    --function-name "$LAMBDA_NAME" \
    --handler "$LAMBDA_HANDLER" \
    --role "arn:aws:iam::${AWS_ACCOUNT_ID}:role/$LAMBDA_IAM_ROLE_NAME" \
    --runtime "$LAMBDA_RUNTIME" \
    --zip-file "fileb://$LAMBDA_ZIP" \
    | jq -S .
  )"
  LAMBDA_ARN="$( jq -r .Configuration.FunctionArn <<< "$LAMBDA_METADATA" )"
else
  echo "The AWS Lambda function '$LAMBDA_NAME' already exists, so update it"
  LAMBDA_METADATA="$( aws lambda update-function-code \
    --function-name "$LAMBDA_NAME" \
    --publish \
    --zip-file "fileb://$LAMBDA_ZIP" \
    | jq -S .
  )"
  LAMBDA_ARN="$( jq -r .FunctionArn <<< "$LAMBDA_METADATA" )"
fi
echo "AWS Lambda ARN is $LAMBDA_ARN"
writeYaml aws.lambda.addSubscriber.computed.arn "$LAMBDA_ARN"

echo "Attach the AWSLambdaBasicExecutionRole managed policy to $LAMBDA_IAM_ROLE_NAME."
aws iam attach-role-policy \
  --role-name $LAMBDA_IAM_ROLE_NAME \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole


#### Integrate with API Gateway for REST
#### Some or all of the following code is probably not required

GATEWAY_NAME="addSubscriberTo_$MC_LIST_ID_MSLINN"

API_GATEWAYS="$( aws apigateway get-rest-apis )"
if [ "$( jq ".items[] | select(.name | contains(\"$GATEWAY_NAME\"))" <<< "$API_GATEWAYS" )" ]; then
  echo "API gateway '$GATEWAY_NAME' already exists."
else
  echo "Creating API gateway '$GATEWAY_NAME'."

  API_JSON="$( aws apigateway create-rest-api \
    --name "$GATEWAY_NAME" \
    --description "API for adding a subscriber to the Mailchimp list with ID '$MC_LIST_ID_MSLINN' for the '$DOMAIN' website"
  )"
  REST_API_ID="$( jq -r .id <<< "$API_JSON" )"

  API_RESOURCES="$( aws apigateway get-resources --rest-api-id $REST_API_ID )"
  ROOT_RESOURCE_ID="$( jq -r .items[0].id <<< "$API_RESOURCES" )"

  NEW_RESOURCE="$( aws apigateway create-resource \
    --rest-api-id "$REST_API_ID" \
    --parent-id "$RESOURCE_ID" \
    --path-part "{proxy+}"
  )"
  NEW_RESOURCE_ID=$( jq -r .id <<< $NEW_RESOURCE )

if false; then
  # Is this step useful for any reason?
  aws apigateway put-method \
    --authorization-type "NONE" \
    --http-method ANY \
    --resource-id "$NEW_RESOURCE_ID" \
    --rest-api-id "$REST_API_ID"
fi

# The following came from https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#set-up-lambda-proxy-integration-using-cli
#   Instead of supplying an IAM role for --credentials, call the add-permission command to add resource-based permissions.
#   I need an example of this.
# Alternatively, how to obtain IAM_ROLE_ID? Again, I need an example.
  aws apigateway put-integration \
    --credentials "arn:aws:iam::${IAM_ROLE_ID}:role/apigAwsProxyRole" \
    --http-method ANY \
    --integration-http-method POST \
    --rest-api-id "$REST_API_ID" \
    --resource-id "$NEW_RESOURCE_ID" \
    --type AWS_PROXY \
    --uri arn:aws:apigateway:`aws configure get region`:lambda:path/2015-03-31/functions/$LAMBDA_ARN

  if [ "$LAMBDA_TEST"]; then
    # Deploy the API to a test stage
    aws apigateway create-deployment \
      --rest-api-id "$REST_API_ID" \
      --stage-name test
  else
    # Deploy the API live
    aws apigateway create-deployment \
      --rest-api-id "$REST_API_ID" \
      --stage-name TODO_WhatNameGoesHere
  fi
fi

echo "Check out the defined lambdas at https://console.aws.amazon.com/lambda/home?region=us-east-1#/functions"

【问题讨论】:

  • 你不能直接从没有 JavaScript 的浏览器访问 lambda。您需要 api 网关。只是不要在上面设置任何安全性。这很容易。
  • 我不同意“简单”,但如果你想要一个 lambda 函数来处理来自普通 HTML 表单的匿名 POST 请求的提交,那么 api gateway 是正确的工具。
  • @bryan60,我很想知道如何为简单的解决方案做些什么。如果你能提供一个简单的答案,请这样做。我可能已经在deploy 脚本的下半部分编写了大部分 API 网关代码,尽管它肯定需要更正。
  • 只需使用 aws sam cli。设置脚本需要 15 分钟部署网关和 lambda
  • @bryan60 是的,是的,我知道。请具体。我一直在努力尝试得到一个正确的答案。

标签: amazon-web-services rest aws-lambda aws-api-gateway


【解决方案1】:

Bash 脚本基础架构很糟糕。您最终可能会做对,但有一些工具可以让这个过程变得无比简单。

我更喜欢 Terraform,下面是 API Gateway + lambda 的样子:

provider "aws" {
}

# lambda

resource "random_id" "id" {
  byte_length = 8
}

data "archive_file" "lambda_zip" {
  type        = "zip"
  output_path = "/tmp/lambda.zip"
  source {
    content  = <<EOF
module.exports.handler = async (event, context) => {
// write the lambda code here
    }
};
EOF
    filename = "main.js"
  }
}

resource "aws_lambda_function" "lambda" {
  function_name = "${random_id.id.hex}-function"

  filename         = data.archive_file.lambda_zip.output_path
  source_code_hash = data.archive_file.lambda_zip.output_base64sha256

  handler = "main.handler"
  runtime = "nodejs12.x"
  role    = aws_iam_role.lambda_exec.arn
}

data "aws_iam_policy_document" "lambda_exec_role_policy" {
  statement {
    actions = [
      "logs:CreateLogStream",
      "logs:PutLogEvents"
    ]
    resources = [
      "arn:aws:logs:*:*:*"
    ]
  }
}

resource "aws_cloudwatch_log_group" "loggroup" {
  name              = "/aws/lambda/${aws_lambda_function.lambda.function_name}"
  retention_in_days = 14
}

resource "aws_iam_role_policy" "lambda_exec_role" {
  role   = aws_iam_role.lambda_exec.id
  policy = data.aws_iam_policy_document.lambda_exec_role_policy.json
}

resource "aws_iam_role" "lambda_exec" {
  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Effect": "Allow"
    }
  ]
}
EOF
}

# api gw

resource "aws_apigatewayv2_api" "api" {
  name          = "api-${random_id.id.hex}"
  protocol_type = "HTTP"
  target        = aws_lambda_function.lambda.arn
}

resource "aws_lambda_permission" "apigw" {
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.lambda.arn
  principal     = "apigateway.amazonaws.com"

  source_arn = "${aws_apigatewayv2_api.api.execution_arn}/*/*"
}

output "domain" {
  value = aws_apigatewayv2_api.api.api_endpoint
}

看到最后2个资源是API网关,前面都是给Lambda函数的。

【讨论】:

  • 感谢您提供如此详细的答案。但是,我实际上正在寻找一个 bash 解决方案。似乎最后两个资源包含答案。如果您显示的 API GateWay 代码由一系列 aws cli 调用描述,那么我可以通过使用 CURL POST 到端点进行测试。
猜你喜欢
  • 2017-06-01
  • 2022-10-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-10-03
  • 2020-10-01
  • 2015-04-04
相关资源
最近更新 更多