阿里云OSS(Object Storage Service,对象存储服务)是一种海量、安全、低成本、高可靠的云存储服务,适合存放任意类型的文件。在实际应用开发中,可方便的用于图片管理、文件管理等等,为应用提供图片访问及文件下载服务。

何为“最佳实践”? 即使用客户端直传临时授权。客户端直传避免客户端传到应用服务器再由应用服务器传到阿里云OSS的两步数据传输。临时授权,用阿里OSS官方的话说就是使用STS(Security Token Service,临时授权访问),可以为第三方应用或子用户(即用户身份由您自己管理的用户)颁发一个自定义时效和权限的访问凭证。通俗的讲,假如授权信息不慎泄露,非法访问最多能持续你自定义的有效实践,可最大限度保证数据安全。

在此假设你已经开通阿里云OSS服务并创建了相关bucket,下面将详细介绍从创建阿里云临时授权子账户到Java Springboot后端集成权限验证,再到前端获取临时权限进行文件上传的过程。其中,前端调用分为两种方式,分别是通过Vue和微信小程序进行OSS上传操作。

1 bucket及子账户配置

1.1 bucket配置

由于是使用客户端直传,涉及上传过程的跨域访问问题,另外为了使上传支持multipart方式,需要在bucket中进行如下配置:

  • 来源:*
  • method:按需勾选
  • allowed header:*
  • expose header:ETag
  • 缓存时间:默认600秒
    在这里插入图片描述

1.2 子账户及相关配置

因为是使用临时授权,所以需要通过子账户配合,关于配置过程,阿里云有个详细介绍,这里不再赘述,可参考 这里

配置完子账户及相关策略之后,你将获得以下信息:

  • 子账户的accesskey
  • 子账户的secret
  • 角色的arn
    另外,开通OSS服务后,bucket方面有已下信息:
  • bucket所属的region信息
  • bucket名称

以上五个信息为实现临时授权必须的条件。

2 Springboot集成STS

通过Springboot应用集成STS并通过指定接口提供临时token。

2.1 添加依赖包

需要在应用的build.gradle文件中dependencies节点下添加以下依赖包(下面是gradle方式,使用maven构建项目类似)。

implementation('com.aliyun:aliyun-java-sdk-core:4.5.1')
implementation('com.aliyun:aliyun-java-sdk-sts:3.0.1')

2.2 配置properties

application.properties配置文件夹中(或者对应启动环境的properties文件中)添加如下配置,即第1步骤中获取到的子账户相关信息及bucket相关信息。

#oss相关配置
oss.region=<oss-cn-hangzhou>
oss.bucket=<bucket名称>
oss.accesskey.id=<accesskey字符串>
oss.accesskey.secret=<secret字符串>
oss.role.arn=<角色arn信息,大致格式为'acs:ram::角色id:role/策略名称'>

2.3 创建生成临时授权token的工具类

创建工具类````,用于根据上一步中的配置信息生成临时token,完整代码如下:

import com.alibaba.fastjson.JSONObject;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import com.aliyuncs.sts.model.v20150401.AssumeRoleRequest;
import com.aliyuncs.sts.model.v20150401.AssumeRoleResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * ali oss临时授权访问
 */
public class OssUtil {
    public static Logger logger = LoggerFactory.getLogger(OssUtil.class);
    public static final String ACCESS_KEY_ID = "ACCESS_KEY_ID";
    public static final String ACCESS_KEY_SECRET = "ACCESS_KEY_SECRET";
    public static final String SECURITY_TOKEN = "SECURITY_TOKEN";
    public static final String EXPIRATION = "EXPIRATION";
    //这里使用cn-hangzhou区域,具体根据实际情况而定
    private static final String REGION = "cn-hangzhou";
    private static final String STS_API_VERSION = "2015-04-01";

    public static JSONObject getCredit(String userName, String roleArn, String accessKeyId, String accessKeySecret, String bucketName) throws ClientException {
        // 用于阿里云后台审计使用的临时名称,可根据实际业务传输,具体内容不影响服务使用
        String roleSessionName = userName;
        //运行时的策略权限,这里将权限放到了最大,可根据实际情况而定。在运行时,实际权限为这里设置的权限和第一步中角色配置的策略权限的交集
        JSONObject policyObject = new JSONObject();
        policyObject.fluentPut("Version", "1");
        List<JSONObject> statements = new ArrayList<>();
        statements.add(new JSONObject().fluentPut("Effect", "Allow").fluentPut("Action", Arrays.asList("oss:PutObject")).fluentPut("Resource", Arrays.asList("acs:oss:*:*:" + bucketName, "acs:oss:*:*:" + bucketName + "/*")));
        policyObject.fluentPut("Statement", statements);
        logger.info("ali policy:{}", policyObject.toString());

        //执行角色授权
        IClientProfile profile = DefaultProfile.getProfile(REGION, accessKeyId, accessKeySecret);
        DefaultAcsClient client = new DefaultAcsClient(profile);
        final AssumeRoleRequest request = new AssumeRoleRequest();
        request.setVersion(STS_API_VERSION);
        request.setRoleArn(roleArn);
        request.setRoleSessionName(roleSessionName);
        request.setPolicy(policyObject.toJSONString());
		//临时授权有效实践,9003600
        request.setDurationSeconds(900L);
        final AssumeRoleResponse response = client.getAcsResponse(request);

        JSONObject jsonObject = new JSONObject();
        jsonObject.put(ACCESS_KEY_ID, response.getCredentials().getAccessKeyId());
        jsonObject.put(ACCESS_KEY_SECRET, response.getCredentials().getAccessKeySecret());
        jsonObject.put(SECURITY_TOKEN, response.getCredentials().getBizSecurityToken());
        jsonObject.put(EXPIRATION, response.getCredentials().getExpiration());
        return jsonObject;
    }
}

2.4 创建控制器

创建控制器OssController,用于提供获取临时token的接口,完整代码如下:
针对图片类型的文件,为了便于管理及使用相关处理功能(如水印、图片重量、裁剪)等,将其放到一个单独的目录,所以下面对文件的存放位置分为图片和普通文件两种。


import com.alibaba.fastjson.JSONObject;
import com.aliyuncs.exceptions.ClientException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/oss")
public class OssController {
    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Value("${oss.region}")
    private String ossRegion;

    @Value("${oss.bucket}")
    private String ossBucket;

    @Value("${oss.accesskey.id}")
    private String ossAccessKeyId;

    @Value("${oss.accesskey.secret}")
    private String ossAccessKeySecret;

    @Value("${oss.role.arn}")
    private String ossRoleArn;

    /**
     * 获取授权信息
     *
     * @param type 授权类型,图片类型或普通文件类型
     * @return
     * @throws ClientException
     */
    @PostMapping("/getCredit")
    public JSONObject getCredit(string type) throws ClientException {
        JSONObject jsonObject = new JSONObject();
        JSONObject creditInfo;
        String basePath;
        switch (type) {
            case image:
                basePath = "image";
                break;
            default:
                basePath = "file";
        }
        creditInfo = OssUtil.getCredit("<根据实际业务生成用户名或者写成静态用户名也可>", ossRoleArn, ossAccessKeyId, ossAccessKeySecret, this.ossBucket);
        //文件存放地域
        jsonObject.put("region", ossRegion);
        //临时访问accessKey
        jsonObject.put("accessKeyId", creditInfo.getString(OssUtil.ACCESS_KEY_ID));
        //临时访问accessKeySecret
        jsonObject.put("accessKeySecret", creditInfo.getString(OssUtil.ACCESS_KEY_SECRET));
        //临时访问
        jsonObject.put("securityToken", creditInfo.getString(OssUtil.SECURITY_TOKEN));
        //临时访问过期时间
        jsonObject.put("expiration", creditInfo.getString(OssUtil.EXPIRATION));
        //bucket名称
        jsonObject.put("bucket", this.ossBucket);
        //文件的存放基目录
        jsonObject.put("basePath", String.format("oss/%s", basePath));
        return jsonObject;
    }
}

创建好控制器后,启动SpringBoot项目,便可通过http://localhost:端口/oss/getCredit地址获取临时授权信息。

3 Vue项目集成OSS文件上传

3.1 依赖项

在Vue项目中集成OSS文件上传需要依赖aliyun-oss-sdk包,可直接在public.index引用标签如下:

<script src="//gosspublic.alicdn.com/aliyun-oss-sdk-5.2.0.min.js"></script>

或者使用包管理工具npm安装ali-oss引用如下:

npm install ali-oss --save

另外,项目中发送请求使用的是axios组件,具体可根据实际情况而定。

3.2 OSS文件上传工具

创建oss.js工具js文件,完整代码如下:

import Vue from 'vue';
/**
/ * 上传一个文件
*/
const uploadAction = async option => {
  //从后台拿到的临时授权信息
  let client = new OSS.Wrapper({
    region: option.token.region,
    accessKeyId: option.token.accessKeyId,
    accessKeySecret: option.token.accessKeySecret,
    stsToken: option.token.securityToken,
    bucket: option.token.bucket
  });

  let filePath = '/' + option.fileName;
  //将多个/替换为1个/,防止oss文件路径中多个/造成空目录
  filePath = filePath.replace(/[\/]+/g, '\/');
  return client.multipartUpload(
    filePath,
    option.file, {
      //上传过程回调,参数为(上传文件信息,上传百分比),可通过option的progress属性传递自定义操作,默认为打印当前上传进度
      progress: function* (p) {
        (option.progress || console.log)(option.file, 'Uploading Progress: ' + p * 100)
      }
    }
  )
}

/**
 * 上传多个文件
 */
const uploadFiles = async (files, fileDir, progress = console.log) => {
  let credit = await Vue.axios.post("/oss/getCredit");
  let urls = [];
  for (let index = 0; index < files.length; index++) {
    let res = await uploadAction({
      //待上传文件信息
      file: files[index],
      //临时访问授权信息
      token: credit.data,
      //上传文件名称,为防止文件被覆盖(oss上传重复文件会覆盖同名文件),使用时间戳生成新的文件名,具体可根据实际业务而定
      fileName: (credit.data.basePath + `/${fileDir}/` + new Date().getTime()),
	  //可选参数,图片上传过程中触发的操作
      progress: progress
    });
    if (res.name) {
      urls.push(`${res.name}`);
    }
  }
  //返回多个文件上传结果,为多个oss文件路径地址(不包含host路径)
  return urls;
}

export default {
  upload: uploadFiles
}

Vue.prototype.$upload = uploadFiles;

3.3 Vue页面中上传文件

在页面中引入上一步中的oss.js或者在main.js中引入都可。使用方式如下,在页面中放置input类型为file的标签,设置change时间如下,选中文件后即可自动执行文件上传。在实际应用中,可根据实际情况使用其他图片或文件上传组件,只需要自定义上传过程为下面的upload方法即可。

<template>
  <div class="oss-demo-cont">
  	<input type="file" multiple="multiple" ref="fileRef" id="file-input" @change="upload($event.target.files,'test')" />
  </div>
</template>
<script>
export default {
  name: "UploadTest",
  data () {
    return {
    }
  },
  methods: {
    async upload (files, dir) {
      //上传完成后的图片地址
      let urls = await this.$upload(files, dir);
      console.log(urls);
    }
  },
  mounted () {
  }
}
</script>

4 微信小程序集成OSS文件上传

4.1 微信小程序域名白名单

为使上传正常运行,需要将OSS的bucket主域名加到小程序域名白名单中。可在OSS控制台中点击对应Bucket,在bucket概览信息中查看对应的bucket域名。如下图:
在这里插入图片描述

然后在小程序管理后台中,将Bucket外网访问域名分别加到上传和下载的合法域名列表中,如下图:
在这里插入图片描述

4.2 创建js工具类

在微信小程序上传OSS文件需要添加crypto-jsjs-base64工具,可参考这里。目前小程序已支持npm包管理,通过npm安装上述两个依赖工具,并在微信开发工具中执行"工具>构建npm",则可在小程序中使用相应依赖包。

创建js文件request.js,完整代码如下:

import crypto from 'crypto-js';
import {
  Base64
} from 'js-base64';
//需替换为实际地址
const ossHost = '<oss bucket 地址>';
//需替换为实际地址
const apiHost = '<后台接口 地址>';
let post = (url, data = {}) => {
  return new Promise((resolve, reject) => {
    doRequest(url, resolve, reject, data);
  });
}

let doRequest = (url, resolve, reject, data = {}, method = "POST") => {
  wx.request({
    url: url,
    data: data,
    method: method,
    header: {
      'Content-Type': 'application/x-www-form-urlencoded',
      'Accept': 'application/json'
    },
    success: res => {
      resolve(res.data);
    },
    fail: error => {
      reject(error);
    },
    complete: () => {}
  });
}

/**
 * 获取oss临时授权信息
 */
let getUploadParams = async () => {
  const date = new Date();
  date.setHours(date.getHours() + 1);
  const policyText = {
    expiration: date.toISOString(),
    conditions: [
      // 限制上传大小。
      ["content-length-range", 0, 1024 * 1024 * 1024],
    ],
  };
  const res = await post(`${apiHost}/oss/getCredit`);
  const policy = Base64.encode(JSON.stringify(policyText));
  const signature = crypto.enc.Base64.stringify(crypto.HmacSHA1(policy, res.data.accessKeySecret));
  return {
    OSSAccessKeyId: res.data.accessKeyId,
    signature,
    policy,
    'x-oss-security-token': res.data.securityToken,
    basePath: res.data.basePath
  }
}

/**
 * 上传一个文件
 * @param {object} param 授权信息
 * @param {string} localFilePath 本地图片路径
 * @param {string} dir 远程目录
 */
let uploadOne = (param, localFilePath, dir) => {
  return new Promise((resolve) => {
    let remotePath = `${param.basePath}/${dir}/${new Date().getTime()}`.replace(/[\/]+/g, '\/');
    wx.uploadFile({
      url: ossHost,
      filePath: localFilePath,
      name: 'file',
      formData: {
        key: remotePath,
        ...param
      },
      success: (res) => {
        console.log('上传结果 ' + localFilePath, res);
        //阿里云上传成功放回状态码为204
        if (res.statusCode === 204) {
          resolve(remotePath);
        } else {
          resolve("");
        }
      },
      fail: err => {
        console.log('上传失败 ' + localFilePath, err);
        resolve("");
      }
    });
  });
}

/**
 * 批量上传图片
 * @param {array} localFilePathList 本地图片地址列表
 * @param {string} uploadDir 远程上传目录
 */
let upload = async (localFilePathList, uploadDir) => {
  let param = await getUploadParams();
  let urlList = [];
  if (localFilePathList.length <= 0) {
    return urlList;
  }
  for (let i = 0; i < localFilePathList.length; i++) {
    if (!localFilePathList[i]) {
      continue;
    }
    //上传到oss的文件路径地址
    let aliurl = await uploadOne(param, localFilePathList[i], uploadDir);
    if (!!aliurl) {
      urlList.push(`${aliurl}`);
    }
  }
  //上传的多个oss文件路径地址
  return urlList;
}
/**
 * 选择图片并上传,返回上传后的最终图片数组
 * @param {*} oraList 原图片数组
 * @param {*} ossSubPath 图片上传oss路径
 */
const chooseImage = (oraList, ossSubPath) => {
  return new Promise((resolve) => {
    wx.chooseImage({
      count: 9 - (oraList || []).length,
      sizeType: ['original', 'compressed'],
      sourceType: ['album', 'camera'],
      success: async (res) => {
        let uploadRes = await upload(res.tempFilePaths, ossSubPath);
        console.log(`upload result:${JSON.stringify(uploadRes)}`);
        let uploaderList = (oraList || []).concat(uploadRes);
        resolve(uploaderList);
      },
      fail: error => {
        console.log(`upload error:${JSON.stringify(error)}`);
        resolve(oraList);
      }
    })
  });
}

export default {
  post,
  upload,
  chooseImage
}

4.3 小程序Page页面上传图片

小程序的Page页面通过点击一个view组件实现图片的选择及上传,具体可根据实际情况使用其他触发组件。wxml文件完整代码如下:

<view>
	<view bindtap='chooseImage'>
		点击选择图片上传
	</view>
</view>

对应的js文件完整代码如下:

//对应上传工具js文件的路径,根据实际情况而定
import request from '../../../utils/request';
Page({

  /**
   * 页面的初始数据
   */
  data: {
    
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad(options) {
    
  },

  /**
   * 生命周期函数--监听页面初次渲染完成
   */
  onReady() {

  },

  /**
   * 生命周期函数--监听页面显示
   */
  onShow() {

  },

  /**
   * 生命周期函数--监听页面隐藏
   */
  onHide() {

  },

  /**
   * 生命周期函数--监听页面卸载
   */
  onUnload() {

  },

  /**
   * 页面相关事件处理函数--监听用户下拉动作
   */
  onPullDownRefresh() {

  },

  /**
   * 页面上拉触底事件的处理函数
   */
  onReachBottom() {

  },

  /**
   * 用户点击右上角分享
   */
  onShareAppMessage() {

  },
  save() {
    util.chooseImage(this.data.oilCardUploaderList, "/test").then(res => {
      console.log('上传结果', res);
    });
  }
})

5 参考

[1] STS临时授权访问OSS
[2] 微信小程序直传实践

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐