S3 Presign URL调用上传附件

一、背景

S3 Presign URL是通过S3服务生成一个带签名的URL,这个URL包含了S3桶名、文件名(目录名)、所属账号、时间戳、有效期等信息。当在有效期内的时候,通过Post请求发送到这个URL,可以上传/下载。当有效期超过之后,访问此文件,会提示403没有权限。

使用Presign URL的好处是安全:

  • 调用API接口时候,只暴露Access Key ID,而不暴露Secret Key,也就是不暴露API的密码。
  • Presign URL可设置过期时间,例如生成URL后限制60秒,那么上传操作必须在60秒内完成,60秒后之前生成的URL和时间戳过期。

S3 Presign URL的原理讲解:https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/dev/PresignedUrlUploadObject.html

使用Python SDK调用Presign URL:https://boto3.amazonaws.com/v1/documentation/api/latest/guide/s3-presigned-urls.html

二、生成Presign URL

生成Presign URL是必须在后端完成,需要与服务器和AWS S3进行交互。因此一般可以定义一个服务或接口,专门用于生成S3 Presign URL,要上传前调用这个接口,获得特定权限。调用前,要事先人为指定文件名,S3要求基于一个确定的文件名来生成签名。

本实验使用Python3代码模拟服务器端的行为。为了运行本程序,需要安装Python 3.8的request库,用于向构建的URL发起POST操作。安装命令如下:

pip install requests

编写如下脚本,此脚本要求本级安装有AWS CLI,能正常执行CLI工具。将如下内容中S3桶名称替换为实际使用的痛名称,将文件名替换为要上传的文件名,最后保存为 generateURL.py

import boto3
import requests
import json

bucket_name = "s3-js-upload-demo"
object_name = "upload-file-01.jpg"

def create_presigned_post(bucket_name, object_name,
                          fields=None, conditions=None, expiration=120):
    s3_client = boto3.client('s3')
    response = s3_client.generate_presigned_post(bucket_name,
                                                    object_name,
                                                    Fields=fields,
                                                    Conditions=conditions,
                                                    ExpiresIn=expiration)
    return response

response = create_presigned_post(bucket_name, object_name)
url = response['url']
fileds = response['fields']
print(url)
print(fileds)

在Console下运行,返回结果如下。

# lxy @ myMBP in ~ [9:47:43] 
$ /usr/local/bin/python3 /Users/lxy/Documents/AWS/MyWorkshop/S3_PreSign-Upload/generateURL.py                                                                                                              
https://s3-js-upload-demo.s3-accelerate.amazonaws.com/
{'key': 'upload-file-01.jpg', 'AWSAccessKeyId': 'AKIAR57Y4KKLEJSU5S5O', 'policy': 'eyJleHBpcmF0aW9uIjogIjIwMjAtMTAtMjBUMDE6NDk6NDVaIiwgImNvbmRpdGlvbnMiOiBbeyJidWNrZXQiOiAiczMtanMtdXBsb2FkLWRlbW8ifSwgeyJrZXkiOiAidXBsb2FkLWZpbGUtMDEuanBnIn1dfQ==', 'signature': '6+vDtE7dwdzSPlkOXLQa4Reeb8o='}

在如下一段返回结果中,第一行的地址是URL,也就是调用S3的Endpoint终端节点。然后分别返回的字段包括key(文件名)、AWSAccessKeyId(API Access Key ID)、policy(对S3操作策略)、signature(签名)等。在后续的代码调用中都需要传入给S3。

接下来我们分别从后台、前台上传测试。

三、从后台上传测试

在上传过程时候,可以从客户端的前端如HTML/Javascript等多种方式发起Post操作上传。也可以使用Java/Python等后台程序完成。

以Python为例,编写如下脚本,并另存为 uploadToS3.py,保存后,修改其中要上传的文件名。

import boto3
import requests
import json

object_name = 'upload-file-01.jpg'

url = "https://s3-js-upload-demo.s3-accelerate.amazonaws.com/"
fileds = {'key': 'upload-file-01.jpg', 'AWSAccessKeyId': 'AKIAR57Y4KKLEJSU5S5O', 'policy': 'eyJleHBpcmF0aW9uIjogIjIwMjAtMTAtMjBUMDE6NDk6NDVaIiwgImNvbmRpdGlvbnMiOiBbeyJidWNrZXQiOiAiczMtanMtdXBsb2FkLWRlbW8ifSwgeyJrZXkiOiAidXBsb2FkLWZpbGUtMDEuanBnIn1dfQ==', 'signature': '6+vDtE7dwdzSPlkOXLQa4Reeb8o='}

with open(object_name, 'rb') as f:
    files = {'file': (object_name, f)}
    http_response = requests.post(url, fileds, files=files)
    print(http_response)

执行后返回结果如下。

# lxy @ 8c85905f3ef5 in ~/Documents/AWS/MyWorkshop/S3_PreSign-Upload [9:48:32] 
$ /usr/local/bin/python3 /Users/lxy/Documents/AWS/MyWorkshop/S3_PreSign-Upload/uploadToS3.py                                                                                                               <aws:sgp>
/Users/lxy/Documents/AWS/MyWorkshop/S3_PreSign-Upload
<Response [204]>

如果返回代码是204,这表示上传成功。如果返回结果是403,则表示Post过去的URL地址、Token等拼接的信息不对,无权限写入。

如果报告File Not Found,则是Python运行路径的问题。例如在MacOS上VSCode等开发工具内调试的时候,系统自动的路径其实是 /Users/xxx 这种用户Home目录,因此运行时候会发现要上传的图片不在系统Home路径下,就无法读取。解决办法二选一:

  • 把代码里边改为绝对路径,例如 /Users/abc/Documents/code/xxxx.jpg
  • 或者在开发工具自带的Terminal下切换下路径,执行 cd /Users/abc/Documents/code/ 进入代码和图片所在的路径,再次从VSCode上执行,就可以读取到文件。

上传完毕后,去S3存储桶内即可看到文件。注意在S3 Presign URL的过期时间有效期内,可以反复多次上传,后续上传会覆盖原始文件。

四、从前台上传测试

客户端前端上传,可以用任何一种语言,只要构造POST请求发到刚才获取的PreSign URL地址就可以了。

例如HTML构建如下网页:

<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  </head>
  <body>
    <!-- Copy the 'url' value returned by S3Client.generate_presigned_post() -->
    <form action="https://s3-js-upload-demo.s3-accelerate.amazonaws.com/" method="post" enctype="multipart/form-data">
      <!-- Copy the 'fields' key:values returned by S3Client.generate_presigned_post() -->
      <input type="hidden" name="key" value="upload-file-01.jpg" />
      <input type="hidden" name="AWSAccessKeyId" value="AKIAR57Y4KKLEJSU5S5O" />
      <input type="hidden" name="policy" value="eyJleHBpcmF0aW9uIjogIjIwMjAtMTAtMTRUMTA6MDY6NDRaIiwgImNvbmRpdGlvbnMiOiBbeyJidWNrZXQiOiAiczMtanMtdXBsb2FkLWRlbW8ifSwgeyJrZXkiOiAidXBsb2FkLWZpbGUtMDEuanBnIn1dfQ==" />
      <input type="hidden" name="signature" value="drAiaex/Q18YOug6rUSKm5zr9Zg=" />
    File:
      <input type="file"   name="file" /> <br />
      <input type="submit" name="submit" value="Upload to Amazon S3" />
    </form>
  </body>
</html>

在这段HTML代码中,包含了隐藏的hidden属性,需要替换其中的 policysignature 为前文获得的参数,然后保存为 upload.html 。在开发者本地可以用浏览器直接打开这个upload.html,即可测试上传。

测试过程注意,前文生成的Presign URL的时间有效期是120秒,因此如果修改html、保存到本地等做测试过程超过120秒,会返回403无权限。在实际代码开发过程中,上传html的界面上的参数是通过接口自动带出来,因此只需要按照上传文件的大概体积,给一个适当的上传超时的时间即可。

上传完毕后,去S3存储桶内即可看到文件。注意在S3 Presign URL的过期时间有效期内,可以反复多次上传,后续上传会覆盖原始文件。

五、结论

S3 Presign URL是由服务器端与AWS S3服务生成的包含临时访问地址,可以在有效时间内完成上传操作。生成的Presign URL可以被直接输出为HTML,也可以被放入到Javascript中进行页面纯前端的调用。

当在页面前端使用S3 Presign URL去Post上传文件时候,网络流量从网页直接上传到AWS S3,而不经过后台web服务器。降低了Web服务器的负载。配合S3存储桶的全球加速功能,可以实现快速的各国家就近接入加速上传。

完。