Implement of Upload Files to QiNiu Server

Understand the concept, read the official documents carefully, based on common sense thinking, combined with the reference sample, keep on deliberate practice.

项目需求

功能要求:使用第三方开源库Fast-Android-Networking实现从七牛服务器快速下载和上传文件;
应用场景:快速完成/取消文件下载/上传,支持回调以更新进度等相关UI。

设计及实现

Fast-Android-Networking第三方库

Fast-Android-Networking

关键代码

下载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
String realDownFileURL = auth.privateDownloadUrl(QINIU_DOWNLOAD_PRIVATE_TEST_FILE);
AndroidNetworking.download(realDownFileURL,//"http://om845dfwl.bkt.clouddn.com/smile.mp3"
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath(),
"fast android network test.jpg")
.setTag("downloadTest")
.setPriority(Priority.MEDIUM)
.build()
.setDownloadProgressListener(new DownloadProgressListener() {
@Override
public void onProgress(long bytesDownloaded, long totalBytes) {
// do anything with progress
int progress = (int)(bytesDownloaded/totalBytes);
progressBar.setProgress(progress);
notificationManager.notify(NOTIFICATION_ID_DOWNLOAD,getNotification("Downloading...",
"fast android network test.jpg",progress));
}
})
.startDownload(new DownloadListener() {
@Override
public void onDownloadComplete() {
// do anything after completion
notificationManager.notify(NOTIFICATION_ID_DOWNLOAD,
getNotification("Download Success","fast android network test.jpg", 0));
progressBar.setProgress(100);
}
@Override
public void onError(ANError error) {
// handle error
notificationManager.notify(NOTIFICATION_ID_DOWNLOAD,
getNotification("Download Failed","fast android network test.jpg", -1));
progressBar.setProgress(0);
}
});

上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
String uploadPath =Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath()+"/smile.mp3";
File uploadFile=new File(uploadPath);
QiNiuAuth auth = create(ACCESS_KEY, SECRET_KEY);
String token = auth.uploadToken(BUCKET);
AndroidNetworking.upload(QI_NIU_UPLOAD_URL)//"http://upload.qiniu.com/"
.addMultipartFile("file",uploadFile)
.addMultipartParameter("token",token)
.addMultipartParameter("key","2017/3/26/smile.mp3")
.setTag("uploadTest")
.setPriority(Priority.HIGH)
.build()
.setUploadProgressListener(new UploadProgressListener() {
@Override
public void onProgress(long bytesUploaded, long totalBytes) {
// do anything with progress
long lTotalBytes = uploadFile.length();
int progress = (int)((bytesUploaded*100)/lTotalBytes);
progressBar.setProgress(progress);
notificationManager.notify(NOTIFICATION_ID_DOWNLOAD,
getNotification("Uploading...","smile.mp3",progress));
}
})
.getAsJSONObject(new JSONObjectRequestListener() {
@Override
public void onResponse(JSONObject response) {
// do anything with response
textView.setText(response.toString());
}
@Override
public void onError(ANError error) {
// handle error
notificationManager.notify(NOTIFICATION_ID_DOWNLOAD,
getNotification("Upload Failed","smile.mp3", -1));
progressBar.setProgress(0);
}
});

七牛存储基本概念

资源

资源是七牛云存储服务中的逻辑存储单元。对于每一个账号,该账号里存放的每个资源都有唯一的 空间 与 键(Key) 标识。资源键名是一个字符串,例如:level1/level2/example1.jpg,它可以包含任意字符(包括 UTF-8 编码形式的 Unicode 字符)。

使用者可以在上传资源时为其指定一个方便管理的键名,通过前缀来达到类似于文件目录的分类和层次效果。例如对于一个网站的资源,我们可以用如下键名命名资源:

1
2
3
4
index.html
features/index.html
imgs/features/feature1.png
about.html

空间

空间是资源的组织管理单位,一个资源必然位于某个空间中。可以为每个空间设置一系列的属性,以对资源提供合理的管理动作。 常见的属性有:

  1. 将空间设置为公开或私有,以控制对空间内资源的访问权限。
  2. 设置资源的数据处理样式,以便于用简短方式对资源进行处理。

开发者和用户

开发者是七牛云存储服务的直接使用者,用户是开发者所推出产品的使用者即七牛云存储服务的间接使用者。

键值对

键值对 (Key-Value) 是一个常用的数据结构概念,通常又称为字典 (Dictionary) 或映射 (Map)。每个存放到该数据结构中的值 (Value) 都对应一个全局唯一的键 (Key)。该数据结构的特征是以空间换时间,通过键查询值通常是比较快速的过程。

存储区域

存储区域是在创建空间时指定的,一旦指定后就不允许修改。存储区域表示七牛云对象存储服务的数据中心所在区域,物理位置。您可以根据费用、请求来源等综合考虑选择合适的存储区域。一般来说,选择离您近的存储区域访问速度更快。

存储区域是数个存储机房的逻辑联合体,提供一组独立的具体访问域名,将流量导向最适合的存储机房。

七牛云对象存储目前提供四个存储区域:华东、华北、华南、北美,您可以根据实际需求选用,以获取最高访问性能和最佳用户体验。

注意:不同存储区域的数据并非互为备份副本,比如存储在华东的文件,不能通过华北访问域名访问。

访问密钥 (AK/SK)

Access Key 与 Secret Key 是七牛颁发的一对密钥,用于对操作请求进行授权签名。

  1. 用户凭证 (Access Key) 简称 AK ,是七牛云存储颁发给用户的标识。用户将用户凭证放入访问请求,以便七牛云存储识别访问者的身份。
  2. 签名密钥 (Secret Key) 简称 SK ,是七牛云存储颁发给用户,用于对访问请求签名的字串。用户使用签名密钥对访问请求的核心要素进行签名,获得请求认证 令牌。用户将令牌随同访问请求一起发送至七牛云存储服务,七牛云存储将对令牌进行校验,以确认用户请求的合法性。
    用户凭证和签名密钥成对颁发,不会重复。一个用户可以拥有两对用户凭证和签名密钥,用于不同的访问。

存储机房

存储机房是部署了一套完整的七牛云存储集群的单一数据中心机房,每个机房均配备多个 IP 作为上传和下载入口,并有对应的访问域名指向这些 IP。

编程模型

七牛云存储服务是以键值对方式提供非结构化资源存储服务。向业务服务器提供资源管理服务,向客户端提供资源上传和下载服务。

关键的几个交互过程:

上传

客户端在上传资源到七牛云存储之前要先从业务服务器获取一个有效的上传凭证,因此需要先后和两个服务端打交道。

如果有设置回调,则上传完成时七牛云存储会自动发起回调到指定的业务服务器。

下载

公开资源不需要对应的下载凭证,客户端可以直接从七牛云存储下载对应资源。私有资源需要对应的下载凭证,因此必须先和业务服务器打交道。

按照实际的使用场景,客户端对于内容的展示非常类似一个动态网页的生成过程,因此无论该页面内容是公开还是私有,均需要从业务服务器获取展示该页面的动态布局信息。所以通常显示过程也是需要先后和业务服务器及七牛云存储服务打交道。

关键原则

这个模型的关键点如下:

  1. 整个架构中需要一个业务服务器组件。
  2. 无论如何,访问密钥(AK/SK)均不得包含在客户端的分发包中(如二进制代码、配置文件或网页中)。
  3. SecretKey不得在任何场景中的公网上传输,更不得传输到客户端。
  4. 业务服务器端应维持一个用于管理资源元数据的数据库和一个用于管理最终用户账号信息的数据库。
  5. 原则上客户端和七牛云存储之间的交互只有上传和下载,不应使用任何其他的API。

安全机制

数据安全性是云存储服务的重中之重。云存储的安全机制主要需要考虑以下几个因素:

如何判断该请求方是否合法,且对目标空间有相应的访问权限。
因为服务的访问协议同时支持 HTTP 和 HTTPS,服务端需要判断收到的请求是否经过篡改。
相比上传新资源,覆盖文件或删除已有资源拥有更高的风险。因此对上传或修改动作,需要确认请求方是否拥有修改或删除的权限。
在使用七牛云存储服务的过程中,需要考虑安全机制的场景主要有如下几种:

上传资源
访问资源
管理和修改资源
这三个场景需要考虑不同的安全因素,因此七牛针对性的提供了三种安全机制:上传凭证下载凭证管理凭证

因为凭证的生成需要用到SecretKey,因此该生成动作不应在不受信任的环境中进行。需要注意的是,开发者绝不能将密钥包含在分发给最终用户的程序中,无论是包含在配置文件中还是二进制文件中都会带来非常大的密钥泄漏风险。

推荐的模型如下所示:

上传凭证(UploadToken)
客户端上传前需要先获取从服务端颁发的上传凭证,并在上传资源时将上传凭证包含为请求内容的一部分。不带凭证或带非法凭证的请求将返回 HTTP 错误码 401,代表认证失败。

生成上传凭证时需要指定以下要素:

权限,指定上传的目标空间或允许覆盖的指定资源。
凭证有效期,即一个符合Unix时间戳规范的数值,单位为秒。
注意: 因为时间戳的创建和验证在不同的服务端进行(在业务服务器创建,在云存储服务器验证),因此开发者的业务服务器需要尽可能校准时间,否则可能出现凭证刚创建就过期等各种奇怪的问题。
可选择设置的最终用户标识 ID。这是为了让业务服务器在收到结果回调时能够识别产生该请求的最终用户信息。

我们使用上传策略 (PutPolicy)保存和传递这些设置。关于上传策略和上传凭证的生成细节,请查阅上传凭证。

下载凭证(DownloadToken)
下载私有资源的请求需要带一个合法的下载凭证。不带凭证或带非法凭证的请求将返回 HTTP 错误码 401,代表认证失败。

与上传凭证相比,下载凭证的作用比较简单:

保证请求发起者拥有对目标空间的访问权限。
保证服务端收到的下载请求内容未经中途篡改,具体包括目标资源的 URI 和该访问请求的有效期信息均应未受到篡改。
关于下载凭证的生成细节,请查阅下载凭证。

防盗链
下载还有一种常见的场景,即公开资源的防盗链,例如禁止特定来源域名的访问,禁止非浏览器发起的访问等。

我们可以通过 HTTP 协议支持的 Referer 机制即HTTP Referer来进行相应的来源识别和管理。

防盗链是一个系统设置,不影响开发工作。如发现有盗链情况,开发者可在七牛开发者平台里的 融合CDN加速 中的 高级配置 进行设置。

管理凭证(AccessToken)

在管理现有资源时,例如查看资源元数据、删除或移动资源等,通常需要带一个合法的管理凭证。不带凭证或带非法凭证的管理请求将返回 HTTP 错误码 401,代表认证失败。

管理凭证的作用与下载凭证比较类似:

保证请求发起者拥有对目标空间的管理权限。
保证服务端收到的管理请求内容未经中途篡改,具体包括代表管理动作的 URI 和该管理动作的参数信息均应未受到篡改。
关于管理凭证的生成细节,请查阅管理凭证。

跨域访问
出于安全的考虑,Web 浏览器从很早之前就定下同域安全策略的标准,默认情况下同一域名下的页面只能向同域(包括 CNAME 域名、端口)下的 URL 发送所有类型的 HTTP 请求。而向不同域的地址发送非 GET 请求时,默认情况下只能返回同域安全策略错误。

对此,在发起上传或下载请求的时候,七牛的服务会返回相应的支持跨域的 Header:

上传(upload.qiniu.com)

1
2
3
Access-Control-Allow-Headers: X-File-Name, X-File-Type, X-File-Size
Access-Control-Allow-Methods: OPTIONS, HEAD, POST
Access-Control-Allow-Origin: *

下载(.qiniudn.com)

1
Access-Control-Allow-Origin: *

上传凭证Token在线生成

Token

算法

1.构造上传策略:
用户根据业务需求,确定上传策略要素,构造出具体的上传策略。例如用户要向空间 my-bucket 上传一个名为 sunflower.jpg 的图片,授权有效期截止到 2015-12-31 00:00:00(该有效期指上传完成后在七牛生成文件的时间,而非上传的开始时间),并且希望得到图片的名称、大小、宽高和校验值。那么相应的上传策略各字段分别为:

1
2
3
4
5
6
7
8
9
scope = 'my-bucket:sunflower.jpg'
deadline = 1451491200
returnBody = '{
"name": $(fname),
"size": $(fsize),
"w": $(imageInfo.width),
"h": $(imageInfo.height),
"hash": $(etag)
}'

2.将上传策略序列化成为JSON:

用户可以使用各种语言的 JSON 库,也可以手工拼接字符串。序列化后,应得到如下形式的字符串(字符串值以外部分不含空格或换行):

1
2
3
4
5
6
7
8
9
10
11
12
13
putPolicy =
'{
"scope": "my-bucket:sunflower.jpg",
"deadline":1451491200,
"returnBody":
"{
\"name\":$(fname),
\"size\":$(fsize),
\"w\":$(imageInfo.width),
\"h\":$(imageInfo.height),
\"hash\":$(etag)
}"
}'

3.对 JSON 编码的上传策略进行URL 安全的 Base64 编码,得到待签名字符串:

1
2
3
4
encodedPutPolicy = urlsafe_base64_encode(putPolicy)
#实际值为:
encodedPutPolicy = "eyJzY29wZSI6Im15LWJ1Y2tldDpzdW5mbG93ZXIuanBnIiwiZGVhZGxpbmUiOjE0NTE0OTEyMDAsInJldHVybkJvZHkiOiJ7XCJuYW1lXCI6JChmbmFtZSksXCJzaXplXCI6JChmc2l6ZSksXCJ3XCI6JChpbWFnZUluZm8ud2lkdGgpLFwiaFwiOiQoaW1hZ2VJbmZvLmhlaWdodCksXCJoYXNoXCI6JChldGFnKX0ifQ=="

4.使用访问密钥(AK/SK)对上一步生成的待签名字符串计算HMAC-SHA1签名:

1
2
3
4
sign = hmac_sha1(encodedPutPolicy, "<SecretKey>")
#假设 SecretKey 为 MY_SECRET_KEY,实际签名为:
sign = "c10e287f2b1e7f547b20a9ebce2aada26ab20ef2"

注意:签名结果是二进制数据,此处输出的是每个字节的十六进制表示,以便核对检查。

5.对签名进行URL安全的Base64编码:

1
2
3
4
encodedSign = urlsafe_base64_encode(sign)
#最终签名值为:
encodedSign = "wQ4ofysef1R7IKnrziqtomqyDvI="

6.将访问密钥(AK/SK)、encodedSign 和 encodedPutPolicy 用英文符号 : 连接起来:

1
2
3
4
uploadToken = AccessKey + ':' + encodedSign + ':' + encodedPutPolicy
#假设用户的 AccessKey 为 MY_ACCESS_KEY ,则最后得到的上传凭证应为:
uploadToken = "MY_ACCESS_KEY:wQ4ofysef1R7IKnrziqtomqyDvI=:eyJzY29wZSI6Im15LWJ1Y2tldDpzdW5mbG93ZXIuanBnIiwiZGVhZGxpbmUiOjE0NTE0OTEyMDAsInJldHVybkJvZHkiOiJ7XCJuYW1lXCI6JChmbmFtZSksXCJzaXplXCI6JChmc2l6ZSksXCJ3XCI6JChpbWFnZUluZm8ud2lkdGgpLFwiaFwiOiQoaW1hZ2VJbmZvLmhlaWdodCksXCJoYXNoXCI6JChldGFnKX0ifQ=="

注意:为确保客户端、业务服务器和七牛服务器对于授权截止时间的理解保持一致,需要同步校准各自的时钟。频繁返回 401 状态码时请先检查时钟同步性与生成 deadline 值的代码逻辑。

上传凭证Token本地生成

Auth.java

踩过的坑

报错”incorrect region, please use up-z2.qiniu.com”

因为空间与上传域名有对应关系,详见访问域名和存储区域

up-z2对应华南
UP HTTP 上传 http://up-z2.qiniu.com
上传源站 HTTP 地址, 适用于服务器端上传
http://upload-z2.qiniu.com
上传 HTTP 加速地址, 适用于客户端上传

up.qiniu.com对应华东
UP HTTP 上传 http://up.qiniu.com
上传源站 HTTP 地址, 适用于服务器端上传
http://upload.qiniu.com
上传 HTTP 加速地址, 适用于客户端上传

直传文件

upload接口,用于在一次 HTTP 会话中上传单一的一个文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<form method="post" action="http://upload.qiniu.com/"
enctype="multipart/form-data">
<input name="key" type="hidden" value="<resource_key>">
<input name="x:<custom_name>" type="hidden" value="<custom_value>">
<input name="token" type="hidden" value="<upload_token>">
<input name="file" type="file" />
<input name="crc32" type="hidden" />
<input name="accept" type="hidden" />
</form>
POST / HTTP/1.1
Host: upload.qiniu.com
Content-Type: multipart/form-data; boundary=<frontier>
Content-Length: <multipartContentLength>
--<frontier>
Content-Disposition: form-data; name="token"
<uploadToken>
--<frontier>
Content-Disposition: form-data; name="key"
<key>
--<frontier>
Content-Disposition: form-data; name="<xVariableName>"
<xVariableValue>
--<frontier>
Content-Disposition: form-data; name="crc32"
<crc32>
--<frontier>
Content-Disposition: form-data; name="accept"
<acceptContentType>
--<frontier>
Content-Disposition: form-data; name="file"; filename="<fileName>"
Content-Type: application/octet-stream
Content-Transfer-Encoding: binary
<fileBinaryData>
--<frontier>--

如何在空间下创建文件夹?

在空间中不能创建文件夹,但是为了区分不同的文件,可以这么做:
文件名以 2017/1/12/1.img , 即创建这样的虚拟目录 2017/1/12/ 做区分。

基本术语

ETag

ETag为HTTP协议的一部分,它是 HTTP 提供web缓存验证机制之一。以这种方式使用etag类似于指纹,他们可以通过快速比较来确定两个资源是否相同。

Exif

Exif 意指可交换图像文件格式,专门为数码相机的照片所设计,可以记录数码照片的属性信息和拍摄数据。Exif 信息是由数码相机在拍摄过程中采集的一系列信息,然后这些信息放置在我们熟知的 JPEG/TIFF 文件的头部。

HTTP Referer

HTTP Referer是header的一部分,当浏览器向web服务器发送请求的时候,一般会带上Referer,告诉服务器我是从哪个页面链接过来的,服务器藉此可以获得一些信息用于处理。

HMAC-SHA1

HMAC是哈希运算消息认证码 (Hash-based Message Authentication Code),HMAC运算利用哈希算法,以一个密钥和一个消息为输入,生成一个消息摘要作为输出。您可以使用它同时验证数据的完整性和消息的真实性。HMAC-SHA1签名算法是一种常用的签名算法,用于对一段信息进行生成签名摘要。

键 (key)

空间中资源的唯一标识符。空间中的每个资源都有一个对应的键。因为空间和键一起唯一标识每一个资源,您可以认为七牛云存储是基于空间+键和资源本身之间的基本数据映射。

例如:http://78re52.com1.z0.glb.clouddn.com/resource/AllEast.jpg

78re52.com1.z0.glb.clouddn.com是空间的绑定域名
resource/AllEast.jpg是键

签名密钥

签名密钥 (Secret Key) 简称 SK ,是七牛云存储颁发给用户,用于对访问请求签名的字串。用户使用签名密钥对访问请求的核心要素进行签名,获得请求认证令牌。用户将令牌随同访问请求一起发送至七牛云存储服务,七牛云存储将对令牌进行校验,以确认用户请求的合法性。

JSON

JSON(JavaScript Object Notation)一种轻量级的数据交换格式,具有良好的可读和便于快速编写的特性。可在不同平台之间进行数据交换。JSON采用兼容性很高的、完全独立于语言文本格式,同时也具备类似于C语言的习惯(包括C, C++, C#, Java, JavaScript, Perl, Python等)体系的行为。这些特性使JSON成为理想的数据交换语言。

令牌 (Token)

令牌是用户访问七牛云存储时,进行身份验证的凭证。当用户将一个Bucket设置为私有后,在访问七牛云存储时,必须通过身份验证。用户将访问请求中的一些要素整合起来,用签名密钥对其加密,得到令牌。然后将令牌随同请求一起发送至七牛云存储。用户可以在令牌中指定请求的时效(七牛云存储统一使用UTC时间计算令牌有效期),防止请求被非法使用。

Unix 时间戳

Unix时间戳,是一种时间表示方式,定义为从1970年01月01日00时00分00秒起至现在的总秒数。

参考资料

Android SDK
Java SDK
七牛云上传下载操作指南
七牛云存储必备知识.pdf
七牛云存储文件上传-基础上传服务.pdf