目 录CONTENT

文章目录

web端上传文件-OSS 最佳实践

小张的探险日记
2022-01-13 / 0 评论 / 1 点赞 / 639 阅读 / 8,916 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2022-01-14,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

背景信息

Web端常见的上传方法是用户在浏览器或App端上传文件到应用服务器,应用服务器再把文件上传到OSS。具体流程如下图所示。

image.png

和数据直传到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 。

image.png

STS 支持实现

相关文档
https://help.aliyun.com/document_detail/100624.htm?spm=a2c4g.11186623.0.0.63c65b78wOCosW#concept-xzh-nzk-2gb

  1. App用户登录。App用户和云账号无关,它是App的终端用户,App服务器支持App用户登录。对于每个有效的App用户来说,需要App服务器能定义出每个App用户的最小访问权限。
  2. App服务器请求STS服务获取一个安全令牌(SecurityToken)。在调用STS之前,App服务器需要确定App用户的最小访问权限(用RAM Policy来自定义授权策略)以及凭证的过期时间。然后通过扮演角色(AssumeRole)来获取一个代表角色身份的安全令牌(SecurityToken)。
  3. STS返回给App服务器一个临时访问凭证,包括一个安全令牌(SecurityToken)、临时访问密钥(AccessKeyId和AccessKeySecret)以及过期时间。
  4. App服务器将临时访问凭证返回给App客户端,App客户端可以缓存这个凭证。当凭证失效时,App客户端需要向App服务器申请新的临时访问凭证。例如,临时访问凭证有效期为1小时,那么App客户端可以每30分钟向App服务器请求更新临时访问凭证。
  5. App客户端使用本地缓存的临时访问凭证去请求OSS API。OSS收到访问请求后,会通过STS服务来验证访问凭证,正确响应用户请求。

新建ram 用户

截屏20220114 上午9.50.44.png

截屏20220114 上午9.51.46.png

创建用户并添加权限

AliyunSTSAssumeRoleAccess 和 AliyunOSSFullAccess 权限

image.png

新建角色

image.png

为角色授予上传文件的权限

这里我直接采用了可视化编辑

提供上传 和 删除文件的权限

image.png

生成的 策略内容如下

截屏20220114 上午10.04.37.png

给用户授权角色

选择自定义策略,到这里配置完成,可以到 代码中生成 临时访问密钥(AccessKeyId和AccessKeySecret)

image.png

STS模式代码实现

生成STS SDK 链接地址

https://mvnrepository.com/artifact/com.aliyun/aliyun-java-sdk-sts?spm=a2c4g.11186623.0.0.7c143767gNtXAB

后台代码实现

@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>

效果

1

评论区