如何使用AWS Cognito访问私有S3对象

场景
假设您正在为客户开发一些应用程序。但是,应用程序中与记录相关的一些文件(如PDF、Word、Excel等)需要被访问。为了简化场景,我们假设这些文件存储在AWS的单个**私有(private)**S3存储桶(bucket)中。
用户需要能够通过应用程序中的URL链接从私有S3存储桶访问这些相关文件。我们的解决方案需要作为公司内任何软件的**可移植(portable)**解决方案。
简介
本文的目的是展示如何使用Cognito用户池(user pools)从私有S3存储桶下载文件。除了Cognito之外,还展示了从Cognito到带有授权器(Authorizer)的API Gateway的流程,以及API Gateway与Lambda的协作。
文章中尽可能多地分享了AWS控制台每个步骤的截图。特别是为了让初学者更清楚地了解这些步骤,添加了许多视觉效果。
背景知识
为了更好地理解本文中开发的内容,一些预备阅读可能会有所帮助。特别是对于AWS初学者,以下链接将非常有用:
需要做什么?
对于这样的任务,可以编写多种流程或方法。在这里,我们将实现下图所示的方法。下图提供了如何实现该场景的简要说明。
下图显示我们需要创建一些元素,如Cognito用户池、S3存储桶、API Gateway方法、Lambda函数等。在AWS环境中创建所有资产后,我们需要正确配置它们以便它们能够协同工作。

按照相反的顺序创建AWS环境中的所有元素会更好。例如,要将Lambda与API方法一起使用,如果先开发Lambda函数,则在创建API Gateway方法时可以轻松连接此函数。同样,在第5步中我们应该创建S3 Web存储桶并将
callback.html文件放入其中,以便在第6步创建Cognito用户池时可以使用此文件。当然这不是强制性的,但这个顺序将使开发更容易。因此在这里选择了这种方法。大纲
我们将寻找以下问题的答案。请注意,您需要有一个AWS账户才能完成本文中的所有步骤。
- 如何创建私有S3存储桶?
- 如何创建访问私有S3存储桶中对象的自定义策略?
- 如何创建访问私有S3存储桶中对象的Lambda函数?
- 如何创建使用Lambda函数的Gateway API?
- 如何创建用作Web文件夹的公共S3存储桶?
- 如何创建和配置Cognito用户池?
- 如何测试场景?
1. 如何创建私有S3存储桶?
S3是AWS中基于区域(region-based)的服务之一。S3存储桶中的项目称为对象(object)。因此,在AWS中,S3存储桶的对象和文件这两个术语可以互换使用。
保持"阻止所有公共访问"(Block All Public Access)复选框选中。这里创建了一个私有S3存储桶。虽然有许多额外的配置选项,但为了解决方案的简单性,我们使用默认值创建。

上传一些对象到S3存储桶以测试私有访问。然后,尝试使用未授权的用户或可能的访问链接访问这些对象。虽然我们知道PDF、DOC、XLS等文件,但在AWS S3术语中,它们都被称为对象。

2. 创建访问私有S3存储桶中对象的策略
在AWS中,IAM(身份和访问管理)是所有服务的基础!用户、组、角色和策略是我们需要熟悉的基本概念。
有许多内置(built-in)角色,每个角色都有许多内置策略,这意味着权限。这些被称为"AWS托管"。但是,也可以创建"客户托管"(Customer Managed)角色和策略。因此,这里创建了一个自定义策略。
- 创建一个自定义IAM策略,用于从您的私有S3存储桶获取对象。
- 在AWS中找到现有策略列表,并创建一个新策略,如下所示,仅从您的私有S3存储桶执行
GetObject操作:

如下所示创建自定义策略。选择S3作为服务,仅选择
GetObject作为操作(action):
选择"specific"作为资源(resource),并指定您的私有S3存储桶,以便策略具有所需的功能:

为您的策略命名并创建。您可以使用任何名称,但您需要记住它。

您的自定义策略摘要将如下所示。也可以直接使用此JSON内容创建策略:

策略JSON定义:
JSON
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::private-s3-for-interfacing/*"
}
]
}
3. 创建访问私有S3存储桶中对象的Lambda函数
这里使用了Lambda函数的最新NodeJS版本。创建一个Lambda函数并选择NodeJS。您可以为Lambda函数选择任何受支持的语言,如Python、Go、Java、.NET Core等。

创建Lambda函数时,会显示一个示例"hello"代码。我们需要用我们自己的代码替换它。
如您所见,Lambda开发环境类似于基于Web的轻量级IDE。

用给定的简短示例代码替换现有代码。代码的新版本将如下所示。更改代码后,按"Deploy"按钮以使用Lambda函数。
为了场景的简单性,存储桶名称被静态使用。文件名作为参数以
fn名称发送。虽然默认内容类型(content type)假定为pdf,但它可以是Lambda函数代码中实现的任何文件类型。由于我们将选择在API Gateway连接中使用Lambda函数的代理功能,响应头(response header)包含一些必需的附加数据。NodeJS Lambda代码(以Blob形式返回):
JavaScript
// Lambda函数代码如下所示
// 此代码将以blob内容形式返回响应
// 可以使用附件中的Callback-to-Download-Blob.html下载文件
const AWS = require('aws-sdk');
const S3= new AWS.S3();
exports.handler = async (event, context) => {
let fileName;
let bucketName;
let contentType;
let fileExt;
try {
bucketName = 'private-s3-for-interfacing';
fileName = event["queryStringParameters"]['fn']
contentType = 'application/pdf';
fileExt = 'pdf';
//------------
fileExt = fileName.split('.').pop();
switch (fileExt) {
case 'pdf': contentType = 'application/pdf'; break;
case 'png': contentType = 'image/png'; break;
case 'gif': contentType = 'image/gif'; break;
case 'jpeg': case 'jpg': contentType = 'image/jpeg'; break;
case 'svg': contentType = 'image/svg+xml'; break;
case 'docx': contentType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; break;
case 'xlsx': contentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; break;
case 'pptx': contentType = 'application/vnd.openxmlformats-officedocument.presentationml.presentation'; break;
case 'doc': contentType = 'application/msword'; break;
case 'xls': contentType = 'application/vnd.ms-excel'; break;
case 'csv': contentType = 'text/csv'; break;
case 'ppt': contentType = 'application/vnd.ms-powerpoint'; break;
case 'rtf': contentType = 'application/rtf'; break;
case 'zip': contentType = 'application/zip'; break;
case 'rar': contentType = 'application/vnd.rar'; break;
case '7z': contentType = 'application/x-7z-compressed'; break;
default: ;
}
//------------
const data = await S3.getObject({Bucket: bucketName, Key: fileName}).promise();
return {
headers: {
'Content-Type': contentType,
'Content-Disposition': 'attachment; filename=' + fileName, // 成功的关键
'Content-Encoding': 'base64',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token',
'Access-Control-Allow-Methods': 'GET,OPTIONS'
},
body: data.Body.toString('base64'),
isBase64Encoded: true,
statusCode: 200
}
} catch (err) {
return {
statusCode: err.statusCode || 400,
body: err.message || JSON.stringify(err.message) + ' - fileName: '+ fileName + ' - bucketName: ' + bucketName
}
}
}
也可以在Lambda函数中使用Python代码,如下所示:
Python
# 以下代码可以像上面的NodeJS示例一样开发
import base64
import boto3
import json
import random
s3 = boto3.client('s3')
def lambda_handler(event, context):
try:
fileName = event['queryStringParameters']['fn']
bucketName = 'private-s3-for-interfacing'
contentType = 'application/pdf'
response = s3.get_object(
Bucket=bucketName,
Key=fileName,
)
file = response['Body'].read()
return {
'statusCode': 200,
'headers': {
'Content-Type': contentType,
'Content-Disposition': 'attachment; filename='+ fileName,
'Content-Encoding': 'base64'
# 如果需要,可以在此处添加CORS相关代码
},
'body': base64.b64encode(file).decode('utf-8'),
'isBase64Encoded': True
}
except:
return {
'headers': { 'Content-type': 'text/html' },
'statusCode': 200,
'body': 'Error occurred in Lambda!'
}
另一种方法是使用Lambda生成预签名URL(presigned URL):
JavaScript
// 此方法将提供预签名URL
// 可以使用Callback-for-preSignedUrl.html文件来使用预签名URL链接
var AWS = require('aws-sdk');
var S3 = new AWS.S3({
signatureVersion: 'v4',
});
exports.handler = async (event, context) => {
let fileName;
let bucketName;
let contentType;
bucketName = 'private-s3-for-interfacing';
fileName = event["queryStringParameters"]['fn'];
contentType = 'application/json';
const presignedUrl = S3.getSignedUrl('getObject', {
Bucket: bucketName,
Key: fileName,
Expires: 300 // 秒
});
let responseBody = {'presignedUrl': presignedUrl};
return {
headers: {
'Content-Type': contentType,
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token',
'Access-Control-Allow-Methods': 'GET,OPTIONS'
},
body: JSON.stringify(responseBody),
statusCode: 200
}
};
创建Lambda函数时,会随之创建一个角色。但是,此角色没有访问您私有S3存储桶中对象的权限。现在,我们需要将在之前步骤中创建的"客户托管"策略附加到随Lambda函数创建的此角色上。
创建Lambda函数后,您可以找到自动创建的角色,如下所示:

将您在上一步创建的自定义策略附加到此角色;这样Lambda函数将对您的S3存储桶拥有有限的
GetObject访问权限。
为了让Lambda访问您的S3存储桶,这就是需要做的全部工作。现在,是时候创建一个AWS Gateway方法来使用我们的Lambda函数了。
4. 创建使用Lambda函数的Gateway API
如下所示创建AWS Gateway REST API。可以看到有许多选项,但我们将创建一个"REST" API作为"New API"。为您的API Gateway命名。

创建和运行AWS GW API有几个步骤:
- 创建API
- 创建Resource
- 创建Method
- 部署API(Deploy)
如下所示为您的REST API创建一个
Resource:
这里创建的资源(resource)稍后将在API的URL中使用。

为您创建的资源创建
GET方法:
这里可以创建任何HTTP方法,如
GET、POST、PUT、DELETE等。根据我们的需要,我们只创建GET。不要忘记将之前步骤中创建的Lambda函数与此方法连接。这里选中了Lambda代理集成。这种方法允许我们在Lambda函数中处理所有与响应相关的内容。

创建
GET方法后,API Gateway方法和Lambda函数之间的流程将如下所示:
如下所示为Gateway API启用CORS。可以选中Default 4xx和Default 5xx选项;这样即使错误也可以顺利返回。

创建和配置与AWS Gateway方法相关的所有内容后,现在是时候部署API了。API将部署到如图所示的阶段(stage)。此外,此阶段名称将在公共API URL中使用。

部署后,URL将如下所示。现在可以从任何应用程序使用此链接。

要限制对API网关的访问,我们应该定义一个授权器(Authorizer)。我们可以如下所示定义一个Cognito授权器。
如下图所示,Authorization是需要添加到请求头部分的JWT令牌(token),以使用授权的API方法。
当Cognito托管UI使用Cognito用户/密码提交时,Cognito将用户重定向到回调URL,并传递
id_token和额外的state数据。请注意,我们需要添加到头部的令牌在Token Source下被命名为"Authorization"。

定义基于Cognito的授权器后,可以如下使用:

另一方面,如果您不想为API Gateway定义授权器,可以通过"资源策略(Resource Policy)"限制对API URL的访问,如下所示。
如果资源策略被更改/添加,API必须重新部署。显示为
xxx.xxx.xxx.xxx的IP可以是服务器的IP。如果有人尝试从不同的IP访问URL,将显示以下消息:{"Message":"User: anonymous is not authorized to perform: execute-api:Invoke on resource: arn:aws:execute-api:eu-west-2:********8165:... with an explicit deny"}
资源策略JSON代码如下:
JSON
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "execute-api:Invoke",
"Resource": "*"
},
{
"Effect": "Deny",
"Principal": "*",
"Action": "execute-api:Invoke",
"Resource": "*",
"Condition": {
"NotIpAddress": {
"aws:SourceIp": "xxx.xxx.xxx.xxx"
}
}
}
]
}
5. 用作Web文件夹的公共S3存储桶
解决方案需要两个S3存储桶(bucket)。第一个在之前的部分中创建。第二个现在正在创建,将用作Web文件夹。第一个用作存储所有文件的私有存储桶。

创建一个公共S3存储桶作为Web文件夹。此存储桶包含一个
callback.html文件,因此可以用作Cognito回调(callback)地址。
用于Web的S3存储桶应该是公共(public)的。因此可以应用以下策略:
JSON
// 策略JSON将如下所示
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::web-s3-for-interfacing/*"
}
]
}
下载源文件
您可以从以下链接下载
Callback.html和其他源文件:6. 创建和配置Cognito用户池
- 回调地址:
https://web-s3-for-interfacing.s3.eu-west-2.amazonaws.com/Callback.html - OAuth 2.0 Flows: 选中"implicit grant"选项。
- OAuth 2.0 Scopes: email、openid、profile。
查看下面的托管UI链接。
添加一个额外的"state"URL参数以向托管Cognito登录页面发送参数。"state"参数将被传递到
Callback.html文件。Cognito托管UI链接包含许多URL参数,如下所示:
https://test-for-user-pool-for-s3.auth.eu-west-2.amazoncognito.com/login?client_id=7uuggclp7269oguth08mi2ee04&response_type=token&scope=openid+profile+email&redirect_uri=https://web-s3-for-interfacing.s3.eu-west-2.amazonaws.com/Callback.html&state=fn=testFile.pdf字段:
client_id=7uuggclp7269oguth08mi2ee04response_type=tokenscope=openid+profile+emailredirect_uri=https://web-s3-for-interfacing.s3.eu-west-2.amazonaws.com/Callback.htmlstate=fn=testFile.pdf
state是一个自定义URL参数。它可以发送到托管UI页面并返回到Callback.html页面。应该创建一个客户端应用程序,如下所示:

应用程序客户端设置可以如下确认:

应该设置一个域名(domain name),以便可以用作托管UI的URL。

7. 如何测试场景?
让我们看看如何使用Cognito用户池测试允许受限访问的API。
任何最终用户都可以点击链接来启动此过程。假设我们有一个包含以下HTML内容的网页。如您所见,每个文件的链接都是Cognito托管UI的URL。
LinkToS3Files.html文件可用于测试场景。下载测试文件
结论
希望本文对AWS云环境初学者有所帮助。