文章目录
  1. 1. 阿里云文档
  2. 2. 开发步骤
    1. 2.1. 问题描述
  3. 3. 总结下

阿里云文档

开发CloudClient的阿里云上传下载模块,本来以为像listBucket可以愉快的使用阿里云的各种封装好的API,没想到刚刚开始开发下载文件就遇到了问题。阿里云并没有提供具体的根据用户名密码列举该用户名下所有Bucket的方法。

通过阅读其官网描述的API,发现可以基于RESTful的架构自己向阿里云服务器发送请求,从响应消息中解析所需要的信息。列举所以用户名下所有Bucket的方式是进行GetService操作需要构造一个格式如下的Http请求报文

1
2
3
4
GET / HTTP/1.1
Host: oss.aliyuncs.com
Date: GMT Date
Authorization: SignatureValue

其他的都不难理解,就唯独其中的签名Authorization: SignatureValue的生成让人费解,光是看阿里云提供的签名生成方法就花了很大精力。

1
2
3
4
5
6
7
8
9
"Authorization: OSS " + Access Key Id + ":" + Signature

Signature = base64(hmac-sha1(AccessKeySecret,
VERB + "\n"
+ CONTENT-MD5 + "\n"
+ CONTENT-TYPE + "\n"
+ DATE + "\n"
+ CanonicalizedOSSHeaders
+ CanonicalizedResource))

花了很长时间研究CONTENT-MD5、CONTENT-TYPE到底是什么,还有给出的例子中明明是先用RFC 2104中定义的HMAC-SHA1方法以Access Key Secret为密钥加密然后再BASE64编码成字符串就可以了,但在下面的Python实现中还用到digest方法计算加密后数据的MD5,到底那个是正确的让人很迷惑。

1
2
3
4
5
6
import base64
import hmac
import sha
h = hmac.new("OtxrzxIsfpFjA7SwPzILwy8Bw21TLhquhboDYROV",
"PUT\nODBGOERFMDMzQTczRUY3NUE3NzA5QzdFNUYzMDQxNEM=\ntext/html\nThu, 17 Nov 2005 18:49:58 GMT\nx-oss-magic:abracadabra\nx-oss-meta-author:foo@bar.com\n/oss-example/nelson", sha)
base64.encodestring(h.digest()).strip()

开发步骤

综上所述,在花了很长时间理解阿里云的文档之后,开始实现getService服务。第一步,需要通过HttpClient构建一个HttpGet消息。

1
2
3
4
5
6
7
8
9
10
HttpClient httpClient = new DefaultHttpClient();
String host = "oss.aliyuncs.com";

URI url = URIUtils.createURI("http", host, -1, "/", null, null);//ublic static URI createURI(String scheme,String host,int port,String path,String query,String fragment)
LogUtil.i("QueryAliyun:sendHttpGet", url+"");

HttpGet httpGet = new HttpGet(url);
httpGet.addHeader("Date", getSystemTime());
httpGet.addHeader("Host","oss.aliyuncs.com");
httpGet.addHeader("Authorization", getSignature());

问题描述

第二步,需要按照文档中描述的方式对本次请求进行加密。问题就出现在这一步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private String getSignature(){
String encryptText = "GET" + "\n"
+ "" + "\n"
+ "" + "\n"
+ getSystemTime() + "\n"
+ ""
+ "/";

String signature = "";
try {
String signatureTemp = ClientUtils.HmacSHA1Encrypt(InfoContainer.ALIYUN_SCRECT_ID, encryptText);

signature = Base64.encodeToString(signatureTemp.getByte(), Base64.DEFAULT).trim();

} catch (Exception e) {
LogUtil.e("QueryAliyun:getSignature", " catch exception in HmacSHA1Encrypt");
e.printStackTrace();
}
String authorization = "OSS " + InfoContainer.ALIYUN_ACCESS_ID + ":" + signature;
return authorization;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**  
* 使用 HMAC-SHA1 签名方法对对encryptText进行签名
* @param encryptText 被签名的字符串
* @param encryptKey 密钥
* @return 前面后的byte数组
* @throws Exception
*/

public static String HmacSHA1Encrypt(String encryptKey,String encryptText) throws Exception
{

String macMethod = "HmacSHA1";
String encoding = "UTF-8";
byte[] keyBytes=encryptKey.getBytes(encoding);
SecretKey secretKey = new SecretKeySpec(keyBytes, macMethod);//根据给定的字节数组构造一个密钥,第二参数指定一个密钥算法的名称
Mac mac = Mac.getInstance(macMethod);//生成一个指定 Mac算法 的 Mac对象
mac.init(secretKey);//用给定密钥初始化 Mac 对象
byte[] text = encryptText.getBytes(encoding);
return new String(mac.doFinal(text));
}

我将要加密的文本按照指定的算法进行加密,返回加密结果字符串,再使用BASE64方式编码,与服务器通信后始终是403错误。研究了很久,发现使用阿里云提供的方法对同样的内容进行签名操作就可以通信,于是我将该方法的结果进行BASE64解码,发现解码的结果和使用ClientUtils.HmacSHA1Encrypt方法放回的字符串一样。根据这个结论进一步研究猜测问题出现在signatureTemp.getByte()这个方法上,我将加密的结果作为byte[]返回,而不是先转换成字符串在装换成byte数组,再进行BASE64编码就可以正常通信了。

这让我怀疑通过new String()得到的字符串,再进行getByte[]操作无法得到原有的byte数组。因为new String和getByte方法在不指定编码方式的情况下都是使用当前环境的编码方式,这里是UTF-8,于是我使用了下面的方法进行测试

1
2
3
4
5
6
7
8
9
private void testGetbyte(byte[] signatureTemp) {
String testString;
try {
testString = new String(signatureTemp,"UTF-8");
byte[] testBytes = testString.getBytes("UTF-8");
LogUtil.i("QueryAliyun:testGetbyte", signatureTemp + " and " + testBytes);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}

发现在utf-8的编码规则下,将byte数组装换成字符串在将字符串转换成byte数组,源数组不等于操作过后的数组。进一步研究,发现原因是因为UTF-8编码方式并不是采用单字节的编码方式进行转换,如果将这里的编码方式改成ISO-8859-1,源数组就和操作后的得到的数组一致了。这个坑也是导致我在这个地方花费了很长时间。

第三步,解析返回的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
try {
HttpResponse httpResponse = httpClient.execute(httpGet);
if (httpResponse.getStatusLine().getStatusCode() == 200) {

HttpEntity entity = httpResponse.getEntity();
String result = EntityUtils.toString(entity,"UTF-8");
LogUtil.i("alyunQuery:sendHttpGet", result);
parseXMLWithPull(result);
}
else {
LogUtil.e("alyunQuery:sendHttpGet", "response wrong stauts code:"+ httpResponse.getStatusLine().getStatusCode());
}
} catch (Exception e) {
e.printStackTrace();
}

首先,校验状态码是否正常为200,接着取出相应消息中的响应体,这里的相应体内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<ListAllMyBucketsResult>
<Owner>
<ID>1173032044021568</ID>
<DisplayName>1173032044021568</DisplayName>
</Owner>
<Buckets>
<Bucket>
<Location>oss-cn-beijing</Location>
<Name>njuptjsy</Name>
<CreationDate>2015-05-23T04:06:57.000Z</CreationDate>
</Bucket>
</Buckets>
</ListAllMyBucketsResult>

可以看到服务器发回的消息中包含在Buckets标签,其中用每个子标签bucket包含了相应的bucket的位置、名称、和创建时间信息。下面只需要将开放需要的名称信息提取出来保存在列表中即可。Android对xml的解析在Android中一共有三种方法:
1.SAX解析XML文件采用的是事件驱动,也就是说,他读取每个标签并不需要解析完整个文档,在按内容顺序解析文档的过程中,SAX会判断当前读取到的字符是否符合XML语法中的某部分,如果符合就会触发事件,其实就是一些回调方法,然后进行判断处理。其优点是解析速度快,占用内存少,适用于Android等移动设备,缺点是对于嵌套多个分支的XML文件来说处理不是很方便。

2.DOM解析XML文件时,会将XML文件的所有内容以文档树方式存放在内存中,然后使用DOM API遍历XML树,检索所需的数据,主要用于PC机。其优点是使用DOM解析XML的代码比较直观,相比于基于SAX的实现更加简单,缺点是须将XML文件所有内容存放在内存中,所以消耗内存大,不适用Android等移动设备。

3.Pull解析器是Android内置解析XML文件的解析器,运行方式类似于SAX解析,只是产生的事件是一个数字,而非方法,因此可以使用一个switch对感兴趣的事件进行处理。Pull解析器对节点处理比较好,同样也很省内存,官方推挤使用Pull解析器解析XML文件,而且Android系统本身用到的XML文件内部也是使用Pull解析器进行解析的。这里选用的Pull方法进行解析

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
private void parseXMLWithPull(String result) {
try {
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
XmlPullParser xmlPullParser = factory.newPullParser();
xmlPullParser.setInput(new StringReader(result));
int eventType = xmlPullParser.getEventType();

String name = "";
String location = "";
bucketNames = new ArrayList<String>();
while (eventType != XmlPullParser.END_DOCUMENT) {
String nodeName = xmlPullParser.getName();
switch (eventType) {
case XmlPullParser.START_TAG:
if ("Location".equals(nodeName)) {
location = xmlPullParser.nextText();
}
else if ("Name".equals(nodeName)){
name = xmlPullParser.nextText();
}
break;
case XmlPullParser.END_TAG:
if ("Bucket".equals(nodeName)) {
bucketNames.add(name);
LogUtil.i("alyunQuery:parseXMLWithPull", "get name :" + name);
}
break;
default:
break;
}
eventType = xmlPullParser.next();
}
} catch (Exception e) {
e.printStackTrace();
LogUtil.e("alyunQuery:parseXMLWithPull", "parse XML error");
}
}

在parseXMLWithPull函数中,通过工程方法生成xmlPullParser对象对传入的带解析的流进行解析。pull解析在遇到不同的XML标签时会触发不同的事件,如这里用到的遇到标签的起始事件XmlPullParser.START_TAG、遇到标签的结束事件XmlPullParser.END_TAG和遇到XML文件结束标签事件XmlPullParser.END_DOCUMENT。

总结下

这里虽然ISO-8859-1来编码可以解码出原来的byte数组,但是使用不管什么内容,都用new String(…,”ISO-8859-1”)来建立字符串,然后使用的时候按默认的编码格式(通常在服务器上都是英文系统)输出字符串。这样其实你使用的String并不是按UNICODE来代表真正的字符,而是强行把BYTE数组复制到String的char[]里,一旦你的运行环境改变,你就被迫要修改一大堆的代码。而且也无法在同一个字符串里处理几种不同编码的文字。
另一个是把一种编码格式的字符串,比如是GB2312,转换成另一种格式的字符串,比如UTF-8,然后不指明是UTF-8编码,而直接用new String(…)来建立String,这样放在String里面的字符也是无法确定的,它在不同的系统上代表不同的字符。如果要求别人用“UTF-8格式”的String来交换信息的时候,其实已经破坏了JAVA为了兼容各种语言所做的规定。这种错误的本质思想是还按写C语言的方式,把字符串纯粹当作可以自己自由编码的存储器使用,而忽略了JAVA字符串只有一种编码格式。如果真的想自由编码,用byte[]或者char[]就完全了解决问题的了。

这里不得不吐槽下阿里云的Android API,对各项操作的封装不全也就算了,文档中几乎没有说明性的描述,绝大部分函数都没有任何的说明的文字,传入的形参和返回值也只告诉你类型,使用基本靠猜。即使对自己的命名再有信心,几句说明还是应该有的,具体可以看看AWS的API。
image