aliyun oss 服务端签名后直传

aliyun oss 服务端签名后直传

前言

以前的文件上传都是前端将文件流上传到服务端后,服务端进行处理,返回给前端地址,这种方式适合传统单体的系统架构,不将文件服务作为一个单独的服务进行部署,在现在流行的分布式大环境下,传统的文件存储方式已经不适用,文件存储服务以第三方服务的形式,直接调用即可。例如阿里云 OSS,七牛云,腾讯云的COS 等等。这些第三方的文件服务就为我们提供了存储解决方案,既然我们已经不将文件存储在服务端,那我们也没有必要先让前端将文件流传递到服务端,再由服务端将文件上传到第三方文件服务。


下面是博主基于springboot搭建的一套完整的oss直传的DEMO

码云地址 :https://gitee.com/jack_whh/oss-policy-springboot.git
码云地址


前端直传 官方文档地址
  • 优点:
    - 直传文件流不用过服务端,上传速度不受服务端带宽影响。
    - 由于文件不过服务端,所以也不占用服务端的系统资源。
  • 缺点:
    - 客户端通过JavaScript把AccesssKeyID和AccessKeySecret写在代码里面有泄露的风险。
为解决直传的缺点,所以提出本文介绍重点:服务端签名后直传。
一. 流程介绍
  • 流程如下图所示:
    在这里插入图片描述

  • 当用户要上传一个文件到OSS,而且希望将上传的结果返回给应用服务器时,需要设置一个回调函数,将请求告知应用服务器。用户上传完文件后,不会直接得到返回结果,而是先通知应用服务器,再把结果转达给用户。

代码实现
  • 安装:
<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.10.2</version>
</dependency>

官方地址

  • 前期设置
    - 创建bucket 快捷入口
    - 修改CORS
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

  • 获取policy的主要代码

public Map<String, String> getPolicy() {
        // host的格式为 bucketname.endpoint
        final String host = "http://" + ossProperties.getBucket() + "." + ossProperties.getEndpoint();
        // 直传有效截止时间
        long expireEndTime = System.currentTimeMillis() + (ossProperties.getExpireTime() * 1000);
        Date expiration = new Date(expireEndTime);
        PolicyConditions policyConditions = new PolicyConditions();
        // 设置可上传文件的大小
        policyConditions.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, ossProperties.getMin(), ossProperties.getMax());
        // 设置上传文件的前缀、可忽略
        policyConditions.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);
        // 生成policy
        String postPolicy = ossClient.generatePostPolicy(expiration, policyConditions);
        byte[] binaryData = postPolicy.getBytes(StandardCharsets.UTF_8);
        String encodedPolicy = BinaryUtil.toBase64String(binaryData);
        String postSignature = ossClient.calculatePostSignature(postPolicy);
        // 封装policy等信息
        Map<String, String> aliOssPolicy = new HashMap<>();
        aliOssPolicy.put("ossAccessKeyId", ossProperties.getAccessId());
        aliOssPolicy.put("policy", encodedPolicy);
        aliOssPolicy.put("signature", postSignature);
        aliOssPolicy.put("dir", dir);
        aliOssPolicy.put("host", host);
        aliOssPolicy.put("expire", String.valueOf(expireEndTime / 1000));
        aliOssPolicy.put("callback", getCallBackBody());
        return aliOssPolicy;
    }
private String getCallBackBody() {
        Map<String, String> map = new HashMap<>();
        map.put("callbackUrl", ossProperties.getCallbackUrl());
        map.put("callbackBody", "filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width}");
        map.put("callbackBodyType", "application/x-www-form-urlencoded");
        String s = JSON.toJSONString(map);
        String base64CallbackBody = null;
        try {
            base64CallbackBody = BinaryUtil.toBase64String(s.getBytes("utf-8"));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return base64CallbackBody;
    }
  • 主要参数说明:
{
"accessid":"6MKO******4AUk44",
"host":"http://post-test.oss-cn-hangzhou.aliyuncs.com",
"policy":"eyJleHBpcmF0aW9uIjoiMjAxNS0xMS0wNVQyMDo1Mjoy******Jjdb25kaXRpb25zIjpbWyJjdb250ZW50LWxlbmd0aC1yYW5nZSIsMCwxMDQ4NTc2MDAwXSxbInN0YXJ0cy13aXRoIiwiJGtleSIsInVzZXItZGlyXC8iXV19",
"signature":"VsxOcOudx******z93CLaXPz+4s=",
"expire":1446727949,
"callback":"eyJjYWxsYmFja1VybCI6Imh0dHA6Ly9vc3MtZGVtby5hbGl5dW5jcy5jdb206MjM0NTAiLCJjYWxsYmFja0hvc3QiOiJvc3MtZGVtby5hbGl5dW5jcy5jdb20iLCJjYWxsYmFja0JvZHkiOiJmaWxlbmFtZT0ke29iamVjdH0mc2l6ZT0ke3NpemV9Jm1pbWVUeXBlPSR7bWltZVR5cGV9JmhlaWdodD0ke2ltYWdlSW5mby5oZWlnaHR9JndpZHRoPSR7aW1hZ2VJdbmZvLndpZHRofSIsImNhbGxiYWNrQm9keVR5cGUiOiJhcHBsaWNhdGlvbi94LXd3dy1mb3JtLXVybGVuY29kZWQifQ==",
"dir":"user-dirs/"
}

accessid 密钥key
host oss的上传地址
signature 签名
expire 密钥过期时间
callback 回调的base64编码
dir 要上传的指定文件夹

  • 上述示例的callback内容采用的是Base64编码。经过Base64解码后的内容如下:
{"callbackUrl":"http://oss-demo.aliyuncs.com:23450",
"callbackBody":"filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width}",
"callbackBodyType":"application/x-www-form-urlencoded"}
  • 内容解析如下:

    • CallbackUrl:OSS往这个服务器发送的URL请求上传成功后,oss向服务端发送的回调地址,需是公网地址
    • callbackHost:OSS发送这个请求时,请求头部所带的Host头。
    • callbackBody:OSS请求时,发送给应用服务器的内容,可以包括文件的名称、大小、类型。如果是图片,可以是图片的高度、宽度。
    • callbackBodyType:请求发送的Content-Type。
  • 前端代码,这里使用vue+element-ui 写的一个小页面

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <!-- import CSS -->
    <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
    <title>上传图片</title>
</head>
<body>
<div id="app">
    <div class="div-center-class">
        <el-upload
                class="upload-demo"
                action="#"
                drag
                :http-request="httpRequestHandle"
        >
            <i class="el-icon-upload"></i>
            <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
            <div class="el-upload__tip" slot="tip">只能上传jpg/png文件,且不超过500kb</div>
        </el-upload>
        <div v-if="imgUrl">
            <img :src="imgUrl"></img>
        </div>
    </div>
</div>
</body>
<!-- import Vue before Element -->
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<!-- import JavaScript -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<!--import  axios -->
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
    new Vue({
        el: '#app',
        data: function () {
            return {
                imgUrl: ''
            }
        },
        methods: {
            getpolicy(file) {
                _that = this
                axios.get('ali/oss/getpolicy')
                    .then(function (response) {
                        let {ossAccessKeyId, policy, signature, host, callback} = response.data.data;
                        let formData = new FormData();
                        formData.append("key", `${new Date().getTime()}_${file.name}`);
                        formData.append("success_action_status", 200);  // 让服务端返回200,不设置则默认返回204。
                        formData.append("OssAccessKeyId", ossAccessKeyId);
                        formData.append("policy", policy);
                        formData.append("signature", signature);
                        formData.append("callback", callback);
                        formData.append("file", file); // 必须放在最后
                        // 发送 POST 请求
                        _that.axiosPost("post", host, formData).then(function (res) {
                            _that.imgUrl = res
                        })
                    })
            },
            httpRequestHandle(options) {
                let {file} = options;
                this.getpolicy(file);
            },
            //封装
            //axios封装post请求
            axiosPost(method, url, data) {
                let result = axios({
                    method: method,
                    url: url,
                    data: data
                }).then(resp => {
                    return resp.data.data;
                }).catch(error => {
                    return "exception=" + error;
                });
                return result;
            }
        }
    })
</script>
<style>
    .div-center-class {
        position: absolute;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);
    }
</style>
</html>
  • 前端上传OSS回调

请求路由需和上面的callbackurl一致。

 @PostMapping("/ali/oss/callback")
 public R ossCallback(HttpServletRequest request) throws IOException {
//        request 只能获取一次
        String body = ossComponent.GetPostBody(request.getInputStream(), Integer.parseInt(request.getHeader("content-length")));
        String[] strings = body.split("&");
        String filename = strings[0].split("=")[1];
        boolean b = ossComponent.VerifyOSSCallbackRequest(request,body);
        if (b) {
            String url = ossComponent.getUrl(filename);
            return R.success(url);
        }else {
            return R.error("上传失败");
        }
    }
  • 回调后,我们需要对请求进行校验,校验方法如下:
/**
     * 验证上传回调的Request
     *
     * @param request
     * @return
     * @throws NumberFormatException
     * @throws IOException
     */
    public boolean VerifyOSSCallbackRequest(HttpServletRequest request,String ossCallbackBody)
            throws NumberFormatException, IOException {
        boolean ret = false;
        String autorizationInput = new String(request.getHeader("Authorization"));
        String pubKeyInput = request.getHeader("x-oss-pub-key-url");
        byte[] authorization = BinaryUtil.fromBase64String(autorizationInput);
        byte[] pubKey = BinaryUtil.fromBase64String(pubKeyInput);
        String pubKeyAddr = new String(pubKey);
        if (!pubKeyAddr.startsWith("http://gosspublic.alicdn.com/")
                && !pubKeyAddr.startsWith("https://gosspublic.alicdn.com/")) {
            System.out.println("pub key addr must be oss addrss");
            return false;
        }
        String retString = executeGet(pubKeyAddr);
        retString = retString.replace("-----BEGIN PUBLIC KEY-----", "");
        retString = retString.replace("-----END PUBLIC KEY-----", "");
        String queryString = request.getQueryString();
        String uri = request.getRequestURI();
        String decodeUri = java.net.URLDecoder.decode(uri, "UTF-8");
        String authStr = decodeUri;
        if (queryString != null && !queryString.equals("")) {
            authStr += "?" + queryString;
        }
        authStr += "\n" + ossCallbackBody;
        ret = doCheck(authStr, authorization, retString);
        return ret;
    }

    /**
     * 获取Post消息体
     *
     * @param is
     * @param contentLen
     * @return
     */
    public String GetPostBody(InputStream is, int contentLen) {
        if (contentLen > 0) {
            int readLen = 0;
            int readLengthThisTime = 0;
            byte[] message = new byte[contentLen];
            try {
                while (readLen != contentLen) {
                    readLengthThisTime = is.read(message, readLen, contentLen - readLen);
                    if (readLengthThisTime == -1) {// Should not happen.
                        break;
                    }
                    readLen += readLengthThisTime;
                }
                return new String(message);
            } catch (IOException e) {
            }
        }
        return "";
    }

   

    /**
     * 验证RSA
     *
     * @param content
     * @param sign
     * @param publicKey
     * @return
     */
    private boolean doCheck(String content, byte[] sign, String publicKey) {
        try {
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            byte[] encodedKey = BinaryUtil.fromBase64String(publicKey);
            PublicKey pubKey = keyFactory.generatePublic(new X509EncodedKeySpec(encodedKey));
            java.security.Signature signature = java.security.Signature.getInstance("MD5withRSA");
            signature.initVerify(pubKey);
            signature.update(content.getBytes());
            boolean bverify = signature.verify(sign);
            return bverify;

        } catch (Exception e) {
            e.printStackTrace();
        }

        return false;
    }

    /**
     * 获取public key
     *
     * @param url
     * @return
     */
    @SuppressWarnings({"finally"})
    private String executeGet(String url) {
        BufferedReader in = null;

        String content = null;
        try {
            // 定义HttpClient
            @SuppressWarnings("resource")
            DefaultHttpClient client = new DefaultHttpClient();
            // 实例化HTTP方法
            HttpGet request = new HttpGet();
            request.setURI(new URI(url));
            HttpResponse response = client.execute(request);

            in = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
            StringBuffer sb = new StringBuffer("");
            String line = "";
            String NL = System.getProperty("line.separator");
            while ((line = in.readLine()) != null) {
                sb.append(line + NL);
            }
            in.close();
            content = sb.toString();
        } catch (Exception e) {
        } finally {
            if (in != null) {
                try {
                    in.close();// 最后要关闭BufferedReader
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            return content;
        }
    }
  • 校验成功后,获取图片的URL返回给前端
    - 获取图片链接的地址
 /**
     * 获得url链接
     *
     * @param key
     * @return
     */
    public String getUrl(String key) {
        if (StringUtils.isBlank(key)) {
            return "";
        }
        Date expiration = new Date(System.currentTimeMillis() + 3600L * 1000 * 24 * 365 * 10);
        URL url = ossClient.generatePresignedUrl(ossProperties.getBucket(), key, expiration);
        if (url != null) {
            return url.toString();
        }
        return null;
    }

key 为上传文件的文件名。
这里可以设置图片链接的有限期。

====================================================================

以上就是配置服务端签名后直传文件的步骤。

下面是博主基于springboot搭建的一套完成的oss直传的DEMO

码云地址 :https://gitee.com/jack_whh/oss-policy-springboot.git
码云地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值