65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (32投票s)

2022年2月14日

CPOL

10分钟阅读

viewsIcon

44072

downloadIcon

341

通过 AWS Cognito 用户池(使用托管 UI)、授权的 API Gateway 和 Lambda 以安全的方式提供 URL 链接来访问私有 S3 存储桶中的对象。

场景

假设您一直在为客户开发一些应用程序。但是,有一些文件,例如 PDF、Word、Excel 等,与应用程序中的记录相关。为了简化场景,假设这些文件存储在 AWS 中的一个私有 S3 存储桶中。用户需要能够通过应用程序中的 URL 链接访问私有 S3 存储桶中任何这些相关文件。我们的解决方案需要作为任何公司内部软件的便携式解决方案运行。

引言

本文的目标是展示如何使用 Cognito 用户池下载私有 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 用户池时使用它。当然,这不是强制性的,但这个顺序将使开发更容易。因此,这里首选这种方法。

Outline

我们将寻找以下问题的答案。请记住,因为这里的所有项目都是在 AWS 环境中创建的,所以您必须拥有一个 AWS 账户才能应用本文中的所有步骤。

  1. 如何创建私有 S3 存储桶
  2. 如何创建用于访问私有 S3 存储桶中对象的自定义策略
  3. 如何创建 Lambda 函数以访问私有 S3 存储桶中的对象
  4. 如何创建 Gateway API 以使用 Lambda 函数
  5. 如何创建公共 S3 存储桶作为 Web 文件夹
  6. 如何创建 Cognito 用户池并配置设置
  7. 如何测试场景

1. 如何创建私有 S3 存储桶

S3 是 AWS 中基于区域的服务之一。S3 存储桶中的项目称为对象。因此,在 AWS 中,S3 存储桶可以使用对象和文件来互相替换。将“阻止所有公共访问”复选框保持选中状态。这里创建了一个私有 S3 存储桶。尽管有许多额外的配置选项,但为了简化解决方案,我们使用默认值创建。

要测试对 S3 存储桶的私有访问,请向其中上传一些对象。然后,尝试使用不允许的用户或可能的访问链接访问这些对象。尽管我们现在将 PDF、DOC、XLS 等视为文件,但在 AWS S3 术语中,这些都称为对象。

2. 如何创建用于访问私有 S3 存储桶中对象的自定义策略

在 AWS 中,IAM(身份和访问管理)是所有服务的基础!用户、组、角色和策略是我们必须熟悉的一些词。

有许多内置角色。每个角色都有许多内置策略,这意味着权限。这些被称为“AWS 托管”。但是,也可以创建“客户托管”角色和策略。因此,这里创建了一个自定义策略。

  • 创建自定义 IAM 策略以从私有 S3 存储桶中获取对象,如下所示。
  • 在 AWS 中查找当前策略列表,并创建一个新策略,仅允许从您的私有 S3 存储桶进行“GetObject”操作,如下所示

创建自定义策略,如下所示。服务选择 S3,操作仅选择“GetObject”,如下所示

将资源选择为特定并选择您的私有 S3 存储桶,以便此策略具有您想要的能力。

为您的策略命名并创建它,如下所示。您可以随意命名,但您应该记住它。

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

//Policy JSON definition can be copied from below

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::private-s3-for-interfacing/*"
        }
    ]
}

3. 如何创建 Lambda 函数以访问私有 S3 存储桶中的对象

这里,Lambda 函数使用 NodeJS 最新版本。创建一个 Lambda 函数并选择 NodeJS。可以选择任何受支持的语言,例如 Python、Go、Java、.NET Core 等,用于 Lambda 函数。为您的 Lambda 函数命名,如下所示

创建 Lambda 函数时,会显示一个示例“hello”代码。我们需要开发自己的代码。

正如所见,Lambda 开发环境看起来像一个基于 Web 的轻量级 IDE。

将以下代码替换为给定的简短示例代码。

新代码将如下所示。更改代码后,按“部署”按钮以使用 Lambda 函数。

为了简化场景,存储桶名称是静态使用的。文件名作为参数发送,名称为“fn”。尽管默认内容类型被接受为 pdf,但它可以是 Lambda 函数代码中实现的任何文件。因为我们将更喜欢在 API Gateway 连接中使用 Lambda 函数代理功能,所以响应头包含一些更多所需的数据。

// Code for Lambda function looks like this
// This code will be returning response as blob content
// Callback-to-Download-Blob.html in attached files could be used to download

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':
            contentType = 'image/jpeg';
            break;
        case 'jpg':
            contentType = 'image/jpeg';
            break;
        case 'svg':
            contentType = '.svg image/svg+xml';
            break;
        case 'docx':
            contentType = 
               'application/vnd.openxmlformats-officedocument.wordprocessingml.document';   
            break;
        case 'xlsx':
            contentType = 
            'Content-Type: 
             application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';  
            break;
        case 'pptx':
            contentType = 
            'Content-Type: 
             application/vnd.openxmlformats-officedocument.presentationml.presentation';    
            break;
        case 'doc':
            contentType = 'Content-Type: application/msword';    
            break;
        case 'xls':
            contentType = 'Content-Type: application/vnd.ms-excel';   
            break;
        case 'csv':
            contentType = 'Content-Type: text/csv';     
            break;    
        case 'ppt':
            contentType = 'Content-Type: application/vnd.ms-powerpoint'; 
            break;
        case 'rtf':
            contentType = 'Content-Type: application/rtf'; 
            break;
        case 'zip':
            contentType = 'Content-Type: application/zip';  
            break;   
        case 'rar':
            contentType = 'Content-Type: application/vnd.rar'; 
            break;
        case '7z':
            contentType = 'Content-Type: application/x-7z-compressed';  
            break;
        default:
            ;  
    }
    
    //------------
    
    //console.log(`Hi from Node.js ${process.version} on Lambda!`);
    const data = await S3.getObject({Bucket: bucketName, Key: fileName}).promise();
    
    return {
       headers: {
          'Content-Type': contentType,
          'Content-Disposition': 'attachment; filename=' + fileName, // key of success
          '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 代码,如下所示

//The following code could be improved as NodeJS above
    
import base64
import boto3
import json
import random

s3 = boto3.client('s3')

def lambda_handler(event, context):
    try:
        
        #fileName = 'testFile.pdf'
        #bucketName = event['pathParameters']['bn']        
        #fileType = event['queryStringParameters']['ft']

        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'

                          #if it is required some more CORS-related code 
                          #could be added here
                        },
            '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,如下所示

// This way will provide presigned url
// Callback-for-preSignedUrl.html in attached files could be used to use the 
// presigned URL link

var AWS = require('aws-sdk');
var S3 = new AWS.S3({
  signatureVersion: 'v4',
});

exports.handler =  async (event, context) => {
    
  let fileName;
  let bucketName;
  let contentType;
  let fileExt;
    
  bucketName = 'private-s3-for-interfacing';
  fileName = event["queryStringParameters"]['fn'];
  contentType = 'application/json';
    
  const presignedUrl = S3.getSignedUrl('getObject', {
    Bucket: bucketName,//'BUCKET NAME',
    Key: fileName, //'UploadedFile',
    Expires: 300 //sec
  });

  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 函数自动创建的角色,如下所示

将您在上一步中创建的自定义策略附加到此角色,以便 Lambda 函数可以对您的 S3 存储桶拥有有限的“GetObject”访问权限。

到目前为止,所有关于 Lambda 访问 S3 存储桶的工作都已完成。现在是时候创建一个 AWS Gateway 方法来使用我们的 Lambda 函数了。

4. 如何创建 Gateway API 以使用 Lambda 函数

创建 AWS Gateway REST API,如下所示。如所见,有许多选项。但是,我们创建一个“REST”作为“新 API”。为您的 API Gateway 命名,如下所示。

创建和运行 AWS GW API 有一些步骤

  • 创建 API
  • 创建资源
  • 创建方法
  • 部署 API

为您的 REST API 创建资源,如下所示

这里创建的资源将在以后用于 API 的 URL 中。

为您创建的资源创建GET方法,如下所示

这里可以创建任何 http 方法,例如 GETPOSTPUTDELETE 等。为了满足我们的需求,我们只创建 GET。不要忘记将我们在前几步中创建的 Lambda 函数绑定到此方法。

这里选中了 Lambda 代理集成。这种方法使我们能够在 Lambda 函数中处理所有与响应相关的内容。

创建 GET 方法后,API Gateway 方法和 Lambda 函数之间的流程将如下所示

为 Gateway API 启用 CORS,如下所示。可以选中默认 4xx默认 5xx,以便即使错误也可以毫无问题地返回。

创建并配置完所有关于 AWS Gateway 方法后,现在是时候部署 API 了,如下所示。API 被部署到一个阶段,如下所示。阶段名称也将用于公共 API URL 中。

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

我们应该定义一个授权器来限制对 API Gateway 的访问。我们可以定义一个 Cognito 授权器,如下所示。

如下图所示,授权是 JWT 令牌,应添加到请求头中以使用授权的 API 方法。

当 Cognito 托管 UI 与 Cognito 用户名/密码提交时,Cognito 将通过传输id_token和附加的状态数据将用户重定向到回调 URL。

请注意,我们应该添加到标头中的令牌在令牌源下称为“授权”。

定义基于 Cognito 的授权器后,可以按如下方式使用它

另一方面,请注意,如果您不想为 API Gateway 定义授权器,则可以使用“资源策略”限制对 API URL 的访问,如下所示。

如果“资源策略”被更改/修改/添加/删除等,则应部署 API。显示为 xxx.xxx.xxx.xxx 的 IP 可以是服务器的 IP。当任何人尝试从不同 IP 访问 URL 时,将显示以下消息。

{"Message":"用户: anonymous 未被授权对资源执行: execute-api:Invoke: arn:aws:execute-api:eu-west-2:********8165:https://x9dxwctglh.execute-api.eu-west-2.amazonaws.com/apiv11/ac?fn=testFile.pdf 具有显式拒绝"}

// Resource Policy JSON code will be as below.

{
    "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. 如何创建公共 S3 存储桶以用作 Web 文件夹

为了解决方案,我们需要有两个 S3 存储桶。第一个已在前面的章节中创建。第二个现在创建并将用作 Web 文件夹。第一个用作私有存储桶以存储所有文件。

创建公共 S3 存储桶作为 Web 文件夹。此存储桶包含一个 callback.html,以便可以用作 Cognito 回调地址。

用于 Web 的 S3 存储桶应该是公共的。因此,可以应用以下策略。

// Policy JSON will look like this

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::web-s3-for-interfacing/*"
        }
    ]
}

Callback.html 的内容如下

  • Callback.html 将接收 filenameid_token 作为参数。
  • FileName 将作为 URL 参数发送到 API GW 方法。
  • id_token 将作为头发送到 API Gateway,用于 Cognito 授权的 API GW 方法。

Callback.html 在下面的 zip 文件中

6. 如何创建 Cognito 用户池并配置设置

请参阅下面的托管 UI 链接。

添加额外的“状态”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

https://test-for-user-pool-for-s3.auth.eu-west-2.amazoncognito.com

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

state 是一个特殊的 URL 参数。它可以发送到托管 UI 页面并返回到 Callback.html

应创建一个客户端应用程序,如下所示

应用客户端设置可以确认,如下所示

应设置域名,以便用作托管 UI 的 URL

7. 如何测试场景

让我们看看如何测试使用 Cognito 用户池限制访问的 API。

任何最终用户都可以单击链接开始此过程。假设我们有一个包含以下 HTML 内容的网页。如所见,每个文件的链接都是 Cognito 托管 UI 的 URL。

下面的 zip 文件中的 LinkToS3Files.html 可用于测试场景。

结论

希望本文对 AWS 云环境的初学者有所帮助。

历史

  • 2022 年 2 月 14 日:初始版本
© . All rights reserved.