阿里云OSS STS最佳实践,看这一篇就够了
阿里云OSS(Object Storage Service,对象存储服务)是一种海量、安全、低成本、高可靠的云存储服务,适合存放任意类型的文件。在实际应用开发中,可方便的用于图片管理、文件管理等等,为应用提供图片访问及文件下载服务。何为“最佳实践”? 即使用客户端直传和临时授权。客户端直传避免客户端传到应用服务器再由应用服务器传到阿里云OSS的两步数据传输。临时授权,用阿里OSS官方的话说就是使用
阿里云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());
//临时授权有效实践,从900到3600
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-js
和js-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] 微信小程序直传实践
更多推荐
所有评论(0)