JAVA模拟PostObject表单上传OSS,实现签名直传

先感慨一下:不亲自去趟一趟自己的坑,永远无法理解用户的痛!!

使用JAVA实现PostObject这个需求,其实来自之前support同学的一段描述,说是有用户需求,但是官方没有任何demo的代码参考,用户自己根据官方文档介绍实现却是各种很难调查的问题。这个背景就不细说了。后来因为项目需要,就照着官网也去实现了一把,各中酸泪尽享其中,总之,我们还是有很多需要改进的地方的。为了用户,自勉,共勉!!

Post Object使用HTML表单上传文件到指定bucket,作为Put的替代品,使得基于浏览器上传文件到bucket成为可能。关于OSS官方文档对PostObject这个API的介绍文档参见这里(建议先阅读这个PostObject的介绍,下面的内容均与此有关,不然可能看不懂哦)。

官网中首先给出了HTTP的请求语法,即HTTP请求头和通过multipart/form-data编码的处于消息实体中用来传递参数的表单域形式;接下来通过表单域的表格形式逐个介绍了“file”和“key”这样“必选”的表单域,“OSSAccessKeyId”、“policy”、“Signature”这种因为其中一个的出现而变成必须出现的表单域,还有REST请求头、x-oss-meta-*用户meta及其他可选的表单域;然后介绍了一些表单域的特殊使用方式和注意事项;最后花了大篇幅介绍Post Policy和Signature的功能、用法。
文档大致清晰,只是因为概念较多,不容易理解,并且在实现中对容易出错的地方没有进行强调,导致问题调查的难度增加。根据亲身经历分析,主要的问题可能两个方面。
  • 对multipart/form-data这种MIME类型的编码方式不熟悉。
  • 对OSS系统解析PostObject请求的实现规则不了解。

接下来,针对上面两个方面进行详细的讲解。

      multipart/form-data的介绍详见RFC 2388,以下几点简单提一下:
      1. “multipart/form-data”包含一系列的域,每个域都有一个类型为“form-data”的content-disposition首部,并且,这个首部包含参数“name”,用来描述该表单域内容的描述信息。所以,每个域都会有如文档中给出的示例形式:
Content-Disposition: form-data; name="your_key"

 注意:":"和";"后面都有一个空格。
      2. 表单中有需求上传用户文件,可能会需要文件名称或者其他文件属性,需要包含在content-dispoisition首部中,如参数"filename",而且对于表单域中的任何MIME类型,都有一个optional的Content-Type属性,用来标识文件内容类型。所以文档中给出了“file”表单域的示例如下:

Content-Disposition: form-data; name="file"; filename="MyFilename.jpg"
Content-Type: image/jpeg

 注意:“filename”前的";"后仍然有一个空格;Content-Type之后的“:”同样有一个空格。
      3. 使用boundary来分隔数据,为了和主体内容区分,尽量使用复杂的boundary。如文档中给出的HTTP首部中的内容:

Content-Type: multipart/form-data; boundary=9431149156168

      4. 每个表单域的结构都是固定的。规定每个表单域都以"--"boundary+开头,然后回车(/r/n);然后是该表单域的描述信息(见描述1),接着/r/n。如果传送的内容是一个文件的话,那么还会包含文件名信息,回车(/r/n)之后接上文件内容的类型(见描述2)。然后,紧接着再一个回车(/r/n),开始真正的具体内容,最后以/r/n结束。
      5. 在最后的表单域结束,以"--"+boundary+"--"结尾,表示请求体结束。
      6. 补充一点,HTTP请求header与主体信息之间(header和第一个表单域的交界处),也需要有一个/r/n用来区分,即多出一个空行,如文档中:

Content-Type: multipart/form-data; boundary=9431149156168

--9431149156168
Content-Disposition: form-data; name="key"

 
      以上,大致是对照RFC 2388的标准描述了OSS官方文档中给出的请求语法以及相关的分析。下面会讲解一小部分OSS系统解析PostObject请求过程,和相关注意事项。

 OSS解析POST请求的大致流程如下图(仅供参考,与真实实现有差异):
JAVA模拟PostObject表单上传OSS,实现签名直传
  请求的处理流程可以简单的理解为三个步骤:
      1. 从HTTP请求header中解析boundary,用来区分域的分界;
      2. 解析各个域的内容,直到遇到‘file’这个表单域;
      3. 解析‘file’表单域。
  所以,文档中会强调,需要将‘file’这个表单域放置在“最后一个域”中,不然,处于file之后的表单域不保证一定能生效哦!如果是把‘key’这个必须的表单域放置在‘file’的后面,亲测,肯定是InvalidArgument。
      
       简单介绍一些图上的某些流程:
       1) check POLICY、OSSACCESSKEYID、SIGNATURE existence: 因为POLICY、OSSACCESSKEYID、SIGNATURE这三个域,其中一个出现,其他两个都会成为必选域,所以这里会做该类检查。
       2) Authorization: 根据POLICY、OSSACCESSKEYID、SIGNATURE等信息对验证该Post请求的合法性。
       3) Policy rule check: 检查请求各个表单域中的设置是否符合policy的配置。
       4) check length Legality: 因为Post请求的body总长度有限制,对可选的域的长度会进行检查。
       5) ParseFile中的ParseContentType: 解析file域中的ContentType字段,该字段不是必须的。


   最后,这里附上JAVA实现的OSS PostObject上传的代码(Maven工程),供各位OSS用户和研究人员参考使用:
import javax.activation.MimetypesFileTypeMap;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Created by yushuting on 16/4/17.
 */
public class OssPostObject {

    private String postFileName = "your_file";  // 确保运行代码的路径中有该文件
    private String ossEndpoint = "your_endpoint";  // 如: http://oss-cn-shanghai.aliyuncs.com
    private String ossAccessId = "your_accessid";  // 你的访问AK信息
    private String ossAccessKey = "your_accesskey";  // 你的访问AK信息
    private String objectName = "your_object_name";  // 你上传文件之后的object名称
    private String bucket = "your_bucket";  // 你之前创建的bucket,确保这个bucket已经创建

    private void PostObject() throws Exception {

        String filepath=postFileName;
        String urlStr = ossEndpoint.replace("http://", "http://"+bucket+"."); // 提交表单的URL为bucket域名

        LinkedHashMap<String, String> textMap = new LinkedHashMap<String, String>();
        // key
        String objectName = this.objectName;
        textMap.put("key", objectName);
        // Content-Disposition
        textMap.put("Content-Disposition", "attachment;filename="+filepath);
        // OSSAccessKeyId
        textMap.put("OSSAccessKeyId", ossAccessId);
        // policy
        String policy = "{\"expiration\": \"2120-01-01T12:00:00.000Z\",\"conditions\": [[\"content-length-range\", 0, 104857600]]}";
        String encodePolicy = java.util.Base64.getEncoder().encodeToString(policy.getBytes());
        textMap.put("policy", encodePolicy);
        // Signature
        String signaturecom = com.aliyun.oss.common.auth.ServiceSignature.create().computeSignature(ossAccessKey, encodePolicy);
        textMap.put("Signature", signaturecom);

        Map<String, String> fileMap = new HashMap<String, String>();
        fileMap.put("file", filepath);

        String ret = formUpload(urlStr, textMap, fileMap);
        System.out.println("[" + bucket + "] post_object:" + objectName);
        System.out.println("post reponse:" + ret);
    }

    private static String formUpload(String urlStr, Map<String, String> textMap, Map<String, String> fileMap) throws Exception {
        String res = "";
        HttpURLConnection conn = null;
        String BOUNDARY = "9431149156168";
        try {
            URL url = new URL(urlStr);
            conn = (HttpURLConnection) url.openConnection();
            conn.setConnectTimeout(5000);
            conn.setReadTimeout(30000);
            conn.setDoOutput(true);
            conn.setDoInput(true);
            conn.setRequestMethod("POST");
            conn.setRequestProperty("User-Agent",
                    "Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-CN; rv:1.9.2.6)");
            conn.setRequestProperty("Content-Type",
                    "multipart/form-data; boundary=" + BOUNDARY);

            OutputStream out = new DataOutputStream(conn.getOutputStream());
            // text
            if (textMap != null) {
                StringBuffer strBuf = new StringBuffer();
                Iterator iter = textMap.entrySet().iterator();
                int i = 0;
                while (iter.hasNext()) {
                    Map.Entry entry = (Map.Entry) iter.next();
                    String inputName = (String) entry.getKey();
                    String inputValue = (String) entry.getValue();
                    if (inputValue == null) {
                        continue;
                    }
                    if (i == 0) {
                        strBuf.append("--").append(BOUNDARY).append(
                                "\r\n");
                        strBuf.append("Content-Disposition: form-data; name=\""
                                + inputName + "\"\r\n\r\n");
                        strBuf.append(inputValue);
                    } else {
                        strBuf.append("\r\n").append("--").append(BOUNDARY).append(
                                "\r\n");
                        strBuf.append("Content-Disposition: form-data; name=\""
                                + inputName + "\"\r\n\r\n");

                        strBuf.append(inputValue);
                    }

                    i++;
                }
                out.write(strBuf.toString().getBytes());
            }

            // file
            if (fileMap != null) {
                Iterator iter = fileMap.entrySet().iterator();
                while (iter.hasNext()) {
                    Map.Entry entry = (Map.Entry) iter.next();
                    String inputName = (String) entry.getKey();
                    String inputValue = (String) entry.getValue();
                    if (inputValue == null) {
                        continue;
                    }
                    File file = new File(inputValue);
                    String filename = file.getName();
                    String contentType = new MimetypesFileTypeMap().getContentType(file);
                    if (contentType == null || contentType.equals("")) {
                        contentType = "application/octet-stream";
                    }

                    StringBuffer strBuf = new StringBuffer();
                    strBuf.append("\r\n").append("--").append(BOUNDARY).append(
                            "\r\n");
                    strBuf.append("Content-Disposition: form-data; name=\""
                            + inputName + "\"; filename=\"" + filename
                            + "\"\r\n");
                    strBuf.append("Content-Type: " + contentType + "\r\n\r\n");

                    out.write(strBuf.toString().getBytes());

                    DataInputStream in = new DataInputStream(new FileInputStream(file));
                    int bytes = 0;
                    byte[] bufferOut = new byte[1024];
                    while ((bytes = in.read(bufferOut)) != -1) {
                        out.write(bufferOut, 0, bytes);
                    }
                    in.close();
                }
                StringBuffer strBuf = new StringBuffer();
                out.write(strBuf.toString().getBytes());
            }

            byte[] endData = ("\r\n--" + BOUNDARY + "--\r\n").getBytes();
            out.write(endData);
            out.flush();
            out.close();

            // 读取返回数据
            StringBuffer strBuf = new StringBuffer();
            BufferedReader reader = new BufferedReader(new InputStreamReader(
                    conn.getInputStream()));
            String line = null;
            while ((line = reader.readLine()) != null) {
                strBuf.append(line).append("\n");
            }
            res = strBuf.toString();
            reader.close();
            reader = null;
        } catch (Exception e) {
            System.err.println("发送POST请求出错: " + urlStr);
            throw e;
        } finally {
            if (conn != null) {
                conn.disconnect();
                conn = null;
            }
        }
        return res;
    }

    public static void main(String[] args) throws Exception {
        OssPostObject ossPostObject = new OssPostObject();
        ossPostObject.PostObject();
    }

}

 

注意在pom.xml加上:
<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>2.2.1</version>
</dependency>

 



------------------------------------------------------分隔符-----------------------------------------------------------

诚聘英才


阿里云函数服务是一个全新的,支持事件驱动编程模式的计算服务。 他帮助用户聚焦自身业务逻辑,以Serverless的方式构建应用,快速的实现低成本,可扩展,高可用的系统,而无需考虑服务器等底层基础设施的管理。 用户能够快速的创建原型,同样的架构能随业务规模平滑伸缩。让计算变得更高效,更经济,更弹性,更可靠。无论小型创业公司,还是大型企业,都受益其中。

我们的团队正在迅速扩张,求贤若渴。我们想寻找这样的队友:

  • 基本功扎实。既能阅读论文追踪业界趋势,又能快速编码解决实际问题。
  • 严谨的,系统化的思维能力。既能整体考虑业务机会,系统架构,运维成本等诸多因素,又能掌控设计/开发/测试/发布的完整流程,预判并控制风险。
  • 好奇心和使命感驱动。乐于探索未知领域,不仅是梦想家,也是践行者。
  • 坚韧、乐观、自信。能在压力和困难中看到机会,让工作充满乐趣!

如果您对云计算充满热情,想要构建一个有影响力计算平台和生态体系,请加入我们,和我们一起实现梦想! 

详见:http://www.atatech.org/articles/53851

将你的简历发送到shuting.yst@alibaba-inc.com,标题  应聘阿里云-姓名

如果你有自己的git地址或者个人博客,将会大大加分哦,一起在邮件中发给我吧~~~

上一篇:Linux 服务器建站新手教程(宝塔建站全流程)-不需要敲一行命令


下一篇:Python strip 内置方法使用上的误区