云计算
19分钟阅读

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

使用Cognito用户池、API Gateway、Lambda和S3集成安全访问私有文件的完整指南。

N
Necmettin Demir
2023年7月21日
加载中...

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

AWS Cognito S3
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账户才能完成本文中的所有步骤。
  1. 如何创建私有S3存储桶?
  2. 如何创建访问私有S3存储桶中对象的自定义策略?
  3. 如何创建访问私有S3存储桶中对象的Lambda函数?
  4. 如何创建使用Lambda函数的Gateway API?
  5. 如何创建用作Web文件夹的公共S3存储桶?
  6. 如何创建和配置Cognito用户池?
  7. 如何测试场景?

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

S3是AWS中基于区域(region-based)的服务之一。S3存储桶中的项目称为对象(object)。因此,在AWS中,S3存储桶的对象和文件这两个术语可以互换使用。
保持"阻止所有公共访问"(Block All Public Access)复选框选中。这里创建了一个私有S3存储桶。虽然有许多额外的配置选项,但为了解决方案的简单性,我们使用默认值创建。
创建S3存储桶
创建S3存储桶
上传一些对象到S3存储桶以测试私有访问。然后,尝试使用未授权的用户或可能的访问链接访问这些对象。虽然我们知道PDF、DOC、XLS等文件,但在AWS S3术语中,它们都被称为对象。
上传文件
上传文件

2. 创建访问私有S3存储桶中对象的策略

在AWS中,IAM(身份和访问管理)是所有服务的基础!用户、组、角色和策略是我们需要熟悉的基本概念。
有许多内置(built-in)角色,每个角色都有许多内置策略,这意味着权限。这些被称为"AWS托管"。但是,也可以创建"客户托管"(Customer Managed)角色和策略。因此,这里创建了一个自定义策略。
  • 创建一个自定义IAM策略,用于从您的私有S3存储桶获取对象。
  • 在AWS中找到现有策略列表,并创建一个新策略,如下所示,仅从您的私有S3存储桶执行GetObject操作:
策略列表
策略列表
如下所示创建自定义策略。选择S3作为服务,仅选择GetObject作为操作(action):
策略设置1
策略设置1
选择"specific"作为资源(resource),并指定您的私有S3存储桶,以便策略具有所需的功能:
策略设置2
策略设置2
为您的策略命名并创建。您可以使用任何名称,但您需要记住它。
策略设置3
策略设置3
您的自定义策略摘要将如下所示。也可以直接使用此JSON内容创建策略:
策略JSON
策略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
创建Lambda
创建Lambda函数时,会显示一个示例"hello"代码。我们需要用我们自己的代码替换它。
如您所见,Lambda开发环境类似于基于Web的轻量级IDE。
更改Lambda代码
更改Lambda代码
用给定的简短示例代码替换现有代码。代码的新版本将如下所示。更改代码后,按"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角色
查找Lambda角色
将您在上一步创建的自定义策略附加到此角色;这样Lambda函数将对您的S3存储桶拥有有限的GetObject访问权限。
附加策略
附加策略
为了让Lambda访问您的S3存储桶,这就是需要做的全部工作。现在,是时候创建一个AWS Gateway方法来使用我们的Lambda函数了。

4. 创建使用Lambda函数的Gateway API

如下所示创建AWS Gateway REST API。可以看到有许多选项,但我们将创建一个"REST" API作为"New API"。为您的API Gateway命名。
创建REST API
创建REST API
创建和运行AWS GW API有几个步骤:
  • 创建API
  • 创建Resource
  • 创建Method
  • 部署API(Deploy)
如下所示为您的REST API创建一个Resource
创建Resource步骤1
创建Resource步骤1
这里创建的资源(resource)稍后将在API的URL中使用。
创建Resource步骤2
创建Resource步骤2
为您创建的资源创建GET方法:
创建GET Method
创建GET Method
这里可以创建任何HTTP方法,如GETPOSTPUTDELETE等。根据我们的需要,我们只创建GET。不要忘记将之前步骤中创建的Lambda函数与此方法连接。
这里选中了Lambda代理集成。这种方法允许我们在Lambda函数中处理所有与响应相关的内容。
Lambda代理集成
Lambda代理集成
创建GET方法后,API Gateway方法和Lambda函数之间的流程将如下所示:
流程视图
流程视图
如下所示为Gateway API启用CORS。可以选中Default 4xxDefault 5xx选项;这样即使错误也可以顺利返回。
启用CORS
启用CORS
创建和配置与AWS Gateway方法相关的所有内容后,现在是时候部署API了。API将部署到如图所示的阶段(stage)。此外,此阶段名称将在公共API URL中使用。
部署API
部署API
部署后,URL将如下所示。现在可以从任何应用程序使用此链接。
部署URL
部署URL
要限制对API网关的访问,我们应该定义一个授权器(Authorizer)。我们可以如下所示定义一个Cognito授权器。
如下图所示,Authorization是需要添加到请求头部分的JWT令牌(token),以使用授权的API方法。
当Cognito托管UI使用Cognito用户/密码提交时,Cognito将用户重定向到回调URL,并传递id_token和额外的state数据。
请注意,我们需要添加到头部的令牌在Token Source下被命名为"Authorization"。
定义Cognito Authorizer
定义Cognito Authorizer
定义基于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存储桶结构
两个S3存储桶结构
创建一个公共S3存储桶作为Web文件夹。此存储桶包含一个callback.html文件,因此可以用作Cognito回调(callback)地址。
创建Web Bucket
创建Web Bucket
用于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=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页面。
应该创建一个客户端应用程序,如下所示:
创建Client App
创建Client App
应用程序客户端设置可以如下确认:
App Client设置
App Client设置
应该设置一个域名(domain name),以便可以用作托管UI的URL。
域名设置
域名设置

7. 如何测试场景?

让我们看看如何使用Cognito用户池测试允许受限访问的API。
任何最终用户都可以点击链接来启动此过程。假设我们有一个包含以下HTML内容的网页。如您所见,每个文件的链接都是Cognito托管UI的URL。
LinkToS3Files.html文件可用于测试场景。

下载测试文件


结论

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

云计算服务

我们在AWS、Azure和Google Cloud平台上提供基础设施设计、迁移、管理和优化服务。

查看我们的服务

联系我们

如需了解有关AWS和我们云计算解决方案的详细信息,请与我们的团队交流。

联系