OSS-服务端签名Web端直传+STS获取临时凭证+POST签名v4版本开发过程中的细节

这里写自定义目录标题

本文主要结合服务端STS获取临时凭证(签名)直传官方文档对开发中比较容易出错的地方加以提醒;建议主要还是以官方文档为主。本文中的代码几乎就是官方给的代码示例。

阅读前请先比对OSS版本,这可能关系到本文中列举的坑是否能解决你的问题

xml 复制代码
		<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>

只不过重要的OSS基本参数官方建议给到环境变量中;本文直接将重要的OSS基础信息给到了Spring主配置文件中(方便后期迁移到Nacos的配置中心管理),知道了这一点,在接下来代码阅读的过程中看到如下的代码片段也就不陌生了。

接下来也将OSS基本参数封装成AliOSSProp的代码顺手贴出来

java 复制代码
/**
 * oss的基础信息
 * prefix = "alibaba.oss"表示Spring、Springboot主配置文件中以alibaba.oss打头的自定义参数值,都将一 一映射到当前类的属性上
 */
@ConfigurationProperties(prefix = "alibaba.oss")
@Component
@Getter
@Setter
public class AliOSSProp {

    private String accessKeyId;

    private String secretAccessKey;

    /**
     * 确保获取临时访问凭证时Endpoint使用STS域名,例如String endpoint = "sts.cn-hangzhou.aliyuncs.com"。
     * 更多信息,请参见步骤五:获取临时访问凭证。
     * https://help.aliyun.com/document_detail/100624.html?spm=api-workbench.Troubleshoot.0.0.5ba77185ILElOT#section-5xa-zdn-s0q
     */
    private String endpoint;

    private String bucket;

    /**
     * RAM 访问控制/身份管理/角色/ARN属性值
     */
    private String roleArn;

    private String region;
}

配置OSS

这个在官方文档中有很详细的教程,不多赘述

服务端代码

服务端最主要的事情:使用STS生成一个临时的凭证给客户端使用;客户端拿到该临时凭证就可以直接将文件上传至OSS

初始化STS Client

这里有个点需要注意:endpoint 需要添加sts.前缀,

考虑到STS Client只需要初始化一次,所以将其注册为一个Bean

java 复制代码
@Bean
    public com.aliyun.sts20150401.Client ossStsClient() throws Exception {
        // 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考。
        com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
                // 必填,请确保代码运行环境设置了环境变量 OSS_ACCESS_KEY_ID。
                .setAccessKeyId(aliOSSProp.getAccessKeyId())
                // 必填,请确保代码运行环境设置了环境变量 OSS_ACCESS_KEY_SECRET。
                .setAccessKeySecret(aliOSSProp.getSecretAccessKey());
        // Endpoint 请参考 https://api.aliyun.com/product/Sts
        // 确保获取临时访问凭证时Endpoint使用STS域名
        config.endpoint = "sts."+aliOSSProp.getEndpoint();
        return new com.aliyun.sts20150401.Client(config);
    }

获取STS临时凭证

这部分没有什么需要注意的点,可以完全照搬

java 复制代码
import com.aliyun.sts20150401.models.AssumeRoleResponseBody;
import com.aliyun.oss.OSSException;

/**
     * 获取STS临时凭证
     * @return AssumeRoleResponseBodyCredentials 对象
     */
    private AssumeRoleResponseBody.AssumeRoleResponseBodyCredentials getCredential(){
        com.aliyun.sts20150401.models.AssumeRoleRequest assumeRoleRequest = new com.aliyun.sts20150401.models.AssumeRoleRequest()
                // 必填,请确保代码运行环境设置了环境变量 OSS_STS_ROLE_ARN
                .setRoleArn(aliOSSProp.getRoleArn())
                .setRoleSessionName("idooyRoleSessionName");// 自定义会话名称
        com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
        try {
            // 复制代码运行请自行打印 API 的返回值
            com.aliyun.sts20150401.models.AssumeRoleResponse response = ossStsClient.assumeRoleWithOptions(assumeRoleRequest, runtime);
            // credentials里包含了后续要用到的AccessKeyId、AccessKeySecret和SecurityToken。
            return response.body.credentials;
        } catch (TeaException error) {
            // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
            // log.error("STS方式获取临时凭证失败原因===》{}",error.getMessage());
            throw new OSSException("STS方式获取临时凭证失败原因===》"+error.getMessage());
        } catch (Exception e) {
            throw new OSSException(e.getMessage());
        }
    }

创建policy计算SigningKey

这部分的代码很长,但是注释很清晰;当然也不需要完全搞懂,照搬就好。

  • 这部分代码中的魔法值(String字面量)要小心,别无意间给修改或者给粘错了
  • 这部分使用了三种日期格式,分别是:"yyyyMMdd'T'HHmmss'Z'"、"yyyyMMdd"、"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"。注意区分
  • uploadDir 变量值请自定义修改
  • 官方直接将结果塞进了map中,本文将其封装为了一个DTO对象
java 复制代码
    @Resource
    private com.aliyun.sts20150401.Client ossStsClient;


    /**
     *     指定过期时间,单位为秒。
     */
    private static final Long EXPIRE_TIME = 3600L;

    private static final String TIME_FORMAT_PATTERN_1 = "yyyyMMdd'T'HHmmss'Z'";
    /**
     * 定义日期时间格式
     */
    private static final String TIME_FORMAT_PATTERN_2 = "yyyyMMdd";


    @Resource
    AliOSSProp aliOSSProp;
    /**
     * 还是"服务端验签web端直传的方式"只不过采用了更加安全的STS方式
     * @return
     */
    @Override
    public STSPolicyDTO getSTSUploadPolicy() throws JsonProcessingException {

        String host = "https://" + aliOSSProp.getBucket() + "." + aliOSSProp.getEndpoint();
        // 设置上传到OSS文件的前缀,可置空此项。置空后,文件将上传至Bucket的根目录下;每一天产生一个文件夹
        // brand/是自定义目录
        String uploadDir = "brand/"+LocalDate.now();
        String region = aliOSSProp.getRegion();
        // 临时的凭证信息
        AssumeRoleResponseBody.AssumeRoleResponseBodyCredentials credential = getCredential();
        String accessKeyId =  credential.getAccessKeyId();
        String accessKeySecret =  credential.getAccessKeySecret();
        String securityToken =  credential.getSecurityToken();

        //获取x-oss-credential里的date,当前日期,格式为yyyyMMdd
        ZonedDateTime today = ZonedDateTime.now().withZoneSameInstant(ZoneOffset.UTC);
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(TIME_FORMAT_PATTERN_2);
        String date = today.format(formatter);
        //获取x-oss-date
        ZonedDateTime now = ZonedDateTime.now().withZoneSameInstant(ZoneOffset.UTC);
        DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern(TIME_FORMAT_PATTERN_1);
        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", OSSUtil.generateExpiration(EXPIRE_TIME));

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

        Map<String, String> bucketCondition = new HashMap<>();
        bucketCondition.put("bucket", aliOSSProp.getBucket());
        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", uploadDir));

        policy.put("conditions", conditions);

        String jsonPolicy = mapper.writeValueAsString(policy);

        // 步骤2:构造待签名字符串(StringToSign)。
        // String stringToSign = new String(Base64.encodeBase64(jsonPolicy.getBytes()));

        String stringToSign = cn.hutool.core.codec.Base64.encode(jsonPolicy.getBytes());
        // System.out.println("stringToSign: " + stringToSign);

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

        // 步骤4:计算Signature。
        byte[] result = OSSUtil.getHmacSHA256(signingKey, stringToSign);
        String signature = BinaryUtil.toHex(result);

        return new STSPolicyDTO()
                .setPolicy(stringToSign)
                .setX_oss_credential(x_oss_credential)
                .setX_oss_date(x_oss_date)
                .setSignature(signature)
                .setSecurity_token(securityToken)
                .setDir(uploadDir)
                .setHost(host);
    }

OSSUtil.java

java 复制代码
public class OSSUtil {

    private OSSUtil() {
    }


    /**
     * 定义日期时间格式,例如2023-12-03T13:00:00.000Z
     */
    public static final String TIME_FORMAT_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";



    public static byte[] getHmacSHA256(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(TIME_FORMAT_PATTERN);
        // 格式化日期时间
        String formattedDate = zonedDateTime.format(formatter);
        // 输出结果
        return formattedDate;
    }

}

STSPolicyDTO.java

java 复制代码
@Getter
@Setter
@Accessors(chain = true)
public class STSPolicyDTO {

        /**
         * 官方代码示例中给的""OSS4-HMAC-SHA256""
         */
        private String version="OSS4-HMAC-SHA256";
        private String policy;
        private String x_oss_credential;
        private String x_oss_date;
        private String signature;
        private String security_token;
        private String dir;
        private String host;
}

提供接口

java 复制代码
@RestController
@RequestMapping("/oss")
@Api("OSS文件上传验签")
public class AliOSSController {

    final AliOSSService ossService;

    public AliOSSController(AliOSSService ossService) {
        this.ossService = ossService;
    }

    /**
     * oss上传文件:使用服务端验签web直传的方式
     * 该接口提供web直传所必须的验签信息
     * 文档地址:https://help.aliyun.com/zh/oss/use-cases/obtain-signature-information-from-the-server-and-upload-data-to-oss?spm=a2c4g.11186623.help-menu-31815.d_6_1_0_0.386b4acdkrwKDc&scm=20140722.H_31926._.OR_help-T_cn~zh-V_1
     * @return
     */
    @GetMapping("policy")
    @ApiOperation("STS验签V4版")
    public R<STSPolicyDTO> policy() throws JsonProcessingException {
        // UploadPolicyDTO uploadPolicy = ossService.getUploadPolicy();
        STSPolicyDTO uploadPolicy = ossService.getSTSUploadPolicy();
        return R.ok().data(uploadPolicy);
    }

}

接口的响应数据示例

json 复制代码
{
    "success": true,
    "code": 2000,
    "message": "成功",
    "data": {
        "version": "OSS4-HMAC-SHA256",
        "policy": "eyJleHBpcmF0aW9uIjoiMjAyNS0wNy0zMFQxNToyMjowNy4wMDBaIiwiY29uZGl0aW9ucyI6W3siYnVja2V0IjoiaWRvb3ktbWFsbCJ9LHsieC1vc3Mtc2VjdXJpdHktdG9rZW4iOiJDQUlTelFKMXE2RnQ1QjJ5ZlNqSXI1blFFZXo1aDVzVjRZYXpZM1BjZ1ZFZGFkcGl2cFB5aWp6MklIaE1lWEJoQStrZHR2b3duMnBaN3Z3Y2xyMXlSNWhDVkhiRGFjWkw0NDlNOEFTblJZUEV0cFFiWm1DelpjYjNkMUtJQWp2WGdlWHdBWXlnUHY2L0Y5NnBiMWZiN0Z3UnBaTHhhVFNsV1hHOExKU05rdVFKUjk4TFh3NitIMUVrYlpVc1VXa0Vrc0lCTW1iTFB2dUFLd1BqaG5HcWJIQmxvUTFoazJoeW04L2RxNCsra2tPRzBnU2xsYkJPKzltdWNzUDdNWmxXVWMwaEE0dnY3b3RmYmJIYzFTTmMwUjlPK1pwdGdiWk1rVFc5NVluRlhnQUF1VXpkYTdTTHFZYytkRlVuZk00OUFMVUJzL1gyME9CZ3Z1dmFtNVJIYk9USnNUek1PczYyWmZkRG9LT3NjSXZCWHI2eUpRamxvSElPQzZpd0xHL3pxUzBtVjJBNTlmOG1GQ0haMG9hWjZOSDlvOXdiODBKR0ZZRURvL1phcHNKdXFzSkpPdUtlcEE2QVZ0VW44QVhuSzkwYWdBRWloRDByMzE0TnNadUp6UHFkbkF3aUVXWjhWdWFvWlZUUzl0eTdmTW1vUTRIaGsrODE3U3BqeWVvMloycGo3T21zaFVWRmpKNFN5cjVsMWo2VFM3aVBqcFZlRG1xZjhtZ0tRc2F0bFdReHFwdk9MQmIray9QTktNei9kRXA4RFF3clg3S2x6b1RVRTA4bndKK0dqeEFFd0FkVWh3blBBbk14dVlXTEZrdUtZQ0FBIn0seyJ4LW9zcy1zaWduYXR1cmUtdmVyc2lvbiI6Ik9TUzQtSE1BQy1TSEEyNTYifSx7Ingtb3NzLWNyZWRlbnRpYWwiOiJTVFMuTlplWlZNakQ0VkRYYVVtZVFIZVVOUVZZaC8yMDI1MDczMC9jbi1jaGVuZ2R1L29zcy9hbGl5dW5fdjRfcmVxdWVzdCJ9LHsieC1vc3MtZGF0ZSI6IjIwMjUwNzMwVDE0MjIwN1oifSxbImNvbnRlbnQtbGVuZ3RoLXJhbmdlIiwxLDEwMjQwMDAwXSxbImVxIiwiJHN1Y2Nlc3NfYWN0aW9uX3N0YXR1cyIsIjIwMCJdLFsic3RhcnRzLXdpdGgiLCIka2V5IiwiYnJhbmQvMjAyNS0wNy0zMCJdXX0=",
        "x_oss_credential": "STS.NZeZVMjD4VDXaUmeQHeUNQVYh/20250730/cn-chengdu/oss/aliyun_v4_request",
        "x_oss_date": "20250730T142207Z",
        "signature": "e583ba3040d9ac4544b4008f7193edf30fdef75cca6d4ca0e449150cf4904165",
        "security_token": "CAISzQJ1q6Ft5B2yfSjIr5nQEez5h5sV4YazY3PcgVEdadpivpPyijz2IHhMeXBhA+kdtvown2pZ7vwclr1yR5hCVHbDacZL449M8ASnRYPEtpQbZmCzZcb3d1KIAjvXgeXwAYygPv6/F96pb1fb7FwRpZLxaTSlWXG8LJSNkuQJR98LXw6+H1EkbZUsUWkEksIBMmbLPvuAKwPjhnGqbHBloQ1hk2hym8/dq4++kkOG0gSllbBO+9mucsP7MZlWUc0hA4vv7otfbbHc1SNc0R9O+ZptgbZMkTW95YnFXgAAuUzda7SLqYc+dFUnfM49ALUBs/X20OBgvuvam5RHbOTJsTzMOs62ZfdDoKOscIvBXr6yJQjloHIOC6iwLG/zqS0mV2A59f8mFCHZ0oaZ6NH9o9wb80JGFYEDo/ZapsJuqsJJOuKepA6AVtUn8AXnK90agAEihD0r314NsZuJzPqdnAwiEWZ8VuaoZVTS9ty7fMmoQ4Hhk+817Spjyeo2Z2pj7OmshUVFjJ4Syr5l1j6TS7iPjpVeDmqf8mgKQsatlWQxqpvOLBb+k/PNKMz/dEp8DQwrX7KlzoTUE08nwJ+GjxAEwAdUhwnPAnMxuYWLFkuKYCAA",
        "dir": "brand/2025-07-30",
        "host": "目标bucket的公网访问域名"
    }
}

Apifox模拟Web端文件直传

文件上传的地址,就是你自己目标bucket的公网访问域名,也可以直接从上面接口的响应字段host获取,这部分就是结合自己的前端项目自行开发。将上传的参数名称做个简单的说明。

请求参数名称:

json 复制代码
{
                "success_action_status", "200"
                "policy", data.policy
               "x-oss-signature", data.signature
                "x-oss-signature-version", "OSS4-HMAC-SHA256"
               "x-oss-credential", data.x_oss_credential
               "x-oss-date", data.x_oss_date
                "key", data.dir + file.name // 文件名
                "x-oss-security-token", data.security_token
                "file", file); // file 必须为最后一个表单域
}                

没啥文章功底,主要是站在自己的角度一通记录而已;多包涵,欢迎在评论区讨论共同学习。

相关推荐
余杭子曰19 分钟前
组件设计模式:聪明组件还是傻瓜组件?
前端
杨超越luckly26 分钟前
HTML应用指南:利用GET请求获取全国小米之家门店位置信息
前端·arcgis·html·数据可视化·shp
海绵宝龙34 分钟前
axios封装对比
开发语言·前端·javascript
Data_Adventure35 分钟前
setDragImage
前端·javascript
南岸月明39 分钟前
七月复盘,i人副业自媒体成长笔记:从迷茫到觉醒的真实经历
前端
静水流深LY41 分钟前
Vue2学习-el与data的两种写法
前端·vue.js·学习
玲小珑1 小时前
Next.js 教程系列(二十一)核心 Web Vitals 与性能调优
前端·next.js
YGY Webgis糕手之路1 小时前
Cesium 快速入门(八)Primitive(图元)系统深度解析
前端·经验分享·笔记·vue·web
懋学的前端攻城狮1 小时前
从 UI = f(state) 到 Fiber 架构:解构 React 设计哲学的“第一性原理”
前端·react.js·前端框架
三原1 小时前
6年前端学习Java Spring boot 要怎么学?
java·前端·javascript