背景信息
Web端常见的上传方法是用户在浏览器或App端上传文件到应用服务器,应用服务器再把文件上传到OSS。具体流程如下图所示。
和数据直传到OSS相比,以上方法存在以下缺点:
上传慢:用户数据需先上传到应用服务器,之后再上传到OSS,网络传输时间比直传到OSS多一倍。如果用户数据不通过应用服务器中转,而是直传到OSS,速度将大大提升。而且OSS采用BGP带宽,能保证各地各运营商之间的传输速度。
扩展性差:如果后续用户数量逐渐增加,则应用服务器会成为瓶颈。
费用高:需要准备多台应用服务器。由于OSS上行流量是免费的,如果数据直传到OSS,将节省多台应用服务器的费用。
技术方案
目前通过Web端将文件上传到OSS,有以下三种方案:
- 利用OSS Browser.js SDK将文件上传到OSS
该方案通过OSS Browser.js SDK直传数据到OSS,详细的SDK Demo请参见上传文件。在网络条件不好的状况下可以通过断点续传的方式上传大文件。该方案在个别浏览器上有兼容性问题,目前兼容IE10及以上版本浏览器,主流版本的Edge、Chrome、Firefox、Safari浏览器,以及大部分的Android、iOS、WindowsPhone手机上的浏览器。 - 使用表单上传方式将文件上传到OSS
利用OSS提供的PostObject接口,通过表单上传的方式将文件上传到OSS。该方案兼容大部分浏览器,但在网络状况不好的时候,如果单个文件上传失败,只能重试上传。操作方法请参见PostObject 上传方案 - 通过小程序上传文件到OSS
通过小程序,如微信小程序和支付宝小程序,利用OSS提供的PostObject接口来实现表单上传。操作方式请参见微信小程序直传实践和支付宝小程序直传实践。
Browser.js 简单上传
注意事项
当您使用webpack或browserify等打包工具时,请通过
npm install ali-oss
的方式安装Browser.js SDK。
由于Browser.js SDK通常在浏览器环境下使用,为避免暴露阿里云账号访问密钥(AccessKey ID和AccessKey Secret),强烈建议您使用临时访问凭证的方式执行OSS相关操作。
搭建STS服务的具体操作请参见开发指南中的使用STS临时访问凭证访问OSS使用STS临时访问凭证访问OSS。您可以通过调用STS服务的AssumeRole接口或者使用各语言各语言STS SDK来获取临时访问凭证。临时访问凭证包括临时访问密钥(AccessKey ID和AccessKey Secret)和安全令牌(SecurityToken)。
代码实现
<script >
const oss = require('ali-oss');
const client = oss({//设置OSS参数,输入自己的accessKeyId、accessKeySecret、bucket和region
accessKeyId: 'xxxxxxxxx',
accessKeySecret: 'xxxxxxx',
bucket: 'xxxxxxx-website',
region: 'oss-cn-shanghai'
});
export default {
name: "article",
data(){
return{
OSSPath: 'article/',//要上传至OSS的路径
//是否已发送表示,防止重复点击
sendFlag : false,
article:{
subjectId:'',
title: '',
content: '',
labeNames:[],
//todo 先默认给 哆啦A梦的图
frontCover:'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.jj20.com%2Fup%2Fallimg%2F711%2F101913115130%2F131019115130-5-1200.jpg&refer=http%3A%2F%2Fimg.jj20.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1635414322&t=0f0381ade9c5b4b7a243d83f489756e7',
desc:'',
//-1 草稿;0-正常;
topicStatus:0
}
}
},
methods:{
// 结合markdown 文本编辑器实现图片上传
imgAdd: async function (pos, $file) {
console.log(pos)
console.log($file)
try {
let result = await client.put(this.OSSPath + $file.name, $file);//此处文件命名使用的为时间戳防止冲突,可自行修改规则
console.log(result);
console.log(result.url);//完整的文件url
// 把返回的 文件url 插入 文本中
this.$refs.md.$img2Url(pos, result.url);
} catch (e) {
console.log(e);
alert("上传失败")
}
},
imgDel: function(pos){
console.log(pos)
},
textchange : function(v){
console.log(v)
},
textsave:function(){
localStorage.setItem("aritcle",this.value)
console.log(this.value)
},
subArticle(){
if(!this.sendFlag){
var articleJson = JSON.stringify(this.article);
console.log(articleJson);
const _this = this;
this.$api.post('/topic/v1/publishTopic',articleJson, r => {
console.log(r)
if(r.code === '0000'){
_this.sendFlag = true;
//提示发布成功,跳转到首页
this.$message({
message: '发布成功',
type: 'success',
onClose : function (){
_this.$router.push({ path : '/index'})
}
});
}else{
//提示失败
this.$message({
message: '发布失败',
type: 'error'
});
}
})
}
},
}
}
</script>
隐患问题
accessKeyId 和 accessKeySecret 直接写在本地,有权限安全问题,别人可以浏览器工具查看 前端项目源码看到 accessKeyId 和 accessKeySecret 为此 OSS 提供了 STS 模式,保证前端只能拿到 临时有效的 accessKeyId 和 accessKeySecret 。
STS 支持实现
- App用户登录。App用户和云账号无关,它是App的终端用户,App服务器支持App用户登录。对于每个有效的App用户来说,需要App服务器能定义出每个App用户的最小访问权限。
- App服务器请求STS服务获取一个安全令牌(SecurityToken)。在调用STS之前,App服务器需要确定App用户的最小访问权限(用RAM Policy来自定义授权策略)以及凭证的过期时间。然后通过扮演角色(AssumeRole)来获取一个代表角色身份的安全令牌(SecurityToken)。
- STS返回给App服务器一个临时访问凭证,包括一个安全令牌(SecurityToken)、临时访问密钥(AccessKeyId和AccessKeySecret)以及过期时间。
- App服务器将临时访问凭证返回给App客户端,App客户端可以缓存这个凭证。当凭证失效时,App客户端需要向App服务器申请新的临时访问凭证。例如,临时访问凭证有效期为1小时,那么App客户端可以每30分钟向App服务器请求更新临时访问凭证。
- App客户端使用本地缓存的临时访问凭证去请求OSS API。OSS收到访问请求后,会通过STS服务来验证访问凭证,正确响应用户请求。
新建ram 用户
创建用户并添加权限
AliyunSTSAssumeRoleAccess 和 AliyunOSSFullAccess 权限
新建角色
为角色授予上传文件的权限
这里我直接采用了可视化编辑
提供上传 和 删除文件的权限
生成的 策略内容如下
给用户授权角色
选择自定义策略,到这里配置完成,可以到 代码中生成 临时访问密钥(AccessKeyId和AccessKeySecret)
STS模式代码实现
生成STS SDK 链接地址
后台代码实现
@Slf4j
@Service
public class AliOssServiceImpl implements FileService {
// STS接入地址,例如sts.cn-hangzhou.aliyuncs.com。
@Value("${file.ali-oss.endpoint}")
private String endpoint;
// 填写步骤1生成的访问密钥AccessKey ID和AccessKey Secret。
@Value("${file.ali-oss.accessKeyId}")
private String accessKeyId;
@Value("${file.ali-oss.accessKeySecret}")
private String accessKeySecret;
// 填写步骤3获取的角色ARN。
@Value("${file.ali-oss.roleArn}")
private String roleArn;
// 自定义角色会话名称,用来区分不同的令牌,例如可填写为SessionTest。
@Value("${file.ali-oss.roleSessionName}")
private String roleSessionName;
// regionId表示RAM的地域ID。以华东1(杭州)地域为例,regionID填写为cn-hangzhou。也可以保留默认值,默认值为空字符串("")。
@Value("${file.ali-oss.regionId}")
private String regionId;
@Override
public HashMap<String,String> getSTS() {
// 以下Policy用于限制仅允许使用临时访问凭证向目标存储空间examplebucket上传文件。
// 临时访问凭证最后获得的权限是步骤4设置的角色权限和该Policy设置权限的交集,即仅允许将文件上传至目标存储空间examplebucket下的exampledir目录。
// 这里是 配置的角色(oss) 的策略内容 ,可更改
String policy = "{\n" +
" \"Version\": \"1\",\n" +
" \"Statement\": [\n" +
" {\n" +
" \"Effect\": \"Allow\",\n" +
" \"Action\": [\n" +
" \"oss:PutObject\",\n" +
" \"oss:DeleteObject\"\n" +
" ],\n" +
" \"Resource\": \"acs:oss:*:xxxxxxxxxxx:*/*\"\n" +
" }\n" +
" ]\n" +
"}";
try {
// 添加endpoint。适用于Java SDK 3.12.0及以上版本。
DefaultProfile.addEndpoint(regionId, "Sts", endpoint);
IClientProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, accessKeySecret);
// 构造client。
DefaultAcsClient client = new DefaultAcsClient(profile);
final AssumeRoleRequest request = new AssumeRoleRequest();
request.setMethod(MethodType.POST);
request.setRoleArn(roleArn);
request.setRoleSessionName(roleSessionName);
request.setPolicy(policy); // 如果policy为空,则用户将获得该角色下所有权限。
request.setDurationSeconds(3600L); // 设置临时访问凭证的有效时间为3600秒。
final AssumeRoleResponse response = client.getAcsResponse(request);
return JsonUtils.toBean(JsonUtils.toString(response.getCredentials()),HashMap.class);
} catch (ClientException e) {
log.error("获取 STS 出错:{}",e.getErrMsg());
}
return null;
}
}
前端代码实现
这里结合 mavon-editor markdown文本编辑器上传文件,首先获取到
<template>
<div>
<div style="margin-bottom: 12px;">
<el-input style="width: 90%" maxlength="100" v-model="article.title" placeholder="请输入文章标题..."></el-input>
<!-- <el-button plain>草稿箱</el-button>-->
<el-button type="primary" style="margin-left: 40px;width: 7%" @click="subArticle">发布</el-button>
</div>
<mavon-editor ref="md" @change="textchange" @save="textsave" v-model="article.content"
@imgAdd="imgAdd"
@imgDel="imgDel"/>
</div>
</template>
<script >
const oss = require('ali-oss');
export default {
name: "article",
created: function(){
this.getSTS()
// 3000 秒更新一次
this.timer = setInterval(this.getSTS, 1000 * 3000);
},
data(){
return{
OSSPath: 'article/',//要上传至OSS的路径
ossConfigObj : {},
//是否已发送表示,防止重复点击
sendFlag : false,
article:{
subjectId:'',
title: '',
content: '',
labeNames:[],
//todo 先默认给 哆啦A梦的图
frontCover:'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.jj20.com%2Fup%2Fallimg%2F711%2F101913115130%2F131019115130-5-1200.jpg&refer=http%3A%2F%2Fimg.jj20.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1635414322&t=0f0381ade9c5b4b7a243d83f489756e7',
desc:'',
//-1 草稿;0-正常;
topicStatus:0
}
}
},
methods:{
getSTS : function(){
// 去获取 STS token
this.$api.get('/oss/v1/getSTS',{}, r => {
this.ossConfigObj = {
accessKeyId: r.data.accessKeyId,
accessKeySecret: r.data.accessKeySecret,
stsToken: r.data.securityToken,
}
})
},
imgAdd: async function (pos, $file) {
const client = oss({//设置OSS参数,输入自己的accessKeyId、accessKeySecret、bucket和region
accessKeyId: this.ossConfigObj.accessKeyId,
accessKeySecret: this.ossConfigObj.accessKeySecret,
bucket: 'doraemon-website',
stsToken: this.ossConfigObj.stsToken,
region: 'oss-cn-shanghai'
});
try {
// 时间戳/
//获取扩展名
var fileArr = $file.name.split('.')
let extension = '.' + fileArr[fileArr.length-1]
var date = new Date()
let result = await client.put(this.OSSPath + date.getFullYear()+(date.getMonth()+1)+date.getDate()+ '/' +Date.parse(new Date()) + extension, $file);//此处文件命名使用的为时间戳防止冲突,可自行修改规则
console.log(result);
console.log(result.url);//完整的文件url
this.$refs.md.$img2Url(pos, result.url);
} catch (e) {
console.log(e);
alert("上传失败")
}
},
imgDel: function(pos){
console.log(pos)
},
textchange : function(v){
console.log(v)
},
textsave:function(){
localStorage.setItem("aritcle",this.value)
console.log(this.value)
},
subArticle(){
if(!this.sendFlag){
var articleJson = JSON.stringify(this.article);
console.log(articleJson);
const _this = this;
this.$api.post('/topic/v1/publishTopic',articleJson, r => {
console.log(r)
if(r.code === '0000'){
_this.sendFlag = true;
//提示发布成功,跳转到首页
this.$message({
message: '发布成功',
type: 'success',
onClose : function (){
_this.$router.push({ path : '/index'})
}
});
}else{
//提示失败
this.$message({
message: '发布失败',
type: 'error'
});
}
})
}
},
}
}
</script>
<style scoped>
</style>
评论区