OBS服务应用于互联网数据上传时 使用POST 实现服务端和客户端权限控制和数据上传分离的方法
1 背景
采用JavaScript SDK 等客户端直接签名时,AccessKeyID和AcessKeySecret会暴露在前端页面,因此存在严重的安全隐患。因此,OBS提供了服务端签名后直传的方案 解决此问题。问题代码如下所示(需要在前端使用ak sk作为初始化条件):

2 原理介绍

1. 客户端在登陆后,向app server请求上传对象的鉴权token;
2. App server 根据永久AK SK和针对上传对象和桶的policy生成一个token(具体参考后的代码示例)
3. 前端组件 收到token后使用post请求将token作为一个表单项进行对象上传。
同时我们也做了示例网站进行功能的展示;https://codepen.io/x00403408/pen/xQYZgE 此地址示例如何生成一个token;

https://codepen.io/x00403408/pen/WYMrbY 网站示例POST请求如何使用token进行数据的上传

2.1.2 约束限制
1. Post表单上传是单流上传,没法实现断点续传功能。因此比较适合一些小文件的上传。
2. Post上传对于表单域我们采用强校验模式,只要携带的表单域除我们没定义的外都要包含在policy中参与签名计算。具体参考文档描述:https://support.huaweicloud.com/api-obs/zh-cn_topic_0106557184.html

3 流程和源码解析
3.1 服务端代码
Java SDK 代码示例生成服务端token
package samples_java;
 
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
 
import com.obs.services.ObsClient;
import com.obs.services.ObsConfiguration;
import com.obs.services.exception.ObsException;
import com.obs.services.model.AuthTypeEnum;
import com.obs.services.model.PostSignatureRequest;
import com.obs.services.model.PostSignatureResponse;
 
public class TestPostObject {
    private static final String endPoint = "obs.myhwclouds.com";
 
    private static final String ak = "";
 
    private static final String sk = "";
 
    private static ObsClient obsClient;
 
    private static String bucketName = "";
 
    private static AuthTypeEnum authType = AuthTypeEnum.OBS;
 
    public static void main(String[] args) throws IOException {
        ObsConfiguration config = new ObsConfiguration();
        config.setEndPoint(endPoint);
        config.setAuthType(authType);
 
        try {
            obsClient = new ObsClient(ak, sk, config);
            //创建token
            PostSignatureRequest request = new PostSignatureRequest();
            request.setExpires(3600);
            ArrayList<String> conditions = new ArrayList<String>();
            //Condition可以根据不同业务头域的需求进行增删
            conditions.add("[\"starts-with\",\"$content-type\",\"\"]");
            conditions.add("[\"starts-with\",\"$key\",\"\"]");
            conditions.add("{\"bucket\":\""+bucket+"\"}");
            request.setConditions(conditions);
            PostSignatureResponse response = obsClient.createPostSignature(request);
            String Token = response.getToken();
           
        } catch (Exception ex) {
            if (ex instanceof ObsException) {
                ObsException e = (ObsException) ex;
                System.out.println("Message: " + e.getMessage());
            } else {
                ex.printStackTrace();
            }
        } finally {
            if (obsClient != null) {
                try {
                    obsClient.close();
                } catch (IOException e) {
                }
            }
        }
    }
 
}3.2 客户端代码
客户端的代码在使用POST进行表单上传时候直接构造POST请求,其中传递token等表单域信息即可(JS代码);此处可以参考我们的web示例(https://codepen.io/x00403408/pen/WYMrbY)的代码(截取部分关键代码):
$(window.document).ready(function() {
 
  $('#progressBar').hide();
  $('#upload').click(function() {
    var token = $.trim($('#token').val());
    var bucket = $.trim($('#bucket').val());
    var endpoint = $.trim($('#endpoint').val());
 
    var fileList = $('#inputFile')[0].files;
    if(token === '' || bucket === '' || endpoint === '' || fileList.length <= 0){
      window.alert('输入有误!');
      return;
    }
 
    if(isIpAddress(endpoint)){
      endpoint += '/' + bucket;
    }else{
      endpoint = bucket + '.' + endpoint;
    }
    endpoint = 'https://' + endpoint;
 
 
    var xhr = new XMLHttpRequest();
    xhr.open('POST', endpoint, true);
    var formData = new FormData();
    var key = $.trim($('#key').val()) || fileList[0].name;
    formData.append('key', key);
    formData.append('token', token);
    var contentType = mimeTypes[key.substring(key.lastIndexOf('.') + 1)];
    //if(contentType){
           //formData.append('content-type', contentType);
    //}
    //var tokens = token.split(':');
    //formData.append('AccessKeyId', tokens[0]);
    //formData.append('Signature', tokens[1]);
    //formData.append('Policy', tokens[2]);
    formData.append('file', fileList[0]);
 
 
    var start = new Date().getTime();
    var cost;
 
    xhr.upload.addEventListener('progress', function(event) {
      if (event.lengthComputable) {
        if(!cost){
          $('#progressBar').show();
        }
        cost = new Date().getTime() - start;
        var uploadSpeed = (event.loaded / 1024 / cost * 1000).toFixed(2) + 'KB\/s';
        var percentage = Math.round(event.loaded * 100 / event.total) + '%';
        $('#uploadSpeed').html(uploadSpeed);
        $('#percentage').html(percentage);
        $('#progressBar').css('width', percentage);
      }
    }, false);
 
    xhr.onreadystatechange = function(response) {
      if(xhr.readyState === 4){
        if(xhr.status < 300){
          console.log('上传成功!');
        }else{
          console.log('上传失败!');
        }
       }
    };
    xhr.send(formData);
3.3 过程问题说明
3.3.1 POST 上传对应响应码200或204对兼容性影响说明
POST 上传默认的响应码是204,但是在IE8/9浏览器对此响应码是不做状态上报的,导致web中的JS代码无法获取什么时候上传成功。际上是可以通过 success_action_status 重新定义响应状态码,在Post请求时候需要携带此项到表单域中。(POLICY中:["starts-with", "$success_action_status", ""],)
请求示例:

响应示例:

3.3.2 文件的content-type设置
因为 在web/app类应用中为了防止不同用户上传的图片产生重名问题,客户在上传时候会针对原始文件名称生成一个唯一的对象名称KEY值,这样防止了存储在对象存储时候对象名称相同时候覆盖。

而这个时候导致一个问题,存储在对象存储的文件缺失了后缀名,JS-sdk不会增加content-type描述(通过后缀名增加)。OBS对于POST中携带默认的content-type描述不识别,导致设置为application/octect-stream;使得在浏览器下载时候不能在线展示;

那么这个时候处理需要在前端上传时候增加content-type表单域;同时在后端生成上传token时policy也需要增加(POLICY中:["starts-with", "$ content-type ", ""],)来支持前端上传。

- 点赞
 - 收藏
 - 关注作者
 
            
           
评论(0)