阿里云文档
开发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); 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.init(secretKey); 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](https://raw.githubusercontent.com/njuptjsy/mobliecloudapp/master/CloudClient/uml/%E9%98%BF%E9%87%8C%E4%BA%91androidAPI%E6%88%AA%E5%9B%BE.png)