在Web页面中安全地上传文件
为何不建议直接用 Javascript SDK 来上传文件?
- 相比在 iOS 或 Android 中使用 AK 与 SK 上传文件,在 Web 页面上使用 AK 与 SK 会更有挑战。
- 主要原因在于:
- Web 页面基于开放的 HTML 标记语言 与 Javascript 脚本语言,它们的内容通常以源代码的形式直接由用户的浏览器(例如 Chrome)来执行与渲染。
- 使用 Javascript SDK 通常需要将 AK 与 SK 直接放在源代码或某种公开的配置文件当中,这容易造成关键资料的泄露和攻击。
更好的方案
所幸,Javascript 拥有完整高效的 HTTP 网络操作能力,这样我们可以:
- 让后端程序(golang、python、php、nodejs)调用相应的 SDK 生成一个具有 PutObject 方法效力的「预签名」链接传递给 Web 页面的 Javascript 脚本。
- Javascript 脚本利用 网络请求组件(如 axios)来将本地文件 PUT 到上面提到的「预签名 PutObject 链接」
让后端程序生成 PutObject 预签名链接示例:
presigned_url_demo.py
import boto3
from flask import Flask
from flask import render_template_string
from flask import request
app = Flask(__name__)
@app.get('/s3_upload_url')
def get_upload_url():
# Config
s3endpoint = 'https://s3.bitiful.net' # 请填入控制台 “Bucket 设置” 页面底部的 “Endpoint” 标签中的信息
s3region = 'cn-east-1'
s3accessKeyId = '子账户 accessKey' # 请到控制台创建子账户,并为子账户创建相应 accessKey
s3SecretKeyId = '子账户 secretKey' # !!切记,创建子账户时,需要手动为其分配具体权限!!
# 连接 S3
client = boto3.client(
's3',
aws_access_key_id = s3accessKeyId,
aws_secret_access_key = s3SecretKeyId,
endpoint_url = s3endpoint,
region_name = s3region
)
url = client.generate_presigned_url(
'put_object',
Params={
'Bucket': 'xxxxxx',
'Key': request.args.get('key'),
},
ExpiresIn=3600
)
return {'url': url}
- 来源:https://juejin.cn/post/7019216042220077092
- 参考:https://northflank.com/guides/connect-nodejs-to-minio-with-tls-using-aws-s3
presigned_url_demo.node.js
const {
S3Client
} = require("@aws-sdk/client-s3");
// Create an Amazon S3 service client object.
const s3Client = new S3Client({
endpoint: "https://s3.bitiful.net",
credentials: {
accessKeyId: "******",
secretAccessKey: "**************",
}
});
//引入相关模块
const {
PutObjectCommand,
} = require("@aws-sdk/client-s3");
const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");
app.get("/s3_upload_url", (req, res, next) => {
//初始化命令实体
const putCmd = new PutObjectCommand({
Bucket: "****",
Key: "image.jpg"
});
//获取签名
getSignedUrl(s3Client, putCmd, { expiresIn: 3600 }).then((url) => {
//将签名好的url回传给前台
res.send(url);
next();
});
});
upload_demo.web.html
<!DOCTYPE html>
<html>
<head>
<title>S3 File Upload</title>
</head>
<body>
<div>
<h1>S3 File Upload</h1>
<input type="file" id="file">
<button onclick="handleClick()">Upload</button>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/1.3.6/axios.min.js"></script>
<script>
let file;
let fileContent;
const fileInput = document.getElementById('file');
const fileReader = new FileReader();
fileInput.addEventListener('change', function (event) {
file = event.target.files[0];
console.log("file is: ", file);
fileReader.readAsArrayBuffer(file);
});
fileReader.onload = function (event) {
fileContent = event.target.result;
// console.log("fileContent is: ", fileContent);
};
function handleClick() {
console.log('click');
const url = `/s3_upload_url`;
const params = {key: '1.jpg'};
axios.get(url, {params: params})
.then(function (res) {
console.log(res);
const url = res.data.url;
const config = {
headers: {
'Content-Type': null,
},
};
axios.put(url, file, config)
.then(function (res) {
console.log("res is: ", res);
});
});
}
</script>
</body>
</html>
请注意
上面的 Javascript 脚本在发起 Put 请求时将「Content-Type」设置为空值。
Bitiful S4 对象 MIME 的最佳实践
上面的 Html Demo 中将 Content-Type 强制置空完全是有意为之,因为:
- axios 采用 PUT 请求时会默认带上
application/x-www-form-urlencoded
的Content-Type
,而这与 S3 PUT 中对Content-Type
的预期不同; - 对于 S3 协议来说,上传时主动带上文件 MIME 并赋值给
Content-Type
可作为元数据同时保存至 S3 后端系统当中(告诉 S3 上传的对象是何种 MIME 类型)
所以最好的做法可能是 ———— 将 Content-Type
指定为上传文件的 MIME 类型,例如:
const config = {
headers: {
'Content-Type': 'image/jpeg',
},
};
那么在 AWS / OSS 中最好的做法可能是:用 Javascript 程序创建有效的文件类型判断,并在请求「预签名链接」时告知服务端我们需要上传一个何种 MIME 类型的文件:
const url = `/s3_upload_url`;
const params = {key: '1.jpg', mime: 'image/jpeg'};
在 Bitiful S4 中,我们有 更好的 选择:
Bitiful S4 在接收到 Content-Type
为空的上传请求时,会自动根据后缀与字节智能地判断文件的 MIME 类型
。
AWS S3 和 OSS 中会全部变成 "application/octet-stream"
这与 Javascript 中 file.type 只依赖后缀名的判断不同,把 MIME 推测工作交给 S4 会更可靠和真实。
而你需要做的一切就是:
- 删除你的 MIME 判断代码
- 发起 Put 请求时将 Content-Type 置为 Null
就像上面的 Html Demo 做的那样。