這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
原創,轉載請註明:http://www.jianshu.com/p/a6a8c3c2cead
一、開篇說明:
以下思考方向,是以Android端為出發點(IOS同理)
AWS:Amazon Web Services (亞馬遜雲端服務)
AWS s3 API文檔:https://aws.amazon.com/cn/documentation/s3/
Minio :(具體的解釋自行百度吧)一個基於 golang 語言開發的 AWS S3 儲存協議的開源實現,並附帶 web ui 介面,可以通過 Minio 搭建私人的相容 AWS S3 協議的儲存伺服器。
二、需求分析
項目需求:最近公司需要搭建一個檔案伺服器,讓移動端(Android、ios)用來儲存圖片等檔案。這個檔案伺服器後台使用minio搭建的。由於minio是基於AWS S3 儲存協議,所以我們移動端也需要實現相同的協議來上傳。移動端,我們確定了三種可行方案(方案優劣僅是基於對APK打包大小的影響程度來確定的):
方案一:基於AWS S3 儲存協議自己實現檔案上傳(最佳方案,最後項目引入檔案最小----大約100K,不影響apk大小)
方案二:使用AWS SDK 實現檔案上傳(中間方案,需要引入項目的jar包1.5M檔案略大)
方案三:使用minio SDK 實現檔案上傳(最差方案,需要引入6M jar包)
三、代碼實現
由於項目需求不同,供大家自行選擇,我將這三種方案實現一一說明。
方案三:
demo下載:https://github.com/Nergal1/minio-demo
1.Android端引入minio SDK jar包會和原生的發生衝突,會報一些安全錯誤。
引入時,去除衝突的包,com.fasterxml.jackson.core和com.google.code.findbugs:
dependencies {
compile ('io.minio:minio:3.0.4'){
excludegroup:'com.fasterxml.jackson.core'
excludegroup:'com.google.code.findbugs'
}
}
2.上傳代碼:
/*上傳圖片
bucketName:服務端隱藏檔夾,必須先建立
objectName:服務端隱藏檔名
inputStream:上傳檔案流
*/
minioClient.putObject(bucketName,objectName,inputStream,inputStream.available(),"application/octet-stream");
提醒:別忘了開啟網路許可權
3.簡易源碼流程圖,數字代表行號:
minio上傳源碼簡易流程圖
方案二:
demo下載:https://github.com/Nergal1/AWS-S3-demo
1.由於AWS SDK源碼中是開啟Service,然後建立線程池實現非同步上傳、下載功能。
資訊清單檔要註冊service:
<service android:name="com.amazonaws.mobileconnectors.s3.transferutility.TransferService"
android:enabled="true"/>
許可權:
android.permission.INTERNET
android.permission.ACCESS_NETWORK_STATE
android.permission.READ_EXTERNAL_STORAGE
android.permission.WRITE_EXTERNAL_STORAGE
Android 6.0+檔案讀寫權限需要代碼中即時擷取,demo中未作相容。如有檔案問題,請自行添加。
2.引入jar包:
aws-android-sdk-core-2.4.2.jar
aws-android-sdk-s3-2.4.2.jar
3.先配置Constants.java中的ENDPOINT、ACCESSKEY、SecretKey、BUCKET_NAME
上傳代碼見UploadActivity.java(下載請自行查看DownloadActivity):
TransferUtility transferUtility= Util.getTransferUtility(this);
//上傳
TransferObserver observer =transferUtility.upload(Constants.BUCKET_NAME,file.getName(),
file);
//設定上傳監聽
observer.setTransferListener(new UploadListener());
//監聽類
private class UploadListener implements TransferListener {
// Simply updates the UI list when notified. 上傳失敗監聽
@Override
public voidonError(intid,Exception e) {
Log.e(TAG,"Error during upload: "+ id,e);
updateList();
}
//上傳進度監聽
@Override
public voidonProgressChanged(intid, longbytesCurrent, longbytesTotal) {
Log.d(TAG,String.format("onProgressChanged: %d, total: %d, current: %d",
id,bytesTotal,bytesCurrent));
updateList();
}
//上傳狀態監聽
@Override
public voidonStateChanged(intid,TransferState newState) {
Log.d(TAG,"onStateChanged: "+ id +", "+ newState);//例如:UploadActivity: onStateChanged: 9, FAILD 失敗狀態
// //根據id 查詢檔案路徑
// TransferObserver transferObserver = transferUtility.getTransferById(id);
// Log.d(TAG, "檔案地址---"+transferObserver.getAbsoluteFilePath());
updateList();
}
}
4.源碼簡易流程圖,數字代表行號:
aws sdk上傳源碼簡易流程圖
方案三:
demo下載:https://github.com/Nergal1/nergal-AWS-demo
1.添加許可權:
android.permission.INTERNET
android.permission.ACCESS_NETWORK_STATE
android.permission.READ_EXTERNAL_STORAGE
android.permission.WRITE_EXTERNAL_STORAGE
Android 6.0+檔案讀寫權限需要代碼中即時擷取,demo中已相容。
2.配置Constants.java中的ENDPOINT、ACCESSKEY、SecretKey、BUCKET_NAME、BUCKET_REGION
上傳代碼:
AWSTransferUtility utility = AWSTransferUtility.getInstance();
//***********檔案上傳,先設定監聽,後上傳檔案*************若需要用其他網路架構實現上傳,請自行實現BaseHttpClient,然後utility.setHttpClient
utility.setUploadListener(new MAWSUploadListener()).upload(file,Constants.BUCKET_NAME,file.getName());
//監聽類實現如下
/**
*@author zhangchen
*@date 2017/5/24 上午8:32
*@Description 檔案上傳監聽方法
*/
class MAWSUploadListener implements AWSUploadListener {
@Override
public voidonComplite(File file) {
System.out.println("onComplite---"+ file.getAbsolutePath());
}
@Override
public voidonError(File file,Throwable e) {
System.out.println("onError---file--"+ file.getAbsolutePath());
System.out.println("onError---Exception--"+ e.toString());
}
@Override
public voidonProgressChanged(File file, longbytesCurrent, longbytesTotal) {
System.out.println("onProgressChanged----"+ bytesCurrent);
}
}
3.哈哈,囉嗦了一大堆,終於到重點了,實現原理分析:
實際上AWS S3 儲存協議只不過是添加一些跟服務端協商的要求標頭,要求標頭的value是用AWS s3 V4簽名演算法算出來的。所以,上傳時帶上指定的要求標頭,以及合法的簽名演算法,其他地方跟普通上傳沒區別。服務端拿到要求標頭後,根據合法的簽名演算法,計算簽名值,然後跟用戶端要求標頭裡的簽名進行校正,成功後,服務端才允許上傳。
首先我們要解決兩個問題:
1.指定的要求標頭有哪些?
首先我們來看一個檔案上傳的請求:
--------------------------要求標頭--------------------------
PUT /test/IMG_20170519_165644_1.jpg HTTP/1.1
Content-MD5: 6PN5Zj06z+D5UkSG1ZxhNA==
x-amz-decoded-content-length: 2566214
x-amz-content-sha256: STREAMING-AWS4-HMAC-SHA256-PAYLOAD
Content-Type: image/jpeg
X-Amz-Date: 20170522T024116Z
User-Agent: aws-sdk-android/2.4.2 Linux/3.10.86-g6be8ceb Dalvik/2.1.0/0 zh_CN TransferService/2.4.2
aws-sdk-retry: 0/0
Accept-Encoding: identity
aws-sdk-invocation-id: 148858f7-e319-4578-b9f8-1b220f6af380
Authorization: AWS4-HMAC-SHA256 Credential=1NA5K80UU85NMPK4BPEW/20170522/us-east-1/s3/aws4_request,SignedHeaders=content-md5;host;x-amz-content-sha256;x-amz-date;x-amz-decoded-content-length,Signature=db4e0abd4290730ed6fd27867d2fa342942372b88f1b5ad49b113ab9c77d6cc9
Content-Length: 2568100
Host: fsst.anbanggroup.com
Connection: Keep-Alive
--------------------------------------請求體--------------
20000;chunk-signature=0cfa38451a889b866dbf43907ce3a72ee85f6b176168fb875903e8353366628e
。。。二進位檔案流。。。
-------------------------------------------
一大堆要求標頭,實際上根據 S3 API 文檔,Authorization要求標頭才是必要的
而Authorization的value中SignedHeaders是規定了使用哪些要求標頭來計算本次請求的簽名值。也就是說content-md5;host;x-amz-content-sha256;x-amz-date;x-amz-decoded-content-length這些要求標頭計算出來Signature的值。根據S3 API 文檔,SignedHeaders中host;x-amz-content-sha256;x-amz-date是必須存在的。
那為什麼本次請求還要加上content-md5,x-amz-decoded-content-length這兩個值?
我們來想想簽名的作用:
a.驗證請求身份
ACCESSKEY、SecretKey就是用來驗證身份的,這兩個參數也是計算Authorization的value中Signature的值所必須的。
b.防止篡改
SignedHeaders就是用來設定防止篡改的要求標頭的,content-md5是防止篡改請求內容(body),x-amz-decoded-content-length防止篡改請求內容長度的。
官方文檔中,SignedHeaders必須的host;x-amz-content-sha256;x-amz-date這幾個也就可以理解了,防止篡改請求地址,請求內容的sha256值,請求時間戳記
c.防止請求籤名被盜用
根據AWS S3 儲存協議,時間戳記(x-amz-date或Date要求標頭)15分鐘內有效,沒有許可權的使用者t通過截獲已簽名的request,可以篡改SignedHeaders中沒有包含的部分,所以官方建議,簽名所有要求標頭和請求體(也就是說SignedHeaders要盡量包含所有),還有最好用Https.
總結:囉嗦一大堆,必要的要求標頭有哪些:Authorization、SignedHeaders中包含的(必須包含的有host;x-amz-content-sha256;x-amz-date),host,x-amz-content-sha256,x-amz-date這些要求標頭是服務端校正簽名必須的。
2.簽名演算法是如何計算的?(即Authorization中的Signature)
有兩種簽名演算法:
第一種,單塊傳輸(本人demo中使用的這種方式)
檔案記憶體讀取兩次(一次計算檔案sha256時,一次檔案上傳時)
單塊上傳要求標頭:
x-amz-content-sha256: 32e820d03db121caf97206f8cbcc6202cf25bf59246e8d6b0e8f6e3502d68f66
或:x-amz-content-sha256: UNSIGNED-PAYLOAD(這種是單塊不進行簽名內容的寫法)
a.
單塊上傳簽名計算流程
功能函數說明:
Lowercase():字串轉成小寫
Hex():base 16編碼(全部小寫)
SHA256Hash():計算請求體body(上傳檔案時,body中只有檔案,即計算檔案的SHA256)的SHA256,然後base64
HMAC-SHA256():通過將key使用SHA256計算 HMAC
java方法:
public static byte[]sumHmac(byte[] key, byte[] data)
throwsNoSuchAlgorithmException,InvalidKeyException {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(newSecretKeySpec(key,"HmacSHA256"));
mac.update(data);
returnmac.doFinal();
}
Trim():去空格
UriEncode():
a.保持'A'-'Z','a'-'z', '0'-'9', '-', '.', '_', '~'不變
b.空白字元必須轉成"%20" (而不是 "+")
c.byte編譯成“%”後面跟兩位的十六進位(字母大寫)
d.如果檔案名稱(object name)類似“photos/Jan/sample.jpg”,其中包含“/”,不進行轉義。
e.方法實現:
public static String UriEncode(CharSequence input, boolean
encodeSlash) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < input.length(); i++) {
char ch = input.charAt(i);
if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a'
&& ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '_' ||
ch == '-' || ch == '~' || ch == '.') {
result.append(ch);
} else if (ch == '/') {
result.append(encodeSlash ? "%2F" : ch);
} else {
result.append(toHexUTF8(ch));
}
}
return result.toString();
}
第二種,多塊傳輸
多塊上傳要求標頭:
x-amz-content-sha256: STREAMING-AWS4-HMAC-SHA256-PAYLOAD
把有效荷載(請求body)分成幾塊,固定或可變大小的塊。通過上傳塊,避免讀取整個檔案的有效荷載來計算簽名.
a.第一塊,計算所有的要求標頭的簽名,空body請求
b.第二塊,將第一個塊的簽名和有效載荷一起計算簽名
c.第n塊,將第n-1塊的簽名和有效載荷一起計算簽名
d.最後,發送0 bytes的塊包含第n塊的簽名。
要求標頭Authorization中的Signature計算同單塊上傳:
多塊上傳簽名計算流程圖
請求體中塊簽名計算:
請求體中比單塊上傳多了這些
chunk-signature的簽名計算流程圖
終於,說完了,第一次寫這麼長,講的有不清楚的多多包涵,有什麼問題可以給我留言,也可以發我郵箱18514689920@163.com.