SQL注入攻击深度解析与防护策略
使用 AWS Lambda 和 AWS API Gateway 的无服务器 API 深度指南(第 1 部分)
多年来,围绕 API 构建软件产品一直都是人们热衷的事情,而从一开始就使用无服务器技术似乎非常有趣,原因有很多——按需定价、自动扩展和更少的运营开销。
为什么
当我阅读人们谈论无服务器技术时,我感觉仍有许多问题尚未解决。
如何
- 使用查询字符串还是路由参数?
- 使用 POST 请求的参数或正文?
- 设置 HTTP 状态代码?
- 设置响应头?
- 从 Lambda 调用 Lambda?
- 存储第三方 API 的密钥?
因此我决定写一篇关于如何使用无服务器技术构建 API 的文章,特别是 AWS Lambda 和 API-Gateway。
本文分为两部分。
这是第一篇,关于架构、设置和身份验证。
第二个是实际工作,上传图像,使用第三方 API 标记图像,然后检索标记和图像。
什么
我们将构建一个简单的 RESTful 图像标记 API。它允许我们上传图像,使用名为Clarifai的第三方 API 自动标记图像,并将标签和图像名称存储到 Elasticsearch 中。
我想到的工作流程是:
- 注册并登录
- 上传图片
- 获取所有图片的所有标签
- 获取所有图像及其标签
- 通过标签获取图像
如何
对于 API,我们使用 API-Gateway,这是亚马逊的全方位无服务器 HTTP 解决方案。它针对 RESTful API 进行了优化,可作为我们系统的入口点。
Amazon Cognito负责处理身份验证。Cognito 是一种托管的无服务器身份验证、授权和数据同步解决方案。我们使用它来注册用户,这样我们就不必在这里重新设计轮子了。
我们的 API 的实际计算工作由AWS Lambda完成,这是一种功能即服务解决方案。Lambda 是一个无服务器的基于事件的系统,允许在发生某些事情时触发函数,例如,HTTP 请求命中我们的 API,或者有人将文件直接上传到 S3。
图像存储在Amazon S3存储桶中。S3 是一种无服务器的基于对象的存储解决方案。它允许通过 HTTP 直接访问和上传文件,并且可以作为API-Gateway成为Lambda的事件源。
标签数据和相应的图像名称存储在Amazon Elasticsearch Service中;这是 Elasticsearch 的 AWS 托管版本。遗憾的是,它不是无服务器的,而是基于 EC2 实例构建的,但前 12 个月有免费套餐。Elasticsearch 是一种非常灵活的文档存储,并带有强大的查询语言。
一项名为 Clarifai 的非 AWS 服务提供了图像识别功能。AWS 有自己的服务,称为 Rekognition,但通过使用 Clarifai,我们可以了解如何存储第三方 API 密钥。
我们构建的整个基础设施由AWS SAM (无服务器应用程序模型)管理。SAM是 AWS CloudFormation 的扩展,可减少设置 AWS Lambda 和 API 网关资源所需的一些样板代码。
我们使用AWS Cloud9作为 IDE,因为它预装了使用 AWS 资源所需的所有工具和权限。
先决条件
设置
让我们首先获取一个基本的无服务器 API,该 API 仅在 AWS SAM、API-Gateway 和 Cognito 的帮助下实现登录。
mkdir serverless-api
cd serverless-api
mkdir functions
touch template.yaml
首先,我们创建文件夹结构和一个template.yaml
文件,其中包含我们使用 SAM 创建的基础设施的定义。
实施 SAM 模板
我们的SAM模板内容如下:
AWSTemplateFormatVersion: "2010-09-09"
Transform: "AWS::Serverless-2016-10-31"
Description: "An example REST API build with serverless technology"
Globals:
Function:
Runtime: nodejs8.10
Handler: index.handler
Timeout: 30
Tags:
Application: Serverless API
Resources:
ServerlessApi:
Type: AWS::Serverless::Api
Properties:
StageName: Prod
Cors: "'*'"
Auth:
DefaultAuthorizer: CognitoAuthorizer
Authorizers:
CognitoAuthorizer:
UserPoolArn: !GetAtt UserPool.Arn
GatewayResponses:
UNAUTHORIZED:
StatusCode: 401
ResponseParameters:
Headers:
Access-Control-Expose-Headers: "'WWW-Authenticate'"
Access-Control-Allow-Origin: "'*'"
WWW-Authenticate: >-
'Bearer realm="admin"'
# ============================== Auth =============================
UserPool:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName: ApiUserPool
LambdaConfig:
PreSignUp: !GetAtt PreSignupFunction.Arn
Policies:
PasswordPolicy:
MinimumLength: 6
UserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
UserPoolId: !Ref UserPool
ClientName: ApiUserPoolClient
GenerateSecret: no
PreSignupFunction:
Type: AWS::Serverless::Function
Properties:
InlineCode: |
exports.handler = async event => {
event.response = { autoConfirmUser: true };
return event;
};
LambdaCognitoUserPoolExecutionPermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !GetAtt PreSignupFunction.Arn
Principal: cognito-idp.amazonaws.com
SourceArn: !Sub "arn:${AWS::Partition}:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/${UserPool}"
AuthFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/auth/
Environment:
Variables:
USER_POOL_ID: !Ref UserPool
USER_POOL_CLIENT_ID: !Ref UserPoolClient
Events:
Signup:
Type: Api
Properties:
RestApiId: !Ref ServerlessApi
Path: /signup
Method: POST
Auth:
Authorizer: NONE
Signin:
Type: Api
Properties:
RestApiId: !Ref ServerlessApi
Path: /signin
Method: POST
Auth:
Authorizer: NONE
Outputs:
ApiUrl:
Description: The target URL of the created API
Value: !Sub "https://${ServerlessApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/"
Export:
Name: ApiUrl
首先,我们定义一个 ServerlessApi 资源。在 SAM 模板中,通常会为我们隐式创建此资源,但是当我们想要启用 CORS 或使用授权器时,我们需要显式定义它。
我们还将所有路由的默认授权器设置为 CognitoAuthorizer,然后使用我们稍后在模板中定义的 Cognito 用户池立即对其进行配置。
然后我们定义一个UNAUTHORIZED
网关响应,因为 API-Gateway 不会自行将 CORS 标头添加到我们的响应中。在我们的 Lambda 支持的路由中,我们可以通过 JavaScript 代码执行此操作,但当我们没有调用它们的权限时,这无济于事。网关响应是一种直接使用 API-Gateway 添加标头的方法。
接下来我们定义Cognito相关的资源:
UserPool
是 Cognito 中保存我们用户账户的一部分。UserPoolClient
是 Cognito 的一部分,允许与用户池进行编程交互。PreSignupFunction
是一个 Lambda 函数,在实际注册用户池之前调用。它允许我们激活用户,而无需向他们发送电子邮件。它只有几行代码,所以我们将其内联。LambdaCognitoUserPoolExecutionPermission
授予管理的 Cognito 服务执行我们的权限PreSignupFunction
。
最后,我们定义一个由 API 网关路由调用的 Lambda 函数。具体来说,POST /signup
和POST /signin
。这AuthFunction
还会将用户池 ID 和用户池客户端 ID 作为环境变量。这些值是在部署时动态生成的,但环境变量是将这些值从 SAM/Cloudformation 传递到函数的一种方式。
通过Global/Function/Handler
在顶部设置和CodeUri
在AuthFunction
属性中设置,我们可以确定 JavaScript 文件的位置以及它如何为 Lambda 导出处理程序函数。
...
Handler: index.handler
...
CodeUri: functions/auth/
该文件必须被调用index.js
,它必须导出一个调用的函数handler
,并且它必须位于functions/auth/
目录中。
我们的端点定义的属性Auth
设置为Authorizer: NONE
API-Gateway 让我们无需令牌即可请求端点。
在文件末尾,我们有一个名为的输出ApiUrl
;我们在部署后使用它从 CloudFormation 获取实际的 API URL。
实施 Auth Lambda 函数
我们functions
在开始时就创建了目录,所以我们只需要添加一个auth
带有index.js
包含index.js
以下代码:
const users = require("./user-management");
exports.handler = async event => {
const body = JSON.parse(event.body);
if (event.path === "/signup") return signUp(body);
return signIn(body);
};
const signUp = async ({ username, password }) => {
try {
await users.signUp(username, password);
return createResponse({ message: "Created" }, 201);
} catch (e) {
console.log(e);
return createResponse({ message: e.message }, 400);
}
};
const signIn = async ({ username, password }) => {
try {
const token = await users.signIn(username, password);
return createResponse({ token }, 201);
} catch (e) {
console.log(e);
return createResponse({ message: e.message }, 400);
}
};
const createResponse = (
data = { message: "OK" },
statusCode = 200
) => ({
statusCode,
body: JSON.stringify(data),
headers: { "Access-Control-Allow-Origin": "*" }
});
它需要一个user-management.js
才能完成其工作,但我们稍后再讨论。首先,让我们看看这个文件的作用。
正如我们在中所说template.yaml
,它导出一个函数,当有人向或端点handler
发送 POST 请求时,该函数从 API-Gateway 接收 HTTP 请求事件。/signup
/signin
在这里我们可以看到对最常见问题之一的答案。
如何访问请求主体?
exports.handler = async event => {
const body = JSON.parse(event.body);
...
};
JavaScript Lambda 函数的第一个参数包含一个事件对象。当使用 API-Gateway 事件调用该函数时,此对象有一个body
属性,该属性包含请求正文的字符串(如果客户端发送了请求正文)。
username
在我们的例子中,我们期望一个带有and 的JSON,password
以便我们可以创建新的用户帐户或将用户登录到他们的帐户,因此我们首先需要JSON.parse()
正文。
接下来,我们有两个函数,signUp
它们signIn
使用所需的user-management
模块(此处调用该模块users
来完成工作)。它们都username
作为password
参数传递。
createResponse
是一个构建响应对象的实用函数。此对象是 Lambda 函数必须返回的内容。
说到这里,我们从一开始的其他问题就得到了答案。
如何设置 HTTP 状态代码?
exports.handler = async event => {
...
return { statusCode: 404 };
};
每个 Lambda 函数都需要返回一个响应对象。该对象必须至少有一个statusCode
属性。否则,API-Gateway 会认为请求失败。
如何设置 HTTP 响应头?
exports.handler = async event => {
...
return {
statusCode: 200,
headers: {
"Access-Control-Allow-Origin": "*"
}
};
};
我们的 Lambda 函数必须返回的响应对象也可以具有headers
属性,它必须是一个以标头名称作为对象键、以标头值作为对象值的对象。
这里我们可以看到,我们需要手动设置 CORS 标头。否则,浏览器将不会接受响应。
现在,让我们看看user-management.js
我们需要的文件。
global.fetch = require("node-fetch");
const Cognito = require("amazon-cognito-identity-js");
const userPool = new Cognito.CognitoUserPool({
UserPoolId: process.env.USER_POOL_ID,
ClientId: process.env.USER_POOL_CLIENT_ID
});
exports.signUp = (username, password) =>
new Promise((resolve, reject) =>
userPool.signUp(username, password, null, null, (error, result) =>
error ? reject(error) : resolve(result)
)
);
exports.signIn = (username, password) =>
new Promise((resolve, reject) => {
const authenticationDetails = new Cognito.AuthenticationDetails({
Username: username,
Password: password
});
const cognitoUser = new Cognito.CognitoUser({
Username: username,
Pool: userPool
});
cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: result => resolve(result.getIdToken().getJwtToken()),
onFailure: reject
});
});
首先,我们使用该node-fetch
软件包来填充fetch
浏览器 API。我们接下来需要的软件包需要此填充amazon-cognito-identity-js
,它为我们完成了与 Cognito 服务通信的繁重工作。
CognitoUserPool
我们借助从 SAM 模板获得的环境变量创建一个对象,并在我们导出的signIn
函数内部使用该对象signUp
。
最令人兴奋的部分是从signIn
Cognito 用户池中获取 ID 令牌并返回它的函数。
此令牌需要在每次向我们的 API 发出请求时Authorization
都以前缀的形式传递到请求标头中Bearer
,并且需要在过期时重新获取。我们的 SAM 模板的 API 配置中的 CognitoAuthorizer 告诉 API-Gateway 如何使用 Cognito 处理其他所有事情。
现在我们需要安装我们使用的软件包。node-fetch
polyfill 包和amazon-cognito-identity-js
包。
为此,我们需要进入functions/auth
目录并初始化一个 NPM 项目,然后安装软件包。
npm init -y
npm i node-fetch amazon-cognito-identity-js
functions/auth
当我们将该函数与其余 API 一起部署到 Lambda 时,目录内的所有文件都将上传到 S3。
部署 API
为了将我们的项目部署到 AWS,我们使用sam
CLI 工具。
首先,我们需要打包 Lambda 函数源并将其上传到 S3 部署存储桶。我们可以使用aws
CLI 创建此存储桶。
aws s3 mb s3://<DEPLOYMENT_BUCKET_NAME>
存储桶名称必须是全球唯一的,因此我们需要发明一个。
当创建成功后,我们需要DEPLOYMENT_BUCKET_NAME
我们package
的 Lambda 源。
sam package --template-file template.yaml \
--s3-bucket <DEPLOYMENT_BUCKET_NAME> \
--output-template-file packaged.yaml
此命令创建一个packaged.yaml
文件,其中包含指向 S3 上打包的 Lambda 源的 URL。
接下来,我们需要使用 CloudFormation 进行实际部署。
sam deploy --template-file packaged.yaml \
--stack-name serverless-api \
--capabilities CAPABILITY_IAM
如果一切顺利,我们使用以下命令来获取 API 的基本 URL。
aws cloudformation describe-stacks \
--stack-name serverless-api \
--query 'Stacks[0].Outputs[?OutputKey==`ApiUrl`].OutputValue' \
--output text
该 URL 应如下所示:
https://<API_ID>.execute-api.<REGION>.amazonaws.com/Prod/
现在可以使用此 URL 向我们创建的/signup
和/signin
端点发出 POST 请求。
结论
本文是关于在 AWS 上创建无服务器 API 的两篇文章中的第一篇。我们讨论了这样做的动机、完成任务所需的 AWS 服务,并在 AWS Cognito 的帮助下实现了基于令牌的身份验证。
我们还回答了使用 API-Gateway 和 Lambda 构建 API 时出现的一些最紧迫的问题。
在下一部分中,我们将实现两个允许我们使用 API 的 Lambda 函数。
ImagesFunction
负责为 S3 存储桶创建上传链接。TagsFunction
处理 S3 上传、第三方 API 集成以及创建标签的列表。
文章来源:In Depth Guide to Serverless APIs with AWS Lambda and AWS API Gateway (Part 1)