表单直传

背景

每个 MSS 的用户,都会用到上传。大多数都是在服务器通过 SDK 上传到 MSS。然而很多应用对在浏览器上传文件到 MSS 的需求很强烈,采用的做法是用户在浏览器上传到应用服务器,然后应用服务器再把文件上传到 MSS。 这种方法有三个缺点:

  • 第一:上传慢。先上传到应用服务器,再上传到 MSS,网络传送多了一倍。如果数据直传到MSS,不走应用服务器,速度将大大提升。
  • 第二:费用高。由于 MSS 上传流量是免费的。如果数据直传到 MSS,不走应用服务器,将节省应用服务器的带宽。
  • 第三:开发维护代价高。应用服务器需要开发相关接收并转发数据到 MSS 的功能。后期还存在扩展维护等开销。

表单直传

表单直传就是在浏览器通过 JaveScript 生成表单直接上传到 MSS。包括用于鉴权的 Signature,文件名字段 key 等连同文件一起封装成表单直接上传到 MSS。 表单内容参考如下:

------WebKitFormBoundaryVDu6DqT79PvZYSgZ
Content-Disposition: form-data; name="AWSAccessKeyId"                   //access key

9cea0fc32fa742fe811e75bd26112ecb
------WebKitFormBoundaryVDu6DqT79PvZYSgZ
Content-Disposition: form-data; name="policy"

eyJjb25kaXRpb25zIjogW3siYnVja2V0IjogImZvcm1wb3N0In0sIFsic3RhcnRzLXdpdGgiLCAiJGtleSIsICJ1cGxvYWQvIl1dLCAiZXhwaXJhdGlvbiI6ICIyMDE3LTEyLTAxVDAwOjAwOjAwWiJ9Cgo=
------WebKitFormBoundaryVDu6DqT79PvZYSgZ
Content-Disposition: form-data; name="signature"                       // signature

MC56JWFf58SSV23/pPss8aM4XXM=
------WebKitFormBoundaryVDu6DqT79PvZYSgZ
Content-Disposition: form-data; name="key"                             // 文件名(key)

upload/js2017
------WebKitFormBoundaryVDu6DqT79PvZYSgZ
Content-Disposition: form-data; name="file"; filename="js2017"
Content-Type: application/octet-stream

hello js                                                               // 文件内容:hello js
------WebKitFormBoundaryVDu6DqT79PvZYSgZ
Content-Disposition: form-data; name="submit"

Upload
------WebKitFormBoundaryVDu6DqT79PvZYSgZ--

policy解析

base64 -D <<< 'eyJjb25kaXRpb25zIjogW3siYnVja2V0IjogImZvcm1wb3N0In0sIFsic3RhcnRzLXdpdGgiLCAiJGtleSIsICJ1cGxvYWQvIl1dLCAiZXhwaXJhdGlvbiI6ICIyMDE3LTEyLTAxVDAwOjAwOjAwWiJ9Cgo='
{"conditions": [
    {"bucket": "formpost"},                                     # 表示上传的Bucket为formpost
    ["starts-with", "$key", "upload/"]                       # 上传的文件名需以upload/开头
],
"expiration": "2017-12-01T00:00:00Z"                 # 此policy在2017.12.01之前有效
}

表单上传鉴权

文件上传需要通过鉴权。表单上传鉴权(singnature)的生成可以在浏览器直接填写 access key 和 secret key 生成。但是这样会暴露 secret key,带来安全隐患。 我们这里采用向后台申请获得 access key、policy 和 signature 的方式来完成上传。示意图如下: 表单上传鉴权

应用后台开发示例

#-*- coding:utf-8 -*-
import BaseHTTPServer
import base64, json
import hmac, hashlib
import sys,os

host = "msstest-corp.sankuai.com"
bucket = "formpost"
upload_dir = "upload"

access_key = "**"
secret_key = "**"

def formpost_token():
    policy_dict = {
        "expiration": "2017-12-01T00:00:00Z",
        "conditions": [
            {"bucket": bucket},
            ["starts-with", "$key", upload_dir],       // 表示上传文件名只能以upload/开头
        ]
    }
    policy_document = json.dumps(policy_dict)
    policy = base64.b64encode(policy_document)
    signature = base64.b64encode(hmac.new(secret_key, policy, hashlib.sha1).digest())

    formFields = [
        ('key', 'upload/${filename}'),
        ('AWSAccessKeyId', access_key),
        ('signature', signature),
        ('policy', policy)
    ]

    token_dict = {}
    token_dict['accessid'] = access_key
    token_dict['host'] = host
    token_dict['policy'] = policy
    token_dict['signature'] = signature
    token_dict['dir'] = upload_dir

    result = json.dumps(token_dict)
    return result

class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
    def do_GET(self):
        try:
            token = formpost_token()
            self.send_content(token)
        except Exception as msg:
            self.handle_error(msg)

    def handle_error(self, msg):
        self.send_content(msg, 404)

    def send_content(self, content, status=200):
        self.send_response(status)
        self.send_header("Content-type", "text/html")
        self.send_header("Origin", "*")
        self.send_header("Content-Length", str(len(content)))

        self.send_header("Access-Control-Allow-Headers", "content-type,x-auth-token")
        self.send_header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS")
        self.send_header("Access-Control-Allow-Origin", "*")
        self.send_header("Access-Control-Expose-Headers", "Cache-Control,Content-Type,Expires,Last-Modified,ETag,X-Timestamp,X-Mss-Trace-id,X-Container-Object-Count,X-Container-Bytes-Used,X-Account-Container-Count,X-Account-Object-Count,X-Account-Bytes-Used,X-Static-Large-Object,X-Container-Read,X-Container-Write,X-Auth-Token")

        self.end_headers()
        self.wfile.write(content)

if __name__ == '__main__':
    serverAddress = ('', 8000)
    server = BaseHTTPServer.HTTPServer(serverAddress, RequestHandler)
    server.serve_forever()

表单上传操作示例

  1. 创建应用后台程序并写入示例代码,然后启动应用后台程序
  2. 点击这里,打开表单上传 JS 页面
  3. 在 Application URL 填写应用后台地址(例子中的地址为http://10.4.240.234:8000),然后点击 Set Application URL,会在表单域示例产生 AWSAccessKeyId、policy 和 signature 三项
  4. 在 Post URL 填写上传地址,示例中上传的桶名为 formpost,然后点击 Set URL
  5. 如果想修改默认文件名,可以删除表单域 key。然后新添加表单域 Name:key,Value 字段需以 upload/ 作为前缀(匹配 policy 里面的 key)
  6. 添加上传文件,示例添加的文件名为js2017
  7. 点击 Upload 按钮完成上传

示例1

生命周期管理

概述

生命周期管理允许用户对一个桶下面的文件进行生命周期管理的配置。这个配置是一套规则,对符合规则的文件会自动进行相关处理,目前我们支持自动删除。

规则说明

  • 生命周期管理的规则以桶为粒度,以天为单位的对符合规则的文件进行自动删除。
  • 规则支持天数(Days)或指定日期(Date)的管理,并支持前缀匹配(可选)。
  • 当配置了天数后,则文件创建的时间超过配置的天数后会被自动删除,若指定了前缀匹配,则与前缀相匹配的文件才会被删除。
  • 当配置了指定日期后,则文件创建的时间在在指定日期之前的文件会被自动删除,若指定了前缀匹配,则与前缀相匹配的文件才会被删除。
  • 每个桶最多配置5条规则。
<LifecycleConfiguration>
  <Rule>
    <ID>..</ID>
    <Expiration>
      <Days>..</Days>         // 指定天数,如30
    </Expiration>
  </Rule>
  <Rule>
    <ID>..</ID>
    <Filter>                  // Filter可选
      <Prefix>del</Prefix>
    </Filter>               
    <Expiration>            
      <Date>..</Date>         // 指定日期,如2017-07-23T22:04:05Z
    </Expiration>
  </Rule>
</LifecycleConfiguration>

举例说明

如桶 backup,包含文件如下:规则配置日期为2017-09-20
2016-12-28 13:36 68261 s3://backup/abc.jpg
2017-08-26 17:33 68261 s3://backup/lena0001
2017-08-26 17:33 68261 s3://backup/lena03
2017-09-19 13:36 68261 s3://backup/del_abc.jpg
2017-09-18 03:33 27582 s3://backup/watermark.png

<LifecycleConfiguration>
  <Rule>
    <ID>id1</ID>
    <Expiration>
      <Days>30</Days>
    </Expiration>
  </Rule>
  <Rule>
    <ID>id2</ID>
    <Filter>
      <Prefix>del</Prefix>
    </Filter>
    <Status>Enabled</Status>
    <Expiration>
      <Date>2017-09-23T22:04:05Z</Date>
    </Expiration>
  </Rule>
</LifecycleConfiguration>

规则id1会在9月21日自动删除abc.jpg(配置后的当天或第二天)
规则id1会在9月27日自动删除lena0001, lena03
规则id2会在9月24日(>09-23 10:00:00)自动删除前缀为del的文件,此处会删除del_abc.jpg

使用说明

配置生命周期管理:s3cmd setlifecycle ${rule_file} s3://${bucket}
删除生命周期管理:s3cmd dellifecycle s3://${bucket}

跨域资源共享

概述

跨域访问,是浏览器出于安全考虑而设置的一个限制,即同源策略。举例说明,当 A,B 两个网站属于不同的域时,如果来自于 A 网站的页面中的 JavaScript 代码希望访问 B 网站的时候,浏览器会拒绝该访问。然而,在实际应用中,经常会有跨域访问的需求。

CORS 介绍

跨域资源共享(Cross-Origin Resource Sharing,简称 CORS),是 HTML5 提供的标准跨域解决方案,具体的CORS规则可以参考 W3C CORS规范
CORS 是一个由浏览器共同遵循的一套控制策略,通过HTTP的Header来进行交互。浏览器在识别到发起的请求是跨域请求的时候,会将Origin的Header加入HTTP请求发送给服务器,如"Origin:http://www.a.com"。服务器端接收到这个请求之后,会根据一定的规则判断是否允许该来源域的请求,如果允许的话,服务器在返回的响应中会附带上Access-Control-Allow-Origin这个Header,内容为www.a.com来表示允许该次跨域访问。如果服务器允许所有的跨域请求的话,将Access-Control-Allow-Origin的Header设置为*即可,浏览器根据是否返回了对应的Header来决定该跨域请求是否成功,如果没有附加对应的Header,浏览器将会拦截该请求。
以上描述的仅仅是简单情况,CORS规范将请求分为两种类型,一种是简单请求,另外一种是带预检的请求。预检机制是一种保护机制,防止资源被本来没有权限的请求修改。浏览器会在发送实际请求之前先发送一个OPTIONS的HTTP请求来判断服务器是否能接受该跨域请求。如果不能接受的话,浏览器会直接阻止接下来实际请求的发生。
预检请求会附带一些关于接下来的请求的信息给服务器,主要有以下几种:

  • Origin:请求的源域信息
  • Access-Control-Request-Method:接下来的请求类型,如POST、GET等
  • Access-Control-Request-Headers:接下来的请求中包含的用户显式设置的Header列表

服务器端收到请求之后,会根据附带的信息来判断是否允许该跨域请求,信息返回同样是通过Header完成的:

  • Access-Control-Allow-Origin:允许跨域的Origin列表
  • Access-Control-Allow-Methods:允许跨域的方法列表
  • Access-Control-Allow-Headers:允许跨域的Header列表
  • Access-Control-Expose-Headers:允许暴露给JavaScript代码的Header列表
  • Access-Control-Max-Age:最大的浏览器缓存时间,单位s。

CORS主要使用场景

CORS使用一定是在使用浏览器的情况下,因为控制访问权限的是浏览器而非服务器。因此使用其它的客户端的时候无需关心任何跨域问题。
使用CORS的主要应用就是在浏览器端使用Ajax直接访问OSS的数据,而无需走用户本身的应用服务器中转。无论上传或者下载。对于同时使用OSS和使用Ajax技术的网站来说,都建议使用CORS来实现与OSS的直接通信。

MSS跨域支持

OSS提供了CORS规则的配置从而根据需求允许或者拒绝相应的跨域请求。该规则是配置在Bucket级别的。详情可以参考PutBucketCORS。
CORS请求的通过与否和OSS的身份验证等是完全独立的,即OSS的CORS规则仅仅是用来决定是否附加CORS相关的Header的一个规则。是否拦截该请求完全由浏览器决定。
当浏览器向MSS发起GET请求,会带一个Origin头部"Origin:http://www.a.com",如果MSS返回的头部没有带"Access-Control-Allow-Origin:http://www.a.com",则浏览器会拒绝接收。MSS采取的策略为当用户发起跨域请求时,如果请求所在的桶没有配置跨域策略,则会直接返回403。
浏览器在上传文件时,会发起一个OPTIONS请求,请求头部"Access-Control-Allow-Method:PUT"。如果没有配置跨域,MSS会返回"Access-Control-Allow-Methods:PUT","Access-Control-Allow-Origin:http://www.a.com",则浏览器才会继续下一步的PUT请求。若没有返回跨域允许的头部,浏览器会停止下一步的PUT操作。

GET请求跨域示例

  • 当没有对Bucket设置跨域时,如果请求带Origin头部,则会报错:
  • $ curl -i -H "Origin:http://www.a.com" "http://mtmss.com/v1/mss_9698a7d474484149970fe7f751eb4fc6/cors-test/test.txt"
HTTP/1.1 403 Forbidden
Server: Tengine
Date: Fri, 24 Nov 2017 06:32:25 GMT
Content-Type: text/xml; charset=utf-8
Content-Length: 286
Connection: keep-alive
X-Amz-Id-2: c6c6c11d21f373e51af4b369cc17b7df
X-Amz-Request-Id: 1511505145675747
X-Ms-Trans-Id: 47d571e36a28fbfd2280e9701d9eb96a-1511505145673812
X-Mss-Trace-Id: 310194632421683019

<?xml version="1.0" encoding="UTF-8"?>
<Error>
  <Code>AccessDenied</Code>
  <Message>Access Denied. Cors access deny</Message>
  <Resource>Bucket:cors-test,Object:test.txt</Resource>
  <RequestId>1511505145675747</RequestId>
  <HostId>c6c6c11d21f373e51af4b369cc17b7df</HostId>
</Error>
  • 设置跨域,创建一个文件(cors.xml),内容如下:
  • $ s3cmd setcors cors.xml s3://cors-test
<CORSConfiguration>
   <CORSRule>
       <AllowedOrigin>http://www.a.com</AllowedOrigin>
       <AllowedMethod>*</AllowedMethod>
       <AllowedHeader>*</AllowedHeader>
   </CORSRule>
</CORSConfiguration>
  • 重新执行 curl -i -H "Origin:http://www.a.com" "http://mtmss.com/v1/mss_9698a7d474484149970fe7f751eb4fc6/cors-test/test.txt"
HTTP/1.1 200 OK
Server: Tengine
Date: Fri, 24 Nov 2017 06:26:06 GMT
Content-Type: text/plain
Content-Length: 12
Connection: keep-alive
Access-Control-Allow-Origin: http://www.a.com
ETag: "25a514f4c216f30bdfe1f8a71f1a5882"
Last-Modified: Fri, 24 Nov 2017 06:10:17 GMT
X-Amz-Id-2: ab9f6e23723d423c026c74a02003454f
X-Amz-Request-Id: 1511504766214314
X-Ms-Trans-Id: 34316b4a9e8b747113019eb526974a3c-1511504766215266
X-Mss-Trace-Id: 310199030467814662
x-amz-meta-s3cmd-attrs: uid:551/gname:chenghua/uname:chenghua/gid:551/mode:33204/mtime:1511503806/atime:1511503806/md5:25a514f4c216f30bdfe1f8a71f1a5882/ctime:1511503806

hello, mss!