基于阿里云对象存储的文件直传+图片处理的手机APP搭建

移动互联时代手机上传数据的场景随处可见,为了方便开发者聚焦于产品的业务逻辑,用户可以直接将文件存储到阿里云对象存储上。阿里云对象存储产品基于STS授权方式为用户提供了安全的上传和下载方式,阿里云对象存储还支持图片处理服务。阿里云对象存储具有成本低、支持海量存储和弹性扩展的特性,能帮助开发者更方便实现移动APP业务的开发。

本教程能帮助用户快速搭建一个基于阿里云对象存储的文件直传+图片处理的手机APP,主要基于STS临时授权、Android SDK和图片处理API三个模块实现。

  • 由于用户的移动端应用存在泄密的风险所以不可能直接存储AK/SK信息,必须使用STS临时授权模式访问阿里云对象存储。STS临时授权模式中会生成一个临时Token,该Token具有一定的时效性,即APP应用只有在Token的时效性内访问才可以完成上传或下载图片服务,过了时效需要重新获取。
  • Andriod SDK帮助用户实现新建阿里云对象存储客户端并将文件存储到阿里云对象存储或从阿里云对象存储中下载文件。
  • 图片处理API主要实现图片的处理如缩略、裁剪、格式转换、旋转、加文字/图片水印等功能。

手机美图APP数据交互如下:

美图APP示例

手机美图示例APP下载地址:

下载完APP并安装完成后可以直接通过应用服务器地址访问阿里云对象存储,并进行图片处理。应用服务器地址是指搭建移动应用的后台服务器,默认开启的端口为8080。关于阿里云对象存储的区域和Bucket设置都需要在应用服务器进行配置。

APP操作方法

该APP支持用户上传、下载和下载缩放图三个功能。

  • 上传:用户填写应用服务器地址后,选择本地需要上传的图片,图片会显示在操作界面下方,点击上传即可。上传成功会显示“File Uploaded”。
  • 下载:用户填写需要下载的文件名称,点击下载按钮即可。下载成功会显示“File Downloaded”。
  • 下载缩放图:下载缩放图时必须指定明确的图片后缀如jpg,然后设置下载图形的宽和高以及旋转角度,点击下载缩放图,则会获取经过处理的图形。下载成功会显示“File Downloaded”。Demo版本下载后的图片都不会存储到本地。

如何搭建美图APP

搭建美图APP包含以下几个步骤:

  1. 开启阿里云对象存储服务并创建Bucket用于存储图片,开通和创建Bucket的详细操作请参见创建Bucket。如果要下载缩放图,需要保证指定的Bucket开启了图片处理服务。
  2. 开通STS服务,用于保证上传和下载图片的安全性。
  3. 部署应用服务器,实现和阿里云对象存储及客户端的交互,美图APP代码请参考:阿里云对象存储美图APP代码。
  4. 下载安装美图APP。

如何部署应用服务器

  1. 从github上下载sample code的代码包,代码包主要包含“bos_meitu_app”和“bos_meitu_app_server”两部分,其中“bos_meitu_app”主要用于定义APP界面及相关动作,“bos_meitu_app_server”为应用服务器相关配置。
  2. 修改“bos_meitu_app_server”中的“MeituAppServerHandler.java”文件,其中定义了ak/sk、阿里云对象存储服务器对应的Endpoint和Bucket名称等信息。

    public String getBosInfo(String bosRequestType) { //配置ak、sk String bosAk = "开发者的ak"; String bosSk = "开发者的SK"; //阿里云提供的stsendpoint String stsEndpoint = "http://sts.bj.baidubce.com"; BceCredentials credentials = new DefaultBceCredentials(bosAk, bosSk); BceClientConfiguration clientConfig = new BceClientConfiguration(); clientConfig.setCredentials(credentials); clientConfig.setEndpoint(stsEndpoint); StsClient stsClient = new StsClient(clientConfig); GetSessionTokenRequest stsReq = new GetSessionTokenRequest(); // request expiration time stsReq.setDurationSeconds(1800); GetSessionTokenResponse stsToken = stsClient.getSessionToken(stsReq); String stsTokenAk = stsToken.getCredentials().getAccessKeyId(); String stsTokenSk = stsToken.getCredentials().getSecretAccessKey(); String stsTokenSessionToken = stsToken.getCredentials().getSessionToken(); // 阿里云对象存储服务的Endpoint地址,及Bucket名称。 String bosEndpoint = "http://bj.bcebos.com"; String bucketName = "Bucket名称"; if (bosRequestType.equalsIgnoreCase("download-processed")) { // the binded image processing domain set by App developer on bce console bosEndpoint = "http://" + bucketName + ".bj.bcebos.com"; } // prefix is the bucket name, and does not specify the object name BosInfo bosInfo = new BosInfo(stsTokenAk, stsTokenSk, stsTokenSessionToken, bosEndpoint, bucketName, "", bucketName); String res = ""; ObjectMapper mapper = new ObjectMapper(); try { res = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(bosInfo); } catch (JsonProcessingException e) { // TODO Auto-generated catch block e.printStackTrace(); res = ""; } System.out.println(res); try { res = new String(res.getBytes(), "utf8"); } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); res = ""; } return res; }

  3. 将修改完成的服务器代码重新编译打包为bos_meitu_app_server.jar,将jar包上传到应用服务器上并执行命令java -jar bos_meitu_app_server.jar。

交互过程

上传图片到阿里云对象存储

上传图片到阿里云对象存储过程中APP、APP Server和阿里云对象存储的交互过程:

  1. APP上传图片时向APP Server发送获取上传方式请求。
  2. APP Server向STS服务器请求阿里云对象存储使用STS访问的AK/SK,STS服务器向APP Server返回STS凭证,包括临时AK、SK和Session Token。
  3. APP Server将STS凭证及上传方式参数返回,上传方式参数包含Bucket名称、Endpoint等。
  4. APP根据返回的信息将文件上传到阿里云对象存储上,阿里云对象存储会将上传结果返回给APP。
  5. APP可以根据需要将上传结果提供给APP Server。

从阿里云对象存储下载图片

从阿里云对象存储下载图片过程中APP、APP Server和阿里云对象存储的交互过程:

  1. APP下载图片时向APP Server发送获取下载方式请求。
  2. APP Server向STS服务器请求阿里云对象存储使用STS访问的AK/SK,STS服务器向APP Server返回STS凭证,包括临时AK、SK和Session Token。
  3. APP Server将STS凭证及下载方式参数返回APP,下载方式参数包含Bucket名称、Endpoint等。
  4. APP根据返回的信息
  5. 从阿里云对象存储上下载文件,阿里云对象存储会将下载结果返回给APP。
  6. APP可以根据需要将下载结果提供给APP Server。

从阿里云对象存储下载缩放图

从阿里云对象存储下载缩放图过程中APP、APP Server和阿里云对象存储的交互过程:

从阿里云对象存储下载缩放图和从阿里云对象存储上下载图片交互过程基本类似,只是在从阿里云对象存储下载缩放图时需要携带APP上设定的图片处理参数,如图片宽、高和旋转角度等。

示例代码

示例代码以Java语言为例讲解美图APP的实现,代码分为APP客户端和应用服务器端两部分。

APP客户端代码样例

APP端代码主要包括阿里云对象存储Client初始化、从APP Server端获取阿里云对象存储信息、及上传文件到阿里云对象存储三个功能模块。

阿里云对象存储Client初始化
public class bos {private string ak = null; private string sk = null; private string endpoint = null; private string ststoken = null; private bosclient client = null; public bos(string ak, string sk, string endpoint, string ststoken) { this.ak = ak; this.sk = sk; this.endpoint = endpoint; this.ststoken = ststoken; client = createclient(); } public bosclient createclient() { bosclientconfiguration config = new bosclientconfiguration(); bcecredentials credentials = null; if (ststoken != null && !ststoken.equalsignorecase("")) { credentials = new defaultbcesessioncredentials(ak, sk, ststoken); } else { credentials = new defaultbcecredentials(ak, sk); } config.setendpoint(endpoint); config.setcredentials(credentials); return new bosclient(config); } public void uploadfile(string bucket, string object, file file) { client.putobject(bucket, object, file); } public void uploadfile(string bucket, string object, inputstream inputstream) { client.putobject(bucket, object, inputstream); } public void uploadfile(string bucket, string object, byte[] data) { client.putobject(bucket, object, data); } public byte[] downloadfilecontent(string bucket, string object) { return client.getobjectcontent(bucket, object); }}
上传文件到阿里云对象存储代码实现
public void uploadPicToBos() { // 1. get pic params from ui: file name, file location uri etc // 2. send params to app server and get sts, bucket name and region // 3. upload selected pic to bos with sts etc, which bos client needs EditText et = (EditText) findViewById(R.id.app_server_addr_edittext); final String appServerAddr = et.getText().toString(); new Thread(new Runnable() { @Override public void run() { Map<String, Object> bosInfo = AppServer.getBosInfoFromAppServer(appServerAddr, "user-demo", AppServer.BosOperationType.UPLOAD); if (bosInfo == null) { return; } showToast(bosInfo.toString(), Toast.LENGTH_LONG); String ak = (String) bosInfo.get("ak"); String sk = (String) bosInfo.get("sk"); String stsToken = (String) bosInfo.get("stsToken"); String endpoint = (String) bosInfo.get("endpoint"); String bucketName = (String) bosInfo.get("bucketName"); String objectName = (String) bosInfo.get("objectName"); String prefix = (String) bosInfo.get("prefix"); Log.i("UploadFileToBos", bosInfo.toString()); // specify a object name if the app server does not specify one if (objectName == null || objectName.equalsIgnoreCase("")) { objectName = ((EditText) findViewById(R.id.bos_object_name_edittext)).getText().toString(); if (prefix != null && !prefix.equalsIgnoreCase("")) { objectName = prefix + "/" + objectName; } } Bos bos = new Bos(ak, sk, endpoint, stsToken); try { byte[] data = Utils.readAllFromStream(MainActivity.this.getContentResolver().openInputStream(selectedPicUri)); bos.uploadFile(bucketName, objectName, data); } catch (Throwable e) { Log.e("MainActivity/Upload", "Failed to upload file to bos: " + e.getMessage()); showToast("Failed to upload file: " + e.getMessage()); return; } // finished uploading file, send a message to inform ui handler.sendEmptyMessage(UPLOAD_FILE_FINISHED); } }).start();}
从APP Server上获取阿里云对象存储信息代码实现
public class AppServer { /** * get info from app server for the file to upload to or download from 阿里云对象存储 * * @param appServerEndpoint app server * @param userName the app user's name, registered in app server * @param bosOperationType download? upload? or? * @return STS, and 阿里云对象存储 endpoint, bucketName, prefix, path, object name etc */ public static Map<String, Object> getBosInfoFromAppServer(String appServerEndpoint, String userName, BosOperationType bosOperationType) { String type = ""; switch (bosOperationType) { // to simplify case UPLOAD: { type = "upload"; break; } case DOWNLOAD: { type = "download"; break; } case DOWNLOAD_PROCESSED: { type = "download-processed"; break; } default:{ break; } } // TODO: this url should be url encoded String appServerUrl = appServerEndpoint + "/?" + "userName=" + userName + "&command=stsToken&type=" + type; // create a http client to contact app server to get sts HttpParams httpParameters = new BasicHttpParams(); HttpClient httpClient = new DefaultHttpClient(httpParameters); HttpGet httpGet = new HttpGet(appServerUrl); httpGet.addHeader("User-Agent", "bos-meitu-app/demo"); httpGet.setHeader("Accept", "*/*"); try { httpGet.setHeader("Host", new URL(appServerUrl).getHost()); } catch (MalformedURLException e) { e.printStackTrace(); } httpGet.setHeader("Accept-Encoding", "identity"); Map<String, Object> bosInfo = new HashMap<String, Object>(); try { HttpResponse response = httpClient.execute(httpGet); if (response.getStatusLine().getStatusCode() != 200) { return null; } HttpEntity entity = response.getEntity(); long len = entity.getContentLength(); InputStream is = entity.getContent(); int off = 0; byte[] b = new byte[(int) len]; while (true) { int readCount = is.read(b, off, (int) len); if (readCount < 0) { break; } off += readCount; } Log.d("AppServer", new String(b, "utf8")); JSONObject jsonObject = new JSONObject(new String(b, "utf8")); Iterator<String> keys = jsonObject.keys(); while (keys.hasNext()) { String key = keys.next(); bosInfo.put(key, jsonObject.get(key)); } } catch (IOException e) { e.printStackTrace(); return null; } catch (JSONException e) { e.printStackTrace(); return null; } return bosInfo; } public enum BosOperationType { UPLOAD, DOWNLOAD, DOWNLOAD_PROCESSED, }}
APP Server端代码样例

APP Server端基于Jetty框架,接口主要处理Android APP获取阿里云对象存储信息的请求。APP Server端会返回临时AK、SK、Session Token、bucket名称、资源路径和资源请求的Endpoint等参数。以下为Jetty处理的代码示例。

@Overridepublic void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { // Inform jetty that this request has now been handled baseRequest.setHandled(true); if (!request.getMethod().equalsIgnoreCase("GET")) { response.setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED); return; } // expected url example: localhost:8080/?command=stsToken&type=download Map<String, String[]> paramMap = request.getParameterMap(); if (paramMap.get("command") == null || paramMap.get("type") == null) { // invalid request response.setStatus(HttpServletResponse.SC_BAD_REQUEST); return; } if (!paramMap.get("command")[0].equalsIgnoreCase("stsToken")) { response.setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED); return; } String responseBody = ""; responseBody = getBosInfo(paramMap.get("type")[0]); // Declare response encoding and types response.setContentType("application/json; charset=utf-8"); // Declare response status code response.setStatus(HttpServletResponse.SC_OK); // Write back response, utf8 encoded response.getWriter().println(responseBody);}/** * Generates bos info needed by app according to requset type(upload, download etc) * this is the key part for uploading file to bos with sts token * @param bosRequestType * @return utf8 encoded json string */public String getBosInfo(String bosRequestType) { // configuration for getting stsToken // bce bos credentials ak sk String bosAk = "your_bos_ak"; String bosSk = "your_bos_sk"; // bce sts service endpoint String stsEndpoint = "http://sts.bj.baidubce.com"; BceCredentials credentials = new DefaultBceCredentials(bosAk, bosSk); BceClientConfiguration clientConfig = new BceClientConfiguration(); clientConfig.setCredentials(credentials); clientConfig.setEndpoint(stsEndpoint); StsClient stsClient = new StsClient(clientConfig); GetSessionTokenRequest stsReq = new GetSessionTokenRequest(); // request expiration time stsReq.setDurationSeconds(1800); GetSessionTokenResponse stsToken = stsClient.getSessionToken(stsReq); String stsTokenAk = stsToken.getCredentials().getAccessKeyId(); String stsTokenSk = stsToken.getCredentials().getSecretAccessKey(); String stsTokenSessionToken = stsToken.getCredentials().getSessionToken(); // **to simplify this demo there is no difference between "download" and "upload"** // parts of bos info String bosEndpoint = "http://bj.bcebos.com"; String bucketName = "bos-android-sdk-app"; if (bosRequestType.equalsIgnoreCase("download-processed")) { // the binded image processing domain set by App developer on bce console bosEndpoint = "http://" + bucketName + ".bj.bcebos.com"; } // prefix is the bucket name, and does not specify the object name BosInfo bosInfo = new BosInfo(stsTokenAk, stsTokenSk, stsTokenSessionToken, bosEndpoint, bucketName, "", bucketName); String res = ""; ObjectMapper mapper = new ObjectMapper(); try { res = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(bosInfo); } catch (JsonProcessingException e) { e.printStackTrace(); res = ""; } try { res = new String(res.getBytes(), "utf8"); } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); res = ""; } return res;}