我正在通过 REST API 仅使用 JavaScript 实现从客户端机器到 Amazon S3 的直接文件上传,而无需任何服务器端代码。一切正常,但有一件事让我担心......
当我向 Amazon S3 REST API 发送请求时,我需要对请求进行签名并将签名放入 Authentication
标头中。要创建签名,我必须使用我的密钥。但是所有事情都发生在客户端,因此,密钥可以很容易地从页面源中泄露(即使我混淆/加密了我的源)。
我该如何处理?这真的有问题吗?也许我可以将特定私钥的使用仅限于来自特定 CORS Origin 的 REST API 调用以及仅限 PUT 和 POST 方法,或者可能仅将密钥链接到 S3 和特定存储桶?可能还有其他身份验证方法吗?
“无服务器”解决方案是理想的,但我可以考虑涉及一些服务器端处理,不包括将文件上传到我的服务器然后发送到 S3。
我认为您想要的是使用 POST 的基于浏览器的上传。
基本上,您确实需要服务器端代码,但它所做的只是生成签名策略。一旦客户端代码具有签名策略,它就可以使用 POST 直接上传到 S3,而无需通过您的服务器。
这是官方文档链接:
图表:http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingHTTPPOST.html
示例代码:http://docs.aws.amazon.com/AmazonS3/latest/dev/HTTPPOSTExamples.html
签署的政策将以如下形式进入您的 html:
<html>
<head>
...
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
...
</head>
<body>
...
<form action="http://johnsmith.s3.amazonaws.com/" method="post" enctype="multipart/form-data">
Key to upload: <input type="input" name="key" value="user/eric/" /><br />
<input type="hidden" name="acl" value="public-read" />
<input type="hidden" name="success_action_redirect" value="http://johnsmith.s3.amazonaws.com/successful_upload.html" />
Content-Type: <input type="input" name="Content-Type" value="image/jpeg" /><br />
<input type="hidden" name="x-amz-meta-uuid" value="14365123651274" />
Tags for File: <input type="input" name="x-amz-meta-tag" value="" /><br />
<input type="hidden" name="AWSAccessKeyId" value="AKIAIOSFODNN7EXAMPLE" />
<input type="hidden" name="Policy" value="POLICY" />
<input type="hidden" name="Signature" value="SIGNATURE" />
File: <input type="file" name="file" /> <br />
<!-- The elements after this will be ignored -->
<input type="submit" name="submit" value="Upload to Amazon S3" />
</form>
...
</html>
请注意,FORM 操作将文件直接发送到 S3,而不是通过您的服务器。
每次您的用户想要上传文件时,您都需要在服务器上创建 POLICY
和 SIGNATURE
。您将页面返回到用户的浏览器。然后,用户可以直接将文件上传到 S3,而无需通过您的服务器。
当您签署策略时,您通常会使策略在几分钟后过期。这会迫使您的用户在上传之前与您的服务器交谈。这使您可以根据需要监控和限制上传。
进出您的服务器的唯一数据是签名的 URL。您的密钥在服务器上保持秘密。
您可以通过 AWS S3 Cognito 执行此操作,请在此处尝试此链接:
http://docs.aws.amazon.com/AWSJavaScriptSDK/guide/browser-examples.html#Amazon_S3
也试试这个代码
只需更改 Region、IdentityPoolId 和您的存储桶名称
Github
你是说你想要一个“无服务器”的解决方案。但这意味着您无法将任何“您的”代码放入循环中。 (注意:一旦您将代码提供给客户端,它现在就是“他们的”代码了。)锁定 CORS 并没有帮助:人们可以轻松编写一个非基于 Web 的工具(或基于 Web 的代理)来添加正确的 CORS 标头来滥用您的系统。
最大的问题是您无法区分不同的用户。您不能允许一个用户列出/访问他的文件,但要阻止其他用户这样做。如果您检测到滥用行为,除了更改密钥外,您无能为力。 (攻击者可能会再次获得它。)
您最好的选择是为您的 javascript 客户端创建一个带有密钥的“IAM 用户”。仅授予它对一个存储桶的写入权限。 (但理想情况下,不要启用 ListBucket 操作,这会使其对攻击者更具吸引力。)
如果您有一台服务器(即使是一个每月 20 美元的简单微型实例),您可以在服务器上签署密钥,同时实时监控/防止滥用。如果没有服务器,您能做的最好的事情就是在事后定期监控滥用情况。这是我要做的:
1) 定期轮换该 IAM 用户的密钥:每天晚上,为该 IAM 用户生成一个新密钥,并替换最旧的密钥。由于有 2 个密钥,每个密钥的有效期为 2 天。
2) 启用 S3 日志记录,并每小时下载一次日志。设置“上传太多”和“下载太多”的警报。您将需要检查总文件大小和上传文件的数量。您需要同时监控全局总数以及每个 IP 地址的总数(阈值较低)。
这些检查可以“无服务器”完成,因为您可以在桌面上运行它们。 (即 S3 完成所有工作,这些过程只是提醒您注意 S3 存储桶的滥用,因此您不会在月底收到巨额 AWS 账单。)
为已接受的答案添加更多信息,您可以参考我的博客以查看使用 AWS 签名版本 4 的代码运行版本。
将在这里总结:
一旦用户选择了要上传的文件,请执行以下操作: 1. 调用 Web 服务器以启动服务以生成所需的参数
在此服务中,调用 AWS IAM 服务以获取临时凭据 获得凭据后,创建存储桶策略(base 64 编码字符串)。然后使用临时秘密访问密钥对存储桶策略进行签名以生成最终签名将必要的参数发送回 UI 收到后,创建一个 html 表单对象,设置所需的参数并发布它。
有关详细信息,请参阅https://wordpress1763.wordpress.com/2016/10/03/browser-based-upload-aws-signature-version-4/
要创建签名,我必须使用我的密钥。但是所有事情都发生在客户端,因此,密钥可以很容易地从页面源中泄露(即使我混淆/加密了我的源)。
这是你误解的地方。使用数字签名的真正原因是您可以在不泄露您的密钥的情况下验证某些内容是否正确。在这种情况下,数字签名用于防止用户修改您为表单发布设置的策略。
诸如此处的数字签名用于整个网络的安全性。如果有人(NSA?)真的能够破解它们,那么他们的目标将比您的 S3 存储桶大得多 :)
我给出了一个简单的代码来将文件从 Javascript 浏览器上传到 AWS S3 并列出 S3 存储桶中的所有文件。
脚步:
要了解如何创建 Create IdentityPoolId http://docs.aws.amazon.com/cognito/latest/developerguide/identity-pools.html 转到 S3 的控制台页面并从存储桶属性打开 cors 配置并将以下 XML 代码写入其中。
如果您没有任何服务器端代码,那么您的安全性取决于在客户端访问 JavaScript 代码的安全性(即拥有该代码的每个人都可以上传一些东西)。
所以我建议,简单地创建一个特殊的 S3 存储桶,它是公共可写的(但不可读),所以你不需要在客户端任何签名的组件。
存储桶名称(例如 GUID)将是您抵御恶意上传的唯一防御措施(但潜在的攻击者无法使用您的存储桶传输数据,因为它只写给他)
以下是使用节点和 serverless 生成策略文档的方法
"use strict";
const uniqid = require('uniqid');
const crypto = require('crypto');
class Token {
/**
* @param {Object} config SSM Parameter store JSON config
*/
constructor(config) {
// Ensure some required properties are set in the SSM configuration object
this.constructor._validateConfig(config);
this.region = config.region; // AWS region e.g. us-west-2
this.bucket = config.bucket; // Bucket name only
this.bucketAcl = config.bucketAcl; // Bucket access policy [private, public-read]
this.accessKey = config.accessKey; // Access key
this.secretKey = config.secretKey; // Access key secret
// Create a really unique videoKey, with folder prefix
this.key = uniqid() + uniqid.process();
// The policy requires the date to be this format e.g. 20181109
const date = new Date().toISOString();
this.dateString = date.substr(0, 4) + date.substr(5, 2) + date.substr(8, 2);
// The number of minutes the policy will need to be used by before it expires
this.policyExpireMinutes = 15;
// HMAC encryption algorithm used to encrypt everything in the request
this.encryptionAlgorithm = 'sha256';
// Client uses encryption algorithm key while making request to S3
this.clientEncryptionAlgorithm = 'AWS4-HMAC-SHA256';
}
/**
* Returns the parameters that FE will use to directly upload to s3
*
* @returns {Object}
*/
getS3FormParameters() {
const credentialPath = this._amazonCredentialPath();
const policy = this._s3UploadPolicy(credentialPath);
const policyBase64 = new Buffer(JSON.stringify(policy)).toString('base64');
const signature = this._s3UploadSignature(policyBase64);
return {
'key': this.key,
'acl': this.bucketAcl,
'success_action_status': '201',
'policy': policyBase64,
'endpoint': "https://" + this.bucket + ".s3-accelerate.amazonaws.com",
'x-amz-algorithm': this.clientEncryptionAlgorithm,
'x-amz-credential': credentialPath,
'x-amz-date': this.dateString + 'T000000Z',
'x-amz-signature': signature
}
}
/**
* Ensure all required properties are set in SSM Parameter Store Config
*
* @param {Object} config
* @private
*/
static _validateConfig(config) {
if (!config.hasOwnProperty('bucket')) {
throw "'bucket' is required in SSM Parameter Store Config";
}
if (!config.hasOwnProperty('region')) {
throw "'region' is required in SSM Parameter Store Config";
}
if (!config.hasOwnProperty('accessKey')) {
throw "'accessKey' is required in SSM Parameter Store Config";
}
if (!config.hasOwnProperty('secretKey')) {
throw "'secretKey' is required in SSM Parameter Store Config";
}
}
/**
* Create a special string called a credentials path used in constructing an upload policy
*
* @returns {String}
* @private
*/
_amazonCredentialPath() {
return this.accessKey + '/' + this.dateString + '/' + this.region + '/s3/aws4_request';
}
/**
* Create an upload policy
*
* @param {String} credentialPath
*
* @returns {{expiration: string, conditions: *[]}}
* @private
*/
_s3UploadPolicy(credentialPath) {
return {
expiration: this._getPolicyExpirationISODate(),
conditions: [
{bucket: this.bucket},
{key: this.key},
{acl: this.bucketAcl},
{success_action_status: "201"},
{'x-amz-algorithm': 'AWS4-HMAC-SHA256'},
{'x-amz-credential': credentialPath},
{'x-amz-date': this.dateString + 'T000000Z'}
],
}
}
/**
* ISO formatted date string of when the policy will expire
*
* @returns {String}
* @private
*/
_getPolicyExpirationISODate() {
return new Date((new Date).getTime() + (this.policyExpireMinutes * 60 * 1000)).toISOString();
}
/**
* HMAC encode a string by a given key
*
* @param {String} key
* @param {String} string
*
* @returns {String}
* @private
*/
_encryptHmac(key, string) {
const hmac = crypto.createHmac(
this.encryptionAlgorithm, key
);
hmac.end(string);
return hmac.read();
}
/**
* Create an upload signature from provided params
* https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html#signing-request-intro
*
* @param policyBase64
*
* @returns {String}
* @private
*/
_s3UploadSignature(policyBase64) {
const dateKey = this._encryptHmac('AWS4' + this.secretKey, this.dateString);
const dateRegionKey = this._encryptHmac(dateKey, this.region);
const dateRegionServiceKey = this._encryptHmac(dateRegionKey, 's3');
const signingKey = this._encryptHmac(dateRegionServiceKey, 'aws4_request');
return this._encryptHmac(signingKey, policyBase64).toString('hex');
}
}
module.exports = Token;
使用的配置对象存储在 SSM Parameter Store 中,如下所示
{
"bucket": "my-bucket-name",
"region": "us-west-2",
"bucketAcl": "private",
"accessKey": "MY_ACCESS_KEY",
"secretKey": "MY_SECRET_ACCESS_KEY",
}
如果您愿意使用 3rd 方服务,auth0.com 支持此集成。 auth0 服务为 AWS 临时会话令牌交换第 3 方 SSO 服务身份验证将限制权限。
请参阅:https://github.com/auth0-samples/auth0-s3-sample/
和 auth0 文档。
我创建了一个基于 VueJS 和 Go 的 UI 以将二进制文件上传到 AWS Secrets Manager https://github.com/ledongthuc/awssecretsmanagerui
上传受保护的文件和更轻松地更新文本数据很有帮助。需要的可以参考。
不定期副业成功案例分享
${filename}
添加到键名中,因此对于上面的示例,使用user/eric/${filename}
而不仅仅是user/eric
。如果user/eric
是一个已经存在的文件夹,上传将静默失败(您甚至会被重定向到 success_action_redirect)并且上传的内容将不存在。只是花了几个小时调试这个认为这是一个权限问题。