阿里云OSS-服务端加签直传说明/示例(SpringBoot)

目录

概述

OSS文件上传方式

1. OSS控制台上传

2. 客户端直传

3. 后端上传

4. 加签直传

服务端加签方式

1. 服务端生成PostObject所需的签名和Post Policy

2.服务端生成STS临时访问凭证

3. 服务端生成PutObject所需的签名URL

实现1:生成PostObject所需的签名

准备

1.OSS服务的开通

2.创建Bucket并设置跨域

3.创建RAM用户并配置权限

服务端代码实现(SpringBoot)

1. 导入依赖 -SDK

2. 编写一个配置类

3.编写一个DTO类

4. 写一个Controller类

apifox获取签名和上传文件

1.访问接口,获取加签参数

2. 使用加签参数上传文件

实现2:使用STS生成临时凭证加签

准备

1. 授予RAM用户调用AssumeRole接口的权限

2. 创建RAM角色

 3. 给RAM角色赋予OSS操作权限

服务端代码实现

1. 导入依赖

2. 编写一个Controller类

apifox的调试

对接前端之前

前端代码

自动上传组件 

手动上传组件

组件测试

Gitee代码仓库


概述

  • 本文说明如何使用加签直传的方式将文件上传到阿里云OSS中,本文会详细的说明阿里云OSS的加签直传参数设置/获取过程以及代码实现过程。

  • 文章最后会提供Vue + SpringBoot的示例代码(Gitee仓库地址)

  • 代码示例提供 STS服务生成临时凭证生成PostObject所需的签名 两种加签方式的实现。

OSS文件上传方式

关于OSS文件上传的方式一般有以下几种:

1. OSS控制台上传

该方式通过OSS提供商的Web管理界面直接上传,无需编写代码,适合小规模、低频率的上传需求。

优势:简单直观,零开发成本,适合非技术人员操作。

劣势:不适合批量上传,无法集成到自有应用中,需要手动登录控制台。 

2. 客户端直传

客户端网页或App直接将文件上传到OSS

优势:减轻服务器负担,节省带宽成本,上传速度快(避免先上传到服务器再中转)。

劣势:需要前端实现上传逻辑,因此存在一定的安全风险(密钥的泄露)

3. 后端上传

文件先上传到自己的服务器再由服务器转发到OSS

优势:控制更严格,可以在服务端进行文件校验、处理,对前端友好,实现简单,可以集成更复杂的业务逻辑。

劣势:服务器需要承担双倍流量,增加服务器负载,上传过程较慢双重传输)。

4. 加签直传

前端直传的一种特殊形式,服务端生成签名,前端使用签名直接上传到OSS结合了前端直传的性能和后端控制的安全性。

优势:保留前端直传的性能优势,服务端可控制上传权限和参数更安,更安全

劣势:实现较复杂,需要前后端配合,签名有效期限制

因此,为了安全性性能两者兼顾,我们一般采用"加签直传"的方式

选择总结:上传文件到OSS需要使用 访问密钥(AccessKey)来完成签名认证,但是在客户端中使用长期有效的访问密钥,可能会导致访问密钥泄露,进而引起安全问题。因此我们采用更安全的"加签直传"方式实现上传。

服务端加签方式

当决定使用"加签直传"的方式上传文件后,实际上服务端的加签的形式有多种,也就是说我们可以有多种加签方式去应付各种不同场景。

下面我们就来了解一下服务端的加签方式: 

1. 服务端生成PostObject所需的签名和Post Policy

原理:服务端生成签名Policy文档,客户端使用这些凭证直接向OSS发起POST请求上传文件,Policy文档定义上传条件(如文件大小限制、存储路径等)。

优势:适合浏览器直传场景,可以严格控制上传条件减轻服务器压力,数据不需要中转。

劣势:签名有效期短,只适用于POST方法上传。

这种方式我们仅需使用accessKeyId 和 secretAccessKey 就能加签获取签名和Policy。

2.服务端生成STS临时访问凭证

原理:通过阿里云STS服务使用主账号密钥获取临时AccessKey、SecretKey和Token,客户端使用这些临时凭证直接访问OSS。

优势:安全性高,主账号密钥不外露,凭证可设置较长过期时间,可设置精细的权限策略。

劣势:实现较复杂,需要额外调用STS服务。

使用STS服务生成临时的凭证的方式呢,我们还需要去创建一个角色并授权后去获取RAN值,然后拿着参数去STS服务获取临时凭证来加签。

3. 服务端生成PutObject所需的签名URL

原理:服务端预签名一个URL,包含必要的认证信息,然后客户端使用这个URL直接通过PUT方法上传文件。

优势:实现简单,客户端实现简单,只需发送标准HTTP请求。

劣势:签名URL有效期有限,只适用于单个操作,对上传条件控制不如Policy灵活。

这种方式也只需要accessKeyId 和 secretAccessKey 来创建预签名URL,然后将将这个URL传给前端去做直传动作。

总结:下面的只会说明两种加签方式,也就是"生成PostObject所需的签名和Post Policy" 以及 "服务端生成STS临时访问凭证" 。


 那么到这呢,我们就了解了文件上传到OSS的几种方式,以及在加签直传中的三种加签方式。

接下来我们就可以去实现后端签名的代码编写了。

实现1:生成PostObject所需的签名

下面会说明使用 AccessKey ID 和 AccessKey Secret进行加签获取签名和Policy等参数。

准备

在开始编写服务端代码之前,我们需要到阿里云OSS控制台中做一些操作,例如创建RAM用户获取密钥、创建Bucket和设置Bucket的跨域等操作

做完准备工作后我们就可以获取到加签时所必需的参数了。

1.OSS服务的开通

看到本文章就默认你已经开通并且了解了OSS的一些基本概念/术语了。

2.创建Bucket并设置跨域

访问Bucket列表 -> 创建Bucket ->  点击刚创建的Bucket

创建Bucket时的地域选择尽量选择OSS服务对象更近的位置。

点击左侧导航 数据安全 > 跨域设置 > 创建规则 

跨域规则的填写(填好保存):

3.创建RAM用户并配置权限

 RAM用户:RAM(Resource Access Management)用户即RAM账号,我们可以为阿里云账号(主账号)创建多个RAM用户并为其授予不同的权限,实现不同RAM用户拥有不同资源访问权限的目的。

所以,我们可以创建一个RAM用户并赋予OSS权限专门用来操作OSS。

使用云账号或账号管理员登录RAM控制台,或参考如下点击进入。

② 在左侧导航栏,选择身份管理 > 用户 ,点击"创建用户"

访问方式区域下,选择使用永久 AccessKey 访问,然后单击确定

创建成功后,保存好AccessKey ID  AccessKey Secret

RAM用户的AccessKey Secret只在创建时显示,后续不支持查看,请妥善保管。 

给刚创建的用户赋予OSS操作权限

 为了方便,上图勾选了赋予OSS的全部权限,但是这样的权限控制细粒度太宽松,在实际开发场景中建议创建自定义权限细粒度更小权限策略。创建自定义权限策略

 至此,我们【实现1代码编写】的准备工作就完成了。

关于敏感信息(密钥)的保存

官方建议先将敏感信息(如accessKeyIdaccessKeySecret配置到环境变量,从而避免在代码里显式地配置,降低泄露风险。

在下面的实现中,我们会直接将密钥信息存放到配置文件中进行读取调用,没有存到环境变量。

因此如果你Copy代码去使用,请注意对于配置文件的可见性保护

服务端代码实现(SpringBoot)

1. 导入依赖 -SDK

<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.17.4</version>
</dependency>

如果使用JDK9以上,还需要导入以下JAXB相关依赖

<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.3.1</version>
</dependency>

<dependency>
    <groupId>javax.activation</groupId>
    <artifactId>activation</artifactId>
    <version>1.1.1</version>
</dependency>

<!-- no more than 2.3.3-->
<dependency>
    <groupId>org.glassfish.jaxb</groupId>
    <artifactId>jaxb-runtime</artifactId>
    <version>2.3.3</version>
</dependency>

2. 编写一个配置类

用来构造OSS的客户端 以及设置上传时的一些参数(如endpoint,bucket 和expireTime等信息)

package com.mh.config;

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import jakarta.annotation.PreDestroy;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Date:2025/5/9
 * author:zmh
 * description: OSS配置
 **/

@Configuration
@Data
public class OssConfig {

    // 到Bucket的【概览】>【访问端口】中查看 
    private String endpoint = "oss-cn-guangzhou.aliyuncs.com";
    
    // 上传到的Bucket名称
    private String bucket = "uni-manage-1746695741875-2576";

    // 指定上传到 OSS 的文件前缀,就是上传到Bucket的哪个目录下
    private String dir = "0509/";
    
    // 指定签名过期时间,单位为秒
    private long expireTime = 3600;
    
    // 构造 host,可以给前端用来拼接图片回显地址
    private String host = "http://" + bucket + "." + endpoint;
    
    // KeyId 和 KeySecret 请到application.yml中进行配置
    @Value("${oss.accessKeyId}")
    private String accessKeyId;

    @Value("${oss.secretAccessKey}")
    private String accessKeySecret;
    
    // OSS客户端,根据上方参数在下面进行构造
    private OSS ossClient;

    @Bean
    public OSS getOssClient() {
        ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
        return ossClient;
    }

    @PreDestroy
    public void onDestroy() {
        ossClient.shutdown();
    }
}

注意看上面注释将自己的参数进行替换。

3.编写一个DTO类

 在该类中我们定义要返回的加签数据,前端可以拿着这个DTO对象去访问所需的直传参数。

package com.mh.dto;

import lombok.Data;

/**
 * Date:2025/5/9
 * author:zmh
 * description: 用来返回PostObject上传所需的签名参数
 **/

@Data
public class OssPolicyDTO {
    private String ossAccessKeyId;
    private String policy;
    private String signature;
    private String dir; // 上传的目录
    private String host;
}

4. 写一个Controller类

在该类中我们编写一个接口,并在其中编写加签并返回数据的逻辑。

package com.mh.controller;

import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.common.utils.BinaryUtil;
import com.aliyun.oss.model.MatchMode;
import com.aliyun.oss.model.PolicyConditions;
import com.mh.config.OssConfig;
import com.mh.dto.OssPolicyDTO;
import lombok.extern.slf4j.Slf4j;
import org.codehaus.jettison.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.UnsupportedEncodingException;
import java.util.Date;

/**
 * Date:2025/5/8
 * author:zmh
 * description: OSS服务
 **/

@CrossOrigin
@RestController
@RequestMapping("/oss")
@Slf4j
public class OssController {

    @Autowired
    private OSS ossClient;

    @Autowired
    private OssConfig ossConfig;

    @GetMapping("/getOssPolicy")
    public OssPolicyDTO getPostObjectSignature(){
        // 初始化DTO对象,后续用来封装返回的数据
        OssPolicyDTO dto = new OssPolicyDTO();

        try {
            // 获取并初始化过期时间
            long expireEndTime = System.currentTimeMillis() + ossConfig.getExpireTime() * 1000;
            Date expiration = new Date(expireEndTime);

            // 创建并新增上传策略:过期时间,上传的目录前缀
            PolicyConditions policyConds = new PolicyConditions();
            policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
            policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, ossConfig.getDir());

            // 获取策略字符串 -> 编码utf字节数组
            String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);
            byte[] binaryData = postPolicy.getBytes("utf-8");
            //再转为字符串
            String encodedPolicy = BinaryUtil.toBase64String(binaryData);

            // 执行签名
            String postSignature = ossClient.calculatePostSignature(postPolicy);

            // 构建返回参数
            dto.setOssAccessKeyId(ossConfig.getAccessKeyId());
            dto.setPolicy(encodedPolicy);
            dto.setSignature(postSignature);
            dto.setDir(ossConfig.getDir());
            dto.setHost(ossConfig.getHost());
        } catch (
                OSSException oe) {
            System.out.println("捕获到一个OSS异常,这意味着您的请求已到达OSS,"
                    + "但由于某种原因被拒绝并返回了错误响应。");
            // 假设此方法存在
            System.out.println("HTTP状态码: " + oe.getRawResponseError());
            System.out.println("错误信息: " + oe.getErrorMessage());
            System.out.println("错误代码: " + oe.getErrorCode());
            System.out.println("请求ID: " + oe.getRequestId());
            System.out.println("主机ID: " + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("捕获到一个客户端异常,这意味着客户端在尝试与OSS通信时"
                    + "遇到了严重的内部问题,例如无法访问网络。");
            System.out.println("错误信息: " + ce.getMessage());
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
        // 返回DTO对象数据
        return dto;
    }

}

至此,我们的服务端代码就编写完了。

下面我们会使用apifox接口测试工具去调用该接口获取上传参数,并进行文件上传测试。

apifox获取签名和上传文件

关于接口访问和文件上传测试,在这先用apifox来获取签名等参数并上传文件。

最后再与vue3 + element-plus进行联调测试。

1.访问接口,获取加签参数

2. 使用加签参数上传文件

在此说明一下key参数该参数应该包含【目录/完整文件名】,这个目录呢是不能随便填写的,目录的定义是在后端的OssConfig中的dir参数定义的。

查看OSS文件列表: 

上传参数值得注意的是:

1. file参数必须在最后。

2. key参数为【目录/文件名】,并且目录名一定要和返回的签名参数dir一致。

3.文件名需要唯一(后面vue示例使用nanoid库保证了文件名唯一),如果上传相同的文件名,第二次为覆盖操作。

 

实现2:使用STS生成临时凭证加签

在这一实现中,我们还而外需要一个参数:角色的ARN值。用来向STS服务请求获取临时凭证。

因此在这我们也还需要做一些准备。

准备

1. 授予RAM用户调用AssumeRole接口的权限

跟上面授予OSS操作权限类似,到用户列表位置给指定用户【新增授权】如下图:

2. 创建RAM角色

进入RAM控制台身份管理 > 角色 

 可信实体类型选择云账号,勾选当前账号 > 确定。

 创建好之后 > 复制保存角色的ARN值

 3. RAM角色赋予OSS操作权限

这里的赋予权限和RAM用户赋予权限一样,为了方便也是在下面我会赋予OSS的全部权限,但是在实际开发中还是建议创建自定义权限策略(例如:只给该角色赋予上传权限)。

 找到角色,点击右边的【新增授权】

勾选AliyunOSSFullAccess(OSS全部权限)> 确定

 至此,我们的准备工作就完成了。

服务端代码实现

1. 导入依赖

<dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>credentials-java</artifactId>
    <version>0.3.4</version>
</dependency>

<dependency>
    <groupId>com.aliyun.kms</groupId>
    <artifactId>kms-transfer-client</artifactId>
    <version>0.1.0</version>
</dependency>

<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.17.4</version>
</dependency>
<dependency>
  <groupId>com.aliyun</groupId>
  <artifactId>sts20150401</artifactId>
  <version>1.1.6</version>
</dependency>

2. 编写一个Controller类

下面贴出来的代码可能会有点长,因为STS的逻辑还是比上面一种加签方式复杂一些。

我会将代码分多部分贴出来但是你要知道的是他们都在同一文件中

ps:建议到文章最下面找到Gitee仓库拉代码下来到IDEA中读会好一些。

首先是Controller类的注解和一些属性

@CrossOrigin
@RestController
@RequestMapping("/oss_sts")
@Slf4j
public class StsController {
    //OSS基础信息 替换为实际的 bucket 名称、 region-id、host。
    String bucket = "uni-manage-1746695741875-2576";
    String region = "cn-guangzhou";
    String host = "http://uni-manage-1746695741875-2576.oss-cn-guangzhou.aliyuncs.com/";

    // 限定上传到OSS的文件前缀(目录)。
    String upload_dir = "0509";

    //指定过期时间,单位为秒。
    Long expire_time = 3600L;

    // 从配置文件application.yml中读取accessKeyId 和 accessKeySecret
    @Value("${oss.accessKeyId}")
    private String accessKeyId;

    @Value("${oss.secretAccessKey}")
    private String accessKeySecret;


    ...其他代码

}

一个函数工具函数

该函数可以先不必深究,大概知道他是用来根据传入的key将传入的data进行计算,然后输出"认证码"作用是在签名的时候用来防止伪造防篡改的。

public static byte[] hmacsha256(byte[] key, String data) {
        try {
            // 初始化HMAC密钥规格,指定算法为HMAC-SHA256并使用提供的密钥。
            SecretKeySpec secretKeySpec = new SecretKeySpec(key, "HmacSHA256");

            // 获取Mac实例,并通过getInstance方法指定使用HMAC-SHA256算法。
            Mac mac = Mac.getInstance("HmacSHA256");
            // 使用密钥初始化Mac对象。
            mac.init(secretKeySpec);

            // 执行HMAC计算,通过doFinal方法接收需要计算的数据并返回计算结果的数组。
            byte[] hmacBytes = mac.doFinal(data.getBytes());

            return hmacBytes;
        } catch (Exception e) {
            throw new RuntimeException("Failed to calculate HMAC-SHA256", e);
        }
    }

第二个工具函数

这个工具函数还是比较好理解吧,将传入的过期秒数转为特定的日期格式字符串。

    /**
     * 通过指定有效的时长(秒)生成过期时间。
     * @param seconds 有效时长(秒)。
     * @return ISO8601 时间字符串,如:"2014-12-01T12:00:00.000Z"。
     */
    public static String generateExpiration(long seconds) {
        // 获取当前时间戳(以秒为单位)
        long now = Instant.now().getEpochSecond();
        // 计算过期时间的时间戳
        long expirationTime = now + seconds;
        // 将时间戳转换为Instant对象,并格式化为ISO8601格式
        Instant instant = Instant.ofEpochSecond(expirationTime);
        // 定义时区为UTC
        ZoneId zone = ZoneOffset.UTC;
        // 将 Instant 转换为 ZonedDateTime
        ZonedDateTime zonedDateTime = instant.atZone(zone);
        // 定义日期时间格式,例如2023-12-03T13:00:00.000Z
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
        // 格式化日期时间
        String formattedDate = zonedDateTime.format(formatter);
        // 输出结果
        return formattedDate;
    }

初始化STS客户端

该方法用于根据accessKeyIdaccessKeySecret endpoint初始化STS客户端,其中的前两个参数在上面的成员属性中有定义并加载配置文件注入值。

    //初始化STS Client
    public com.aliyun.sts20150401.Client createStsClient() throws Exception {
        com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
                // 使用了上面的属性值
                .setAccessKeyId(accessKeyId)
                // 使用了上面的属性值
                .setAccessKeySecret(accessKeySecret);
        // Endpoint 请参考 https://api.aliyun.com/product/Sts
        config.endpoint = "sts.cn-guangzhou.aliyuncs.com";
        return new com.aliyun.sts20150401.Client(config);
    }

获取STS临时凭证

下面代码需要填写角色的ARN值自定义会话名称

    /**
     * 获取STS临时凭证
     * @return AssumeRoleResponseBodyCredentials 对象
     */
    public AssumeRoleResponseBody.AssumeRoleResponseBodyCredentials getCredential() throws Exception {
        com.aliyun.sts20150401.Client client = this.createStsClient();
        com.aliyun.sts20150401.models.AssumeRoleRequest assumeRoleRequest = new com.aliyun.sts20150401.models.AssumeRoleRequest()
                // 填写角色的ARN值
                .setRoleArn("acs:ram::1462366106119483:role/oss-sts")
                .setRoleSessionName("mySession01=");// 自定义会话名称
        com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
        try {
            AssumeRoleResponse assumeRoleResponse = client.assumeRoleWithOptions(assumeRoleRequest, runtime);
            // credentials里包含了后续要用到的AccessKeyId、AccessKeySecret和SecurityToken。
            return assumeRoleResponse.body.credentials; // 返回临时凭证
        } catch (TeaException error) {
            // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
            // 错误 message
            System.out.println(error.getMessage());
            // 诊断地址
            System.out.println(error.getData().get("Recommend"));
            com.aliyun.teautil.Common.assertAsString(error.message);
        } catch (Exception _error) {
            TeaException error = new TeaException(_error.getMessage(), _error);
            // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
            // 错误 message
            System.out.println(error.getMessage());
            // 诊断地址
            System.out.println(error.getData().get("Recommend"));
            com.aliyun.teautil.Common.assertAsString(error.message);
        }
        // 返回一个默认的错误响应对象,避免返回null
        AssumeRoleResponseBody.AssumeRoleResponseBodyCredentials defaultCredentials = new AssumeRoleResponseBody.AssumeRoleResponseBodyCredentials();
        defaultCredentials.accessKeyId = "ERROR_ACCESS_KEY_ID";
        defaultCredentials.accessKeySecret = "ERROR_ACCESS_KEY_SECRET";
        defaultCredentials.securityToken = "ERROR_SECURITY_TOKEN";
        return defaultCredentials;
    }

最后,写一个接口用于获取获取临时凭证并加签返回参数

    @GetMapping("/get_sts_signature")
    public ResponseEntity<Map<String, String>> getPostSignatureForOssUpload() throws Exception {
        // 获取临时凭证
        AssumeRoleResponseBody.AssumeRoleResponseBodyCredentials sts_data = getCredential();
        
        // 取出临时凭证的accessKeyId, accessKeySecret 和 securityToken
        String accesskeyid =  sts_data.accessKeyId;
        String accesskeysecret =  sts_data.accessKeySecret;
        String securitytoken =  sts_data.securityToken;
        
        //获取x-oss-credential里的date,当前日期,格式为yyyyMMdd
        LocalDate today = LocalDate.now();
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
        String date = today.format(formatter);

        //获取x-oss-date
        ZonedDateTime now = ZonedDateTime.now().withZoneSameInstant(ZoneOffset.UTC);
        DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'");
        String x_oss_date = now.format(formatter2);

        // 步骤1:创建policy。
        String x_oss_credential = accesskeyid + "/" + date + "/" + region + "/oss/aliyun_v4_request";

        ObjectMapper mapper = new ObjectMapper();

        Map<String, Object> policy = new HashMap<>();
        policy.put("expiration", generateExpiration(expire_time));

        List<Object> conditions = new ArrayList<>();

        Map<String, String> bucketCondition = new HashMap<>();
        bucketCondition.put("bucket", bucket);
        conditions.add(bucketCondition);

        Map<String, String> securityTokenCondition = new HashMap<>();
        securityTokenCondition.put("x-oss-security-token", securitytoken);
        conditions.add(securityTokenCondition);

        Map<String, String> signatureVersionCondition = new HashMap<>();
        signatureVersionCondition.put("x-oss-signature-version", "OSS4-HMAC-SHA256");
        conditions.add(signatureVersionCondition);

        Map<String, String> credentialCondition = new HashMap<>();
        credentialCondition.put("x-oss-credential", x_oss_credential); // 替换为实际的 access key id
        conditions.add(credentialCondition);

        Map<String, String> dateCondition = new HashMap<>();
        dateCondition.put("x-oss-date", x_oss_date);
        conditions.add(dateCondition);

        conditions.add(Arrays.asList("content-length-range", 1, 10240000));
        conditions.add(Arrays.asList("eq", "$success_action_status", "200"));
        conditions.add(Arrays.asList("starts-with", "$key", upload_dir));

        policy.put("conditions", conditions);
        
        String jsonPolicy = mapper.writeValueAsString(policy);

        // 步骤2:构造待签名字符串(StringToSign)。
        String stringToSign = new String(Base64.getEncoder().encode(jsonPolicy.getBytes())); //encodeBase64
        // System.out.println("stringToSign: " + stringToSign);

        // 步骤3:计算SigningKey。
        byte[] dateKey = hmacsha256(("aliyun_v4" + accesskeysecret).getBytes(), date);
        byte[] dateRegionKey = hmacsha256(dateKey, region);
        byte[] dateRegionServiceKey = hmacsha256(dateRegionKey, "oss");
        byte[] signingKey = hmacsha256(dateRegionServiceKey, "aliyun_v4_request");
        // System.out.println("signingKey: " + BinaryUtil.toBase64String(signingKey));

        // 步骤4:计算Signature。
        byte[] result = hmacsha256(signingKey, stringToSign);
        String signature = BinaryUtil.toHex(result);
        // System.out.println("signature:" + signature);


        Map<String, String> response = new HashMap<>();
        // 将数据添加到 map 中
        response.put("version", "OSS4-HMAC-SHA256");
        response.put("policy", stringToSign);
        response.put("x_oss_credential", x_oss_credential);
        response.put("x_oss_date", x_oss_date);
        response.put("signature", signature);
        response.put("security_token", securitytoken);
        response.put("dir", upload_dir);
        response.put("host", host);
        // 返回带有状态码 200 (OK) 的 ResponseEntity,返回给Web端,进行PostObject操作
        return ResponseEntity.ok(response);
    }

到这,我们的服务端代码也就编写完毕了。

然后就可以像上面的【实现1】进行apifox的调试了。

apifox的调试

调试也先用apifox进行,下面再和vue前端代码进行联调。

1. 调用接口获取直传参数

2. 根据直传参数构造请求上传文件

3.检查OSS文件列表,查看是否上传成功

到这呢,我们的两种加签方式的代码和测试就完毕了。

接下来,我们就可以编写前端vue + element-plus代码对接我们的两个后端接口。

对接前端之前

在对接之前,因为示例会涉及到图片的回显,因此还需要提及一个OSS设置:"读写权限"

进入操作的Bucket > 权限控制 > 读写权限 

我们要将“读写权限”设置为"公共读"。 

因为:如果该值为"私有",则在存储的URL中会混杂着其他的参数,因此URL并不可控导致前端难拼接URL进行图片的获取回显。

可以看一下私有的URL情况:

可以看到URL比较杂,还拼接着超时时间,签名等信息。

设置"公共读"的URL情况:

这下再看会发现:URL简单了很多而且有规律https://bucket名称 + endpoint + 目录/文件名

设置为公共读之后,我们就能预定义访问前缀,最后再拼接上目录/文件名就能够获取图片回显了。

当然,设置了公共读后任何人都可以访问bucket中的数据,因此存在一定的安全和流量问题。

前端代码

使用Vue3 + ElementPlus 进行演示。

下面文章内容的说明策略:

1. 因为上面后端提供有两种加签方式,因此我也写了两个上传组件代码分别使用不同加签方式实现上传。

2. 在两个组件中,我还会使用不同的上传方式进行演示(手动上传自动上传)。

 最终:

自动上传组件 => 【实现1:生成PostObject所需的签名】

手动上传组件 => 【实现2:使用STS生成临时凭证加签的签名】

 

自动上传组件 

自动上传的流程:

1. 在组件加载后获取加签直传参数并存储起来。

2. 当用户点击"上传文件"按钮并选择了文件之后,会自动触发 customUpload函数构造请求参数并上传文件

3. 上传成功构造URL回显图片

下面配合nanoid库对于"唯一的文件名"进行构造

<template>
    <!-- OSS上传组件 -->
    <div class="oss-upload-container">
        <div class="policy_one_title">生成PostObject所需的签名和Policy + 前端自动上传</div>
        <el-upload class="upload-demo" :http-request="customUpload" :limit="fileLimit" :on-exceed="handleExceed"
            :on-success="handleSuccess" :on-error="handleError" :file-list="fileList" :show-file-list="showFileList"
            :multiple="multiple" :accept="accept">
            <el-button type="primary">点击上传</el-button>
            <!-- <template #tip>
                <div class="el-upload__tip">
                    {{ tipText }}
                </div>
            </template> -->
        </el-upload>
        <div class="upload_img" v-if="reshowFlag">
            <img :src="reShowPictureUrl" alt="上传的图片回显" srcset="">
        </div>
    </div>
</template>

<script setup>
import { ref, defineProps, defineEmits, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import axios from 'axios';
import { nanoid } from 'nanoid'

onMounted(() => {
    // 页面加载,获取请求参数暂存到内存中
    getOssSignature();
})

// 父组件调用时传入的参数
const props = defineProps({
    // 限制文件数量
    fileLimit: {
        type: Number,
        default: 1
    },
    // 显示文件列表
    showFileList: {
        type: Boolean,
        default: true
    },
    // 允许多文件上传
    multiple: {
        type: Boolean,
        default: false
    },
    // 接受上传文件的类型,为空表示所有文件类型都可以
    accept: {
        type: String,
        default: ''
    },
    // 文件容量超出时的文本提示
    tipText: {
        type: String,
        default: '请上传文件,大小不超过50MB'
    }
});

// 定义父组件中的触发函数(触发父组件中的回调函数)
const emit = defineEmits(['upload-success', 'upload-error']);
// 上传组件上传的文件列表
const fileList = ref([]);

// 暂存上传的签名参数(这并不是一个好的设计,因为每次页面加载都会获取一次,应该存储到本地持久化,请求前先判断当前存储的参数是否已过期再决定是否重新获取)
const signatureParam = ref({});

// 回显的图片地址
let reShowPictureUrl = ref();
let reshowFlag = ref(false);

// 生成的全局唯一的文件名
let uniqueName = ref("");

// 获取OSS上传参数
async function getOssSignature() {
    const { data: res } = await axios.get('http://localhost:9090/oss/getOssPolicy');
    signatureParam.value = res;
    console.log("页面加载,重新获取签名参数===>", signatureParam.value);
}

// 超出文件数量限制时的处理
const handleExceed = (files) => {
    ElMessage.warning(`最多只能上传 ${props.fileLimit} 个文件`);
};

// 上传成功回调
const handleSuccess = (file) => {
    console.log("上传成功22===>>>", file);
    
    // 构造图片回显
    reShowPictureUrl.value =  `${signatureParam.value.host}/${signatureParam.value.dir}${uniqueName.value}`;
    reshowFlag.value = true;
    ElMessage.success('上传成功');

    
    // 通知父组件执行下一步处理
    emit('upload-success', {
        name: uniqueName.value,
        url: reShowPictureUrl.value
    });
};

// 上传失败回调
const handleError = (err) => {
    ElMessage.error('上传失败,请稍后重试');
    emit('upload-error', err);
};

// 自定义上传处理函数
// 该函数触发的时机:当点击上传按钮并选择文件之后触发(自动:因为上传组件的auto-upload默认为true)
// 如果想要设置为手动触发,也就是手动上传,需要将auto-upload设置为false,然后设置一个上传按钮,给上传组件添加ref,手动调用ref.submit();就会触发该函数
const customUpload = async (options) => {
    const { file, onSuccess, onError } = options;

    console.log("上传的文件", file);

    try {
        // 构造请求参数对象
        const formData = new FormData();

        // 使用nanoid库构造全局唯一的图片名称
        // 获取文件后缀
        const fileExt = file.name.substring(file.name.lastIndexOf('.'))
        // 生成唯一文件名
        const uniqueFileName = `${nanoid()}${fileExt}`
        uniqueName.value = uniqueFileName;

        // 组装表单数据
        formData.append("name", uniqueFileName);
        formData.append("policy", signatureParam.value.policy);
        formData.append("OSSAccessKeyId", signatureParam.value.ossAccessKeyId);
        formData.append("success_action_status", "200");
        formData.append("signature", signatureParam.value.signature);
        formData.append("key", signatureParam.value.dir + uniqueFileName); // 填写目录加文件名,需要注意的是目录需要和后端签名时设置的保持一致
        formData.append("file", file);

        // 发送上传请求到OSS
        const uploadRes = await axios.post(signatureParam.value.host, formData);

        if (uploadRes.status === 200) {
            onSuccess(file);
        } else {
            throw new Error('上传到OSS失败');
        }
    } catch (error) {
        console.error('上传出错:', error);
        onError(error);
    }
};
</script>

<style scoped>
.oss-upload-container {
    margin: 20px 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
}

.el-upload__tip {
    font-size: 12px;
    color: #606266;
    margin-top: 7px;
}

.policy_one_title{
    font-size: 20px;
    font-weight: 600;
    padding-bottom: 20px;
}

.upload_img img{
    width: 150px;
}
</style>

手动上传组件

 对于手动上传组件,需要在<el-upload>中添加auto-uploadfalse,以及添加ref去标识上传组件。

手动上传组件的流程

1. 组件加载后获取加签直传的参数暂存

2. 用户点击"选择文件"按钮去选择要上传的文件,选择后文件不会立即提交

3. 当用户点击"执行上传"按钮后,会根据ref调用上传组件的submit()方法

4. submit()方法执行后就会触发customUpload()函数构造文件上传参数并上传文件

5. 上传成功后执行成功回调去构造URL进行图片回显

<template>
    <!-- OSS上传组件 -->
    <div class="oss-upload-container">
        <div class="policy_one_title">生成STS临时访问凭证 + 前端手动上传</div>
        <el-upload class="upload-demo" :http-request="customUpload" :limit="fileLimit" :on-exceed="handleExceed"
            :on-success="handleSuccess" :on-error="handleError" :file-list="fileList" :show-file-list="showFileList"
            :multiple="multiple" :accept="accept" :auto-upload="false" ref="uploadRef">
            <el-button type="primary">选择文件</el-button>
            <!-- <template #tip>
                <div class="el-upload__tip">
                    {{ tipText }}
                </div>
            </template> -->
        </el-upload>
        <el-button type="success" @click="submitUpload">
            执行上传
        </el-button>
        <div class="upload_img" v-if="reshowFlag">
            <img :src="reShowPictureUrl" alt="上传的图片回显" srcset="">
        </div>
    </div>
</template>

<script setup>
import { ref, defineProps, defineEmits, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import axios from 'axios';

onMounted(() => {
    // 页面加载,获取请求参数暂存到内存中
    getOssSignature();
})

// 父组件调用时传入的参数
const props = defineProps({
    // 限制文件数量
    fileLimit: {
        type: Number,
        default: 1
    },
    // 显示文件列表
    showFileList: {
        type: Boolean,
        default: true
    },
    // 允许多文件上传
    multiple: {
        type: Boolean,
        default: false
    },
    // 接受上传文件的类型,为空表示所有文件类型都可以
    accept: {
        type: String,
        default: ''
    },
    // 文件容量超出时的文本提示
    tipText: {
        type: String,
        default: '请上传文件,大小不超过50MB'
    }
});

// 定义手动上传组件
let uploadRef = ref()

// 定义父组件中的触发函数(触发父组件中的回调函数)
const emit = defineEmits(['upload-success', 'upload-error']);
// 上传组件上传的文件列表
const fileList = ref([]);

// 暂存上传的签名参数(这并不是一个好的设计,因为每次页面加载都会获取一次,应该存储到本地持久化,请求前先判断当前存储的参数是否已过期再决定是否重新获取)
const signatureParam = ref({});

// 回显的图片地址
let reShowPictureUrl = ref();
let reshowFlag = ref(false);

// 生成的全局唯一的文件名
let uniqueName = ref("");

// 手动上传执行函数
const submitUpload = () => {
    uploadRef.value.submit() // 当该函数触发,会执行customUpload函数
}

// 获取OSS上传参数(获STS临时凭证参数)
async function getOssSignature() {
    const { data: res } = await axios.get('http://localhost:9090/oss_sts/get_sts_signature');
    signatureParam.value = res;
    console.log("页面加载,重新获取签名参数===>", signatureParam.value);
}

// 超出文件数量限制时的处理
const handleExceed = (files) => {
    ElMessage.warning(`最多只能上传 ${props.fileLimit} 个文件`);
};

// 上传成功回调
const handleSuccess = (file) => {
    console.log("选择文件成功===>>>", file);
};

function showImg(file) {
    // 构造图片回显
    reShowPictureUrl.value = `${signatureParam.value.host}${signatureParam.value.dir}/${uniqueName.value}`;
    reshowFlag.value = true;
    ElMessage.success('上传成功');

    // 通知父组件执行下一步处理
    emit('upload-success', {
        name: file.name,
        url: reShowPictureUrl.value
    });

}

// 上传失败回调
const handleError = (err) => {
    ElMessage.error('上传失败,请稍后重试');
    emit('upload-error', err);
};

// 自定义上传处理函数
// 该函数触发的时机:当点击上传按钮并选择文件之后触发(自动:因为上传组件的auto-upload默认为true)
// 如果想要设置为手动触发,也就是手动上传,需要将auto-upload设置为false,然后设置一个上传按钮,给上传组件添加ref,手动调用ref.submit();就会触发该函数
const customUpload = async (options) => {
    const { file, onSuccess, onError } = options;

    console.log("上传的文件", file);

    try {
        // 构造请求参数对象
        const formData = new FormData();

        // 使用nanoid库构造全局唯一的图片名称
        // 获取文件后缀
        const fileExt = file.name.substring(file.name.lastIndexOf('.'))
        // 生成唯一文件名
        const uniqueFileName = `${nanoid()}${fileExt}`
        uniqueName.value = uniqueFileName;


        // 组装表单数据
        formData.append("success_action_status", "200");
        formData.append("policy", signatureParam.value.policy);
        formData.append("x-oss-signature", signatureParam.value.signature);
        formData.append("x-oss-signature-version", "OSS4-HMAC-SHA256");
        formData.append("x-oss-credential", signatureParam.value.x_oss_credential);
        formData.append("x-oss-date", signatureParam.value.x_oss_date);
        formData.append("key", signatureParam.value.dir + "/" + file.name); // 文件名
        formData.append("x-oss-security-token", signatureParam.value.security_token);
        formData.append("file", file); // file 必须为最后一个表单域

        // 发送上传请求到OSS
        const uploadRes = await axios.post(signatureParam.value.host, formData);

        if (uploadRes.status === 200) {
            onSuccess(file);
            showImg(file);
        } else {
            throw new Error('上传到OSS失败');
        }
    } catch (error) {
        console.error('上传出错:', error);
        onError(error);
    }
};
</script>

<style scoped>
.oss-upload-container {
    margin: 20px 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
}

.el-upload__tip {
    font-size: 12px;
    color: #606266;
    margin-top: 7px;
}

.policy_one_title {
    font-size: 20px;
    font-weight: 600;
    padding-bottom: 20px;
}

.upload_img img {
    width: 150px;
}
</style>

组件测试

组件调用代码:将两个组件组装上。

<template>
  <div class="app_container">
    <div class="policy_one">
      <OSSUpload></OSSUpload>
    </div>

    <div class="policy_one">
      <OSSUploadSTS></OSSUploadSTS>
    </div>
  </div>
</template>

<script setup>
import OSSUpload from "./components/OSSUpload.vue";
import OSSUploadSTS from "./components/OSSUploadSTS.vue";

</script>

<style scoped>
.policy_one{
  border: solid rgb(110, 175, 210) 1px;
  display: flex;
  justify-content: center;
  align-items: center;
}
</style>

 测试自动上传组件:

 OSS查看上传情况

 手动上传组件测试:

OSS查看上传情况:

 至此,前端代码与后端联调完毕。

 

Gitee代码仓库

Gitee地址:点击访问Gitee-茂和:OSS加签直传https://gitee.com/maohe101/oss_upload_demo

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mao.O

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值