JAVA对接阿里云oss全流程

1. oss配置

参考地址:如何进行服务端签名直传_对象存储(OSS)-阿里云帮助中心

1.1创建RAM用户信息

注意将此信息保存下来

1.2 新增授权

文档中要求分配权限:AliyunSTSAssumeRoleAccess

这里我们把 AliyunOSSReadOnlyAccess也加上,不然无法访问照片

1.3 创建RAM用户角色

这里的角色arn保存好 后面写代码会用到 很多人后来找不到这个东西

1.4 在访问控制创建上传文件的权限策略

1.5 在访问控制为RAM角色授予权限

1.6 在访问控制为RAM角色授予权限

选择你的bucket进行跨域配置

2. 文件上传和下载

2.1 maven依赖

<!-- https://mvnrepository.com/artifact/com.aliyun/credentials-java -->
<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.2 文件上传 和 获取图片信息

参考文档 如何进行服务端签名直传_对象存储(OSS)-阿里云帮助中心

阿里云写的上传和获取图片信息,生成ossClient我用的不太习惯,自己写了一下

获取图片的方式我写在OssUtil.queryPicInfo() 传入文件路径即可,例如2025/06/26/test.png


@RestController
public class OssController {


    @Autowired
    private OssUtil ossUtil;

    //OSS基础信息 替换为实际的 bucket 名称、 region-id、host。
    String bucket = OssUtil.bucket;
    String region = OssUtil.region;
    String host = OssUtil.bucket_host;
    // 设置上传回调URL(即回调服务器地址),必须为公网地址。用于处理应用服务器与OSS之间的通信,OSS会在文件上传完成后,把文件上传信息通过此回调URL发送给应用服务器。
    //限定上传到OSS的文件前缀。


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


    @GetMapping("/get_post_signature_for_oss_upload")
    public Map<String, String> getPostSignatureForOssUpload() throws Throwable {
        AssumeRoleResponseBody.AssumeRoleResponseBodyCredentials sts_data = ossUtil.getCredential();

        String accesskeyid =  sts_data.accessKeyId;
        String accesskeysecret =  sts_data.accessKeySecret;
        String securitytoken =  sts_data.securityToken;

        //获取x-oss-credential里的date,当前日期,格式为yyyyMMdd
        ZonedDateTime today = ZonedDateTime.now().withZoneSameInstant(ZoneOffset.UTC);
        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"));
        //你的文件路径 我这里暂时写死
        String datePath = "2025/06/26/";

        conditions.add(Arrays.asList("starts-with", "$key", datePath));

        policy.put("conditions", conditions);

        String jsonPolicy = mapper.writeValueAsString(policy);

        // 步骤2:构造待签名字符串(StringToSign)。
        String stringToSign = new String(Base64.encodeBase64(jsonPolicy.getBytes()));
        // 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", datePath);
        response.put("host", host);
        // 返回带有状态码 200 (OK) 的 ResponseEntity,返回给Web端,进行PostObject操作
        return response;
    }


    /**
     * 通过指定有效的时长(秒)生成过期时间。
     * @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;
    }


    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);
        }
    }
}


@Service
@Slf4j
public class OssUtil {

    public static String accessKeyId;

    public static String accessKeySecret;
    public static String bucket = "桶名";
    
    //地区 我是北京
    public static String region = "cn-beijing";
    public static String host = "https://oss-cn-beijing.aliyuncs.com";

    public static String bucket_host = "https://桶名.oss-cn-beijing.aliyuncs.com";

    public static String roleArn = "角色处的arn";

    public String queryPicInfo(String filePath) {
        // 创建OSSClient实例。
        // 当OSSClient实例不再使用时,调用shutdown方法以释放资源。
        ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration();
        clientBuilderConfiguration.setSignatureVersion(SignVersion.V4);
        OSS ossClient = OSSClientBuilder.create()
                .endpoint(host)
                .credentialsProvider(new DefaultCredentialProvider(accessKeyId,accessKeySecret))
                .clientConfiguration(clientBuilderConfiguration)
                .region(region)
                .build();

        try {
            // 设置预签名URL过期时间,单位为毫秒。本示例以设置过期时间为1小时为例。
            Date expiration = new Date(new Date().getTime() + 3600 * 1000L);
            // 生成以GET方法访问的预签名URL。本示例没有额外请求头,其他人可以直接通过浏览器访问相关内容。
            URL url = ossClient.generatePresignedUrl(bucket, filePath, expiration);
            return url.toString();
        } catch (OSSException oe) {
            log.info("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            log.info("Error Message:" + oe.getErrorMessage());
            log.info("Error Code:" + oe.getErrorCode());
            log.info("Request ID:" + oe.getRequestId());
            log.info("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            log.info("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            log.info("Error Message:" + ce.getMessage());
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
        return null;
    }




    /**
     * 获取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()
                // 必填,请确保代码运行环境设置了环境变量 OSS_STS_ROLE_ARN
                .setRoleArn(roleArn)
                .setRoleSessionName("ossSession");// 自定义会话名称 随便起
        com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
        try {
            // 复制代码运行请自行打印 API 的返回值
            AssumeRoleResponse response = client.assumeRoleWithOptions(assumeRoleRequest, runtime);
            // credentials里包含了后续要用到的AccessKeyId、AccessKeySecret和SecurityToken。
            return response.body.credentials;
        } catch (TeaException error) {
            // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
            // 错误 message
            log.info(error.getMessage());
            // 诊断地址
            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;
    }

    //初始化STS Client
    public com.aliyun.sts20150401.Client createStsClient() throws Exception {
        // 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考。
        // 建议使用更安全的 STS 方式。
        com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
                // 必填,请确保代码运行环境设置了环境变量 OSS_ACCESS_KEY_ID。
                .setAccessKeyId(accessKeyId)
                // 必填,请确保代码运行环境设置了环境变量 OSS_ACCESS_KEY_SECRET。
                .setAccessKeySecret(accessKeySecret);
        // Endpoint 请参考 https://api.aliyun.com/product/Sts
        config.endpoint = "sts.cn-hangzhou.aliyuncs.com";
        return new com.aliyun.sts20150401.Client(config);
    }



}

 注意 我这里进行了图片压缩,因为oss收费实在是太黑了,按流量收费还是比较贵的,推荐上生产的时候进行图片压缩一下,自己判断一下图片大于多少,进行压缩,实测4M->1M,图片质量在移动端看不受任何影响。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>服务端生成签名上传文件到OSS</title>
</head>
<body>
<div class="container">
    <form>
        <div class="mb-3">
            <label for="file" class="form-label">选择文件:</label>
            <input type="file" class="form-control" id="file" name="file" required />
        </div>
        <button type="submit" class="btn btn-primary">上传</button>
    </form>
</div>

<script type="text/javascript">
    document.addEventListener('DOMContentLoaded', function () {
        const form = document.querySelector("form");
        const fileInput = document.querySelector("#file");

        form.addEventListener("submit", async (event) => {
            event.preventDefault();

            const file = fileInput.files[0];

            if (!file) {
                alert('请选择一个文件再上传。');
                return;
            }

            // 压缩图片 
            const compressedFile = await compressImage(file);

            const filename = file.name;

            fetch("http://localhost:8080/get_post_signature_for_oss_upload", {method: "GET"})
                .then((response) => {
                    if (!response.ok) {
                        throw new Error("获取签名失败");
                    }
                    return response.json();
                })
                .then((data) => {
                    let formData = new FormData();
                    formData.append("success_action_status", "200");
                    formData.append("policy", data.policy);
                    formData.append("x-oss-signature", data.signature);
                    formData.append("x-oss-signature-version", "OSS4-HMAC-SHA256");
                    formData.append("x-oss-credential", data.x_oss_credential);
                    formData.append("x-oss-date", data.x_oss_date);
                    formData.append("key", data.dir + file.name); // 文件名
                    formData.append("x-oss-security-token", data.security_token);
                    formData.append("file", compressedFile); // file 必须为最后一个表单域

                    return fetch(data.host, {
                        method: "POST",
                        body: formData
                    });
                })
                .then((response) => {
                    if (response.ok) {
                        console.log("上传成功");
                        alert("文件已上传");
                    } else {
                        console.log("上传失败", response);
                        alert("上传失败,请稍后再试");
                    }
                })
                .catch((error) => {
                    console.error("发生错误:", error);
                });
        });
        // 图片压缩函数
        async function compressImage(file) {
            return new Promise((resolve) => {
                const reader = new FileReader();
                reader.onload = (e) => {
                    const img = new Image();
                    img.onload = () => {
                        const canvas = document.createElement('canvas');
                        const ctx = canvas.getContext('2d');

                        // 设置目标尺寸(例如最大宽度或高度为 1024)
                        const MAX_SIZE = 1024;
                        let width = img.width;
                        let height = img.height;

                        if (width > height && width > MAX_SIZE) {
                            height *= MAX_SIZE / width;
                            width = MAX_SIZE;
                        } else if (height > MAX_SIZE) {
                            width *= MAX_SIZE / height;
                            height = MAX_SIZE;
                        }

                        canvas.width = width;
                        canvas.height = height;

                        // 绘制图片到 canvas 上(缩放)
                        ctx.drawImage(img, 0, 0, width, height);

                        // 导出为 Blob(JPEG 格式,质量 0.7)
                        canvas.toBlob(
                            (blob) => {
                                // 创建新的 File 对象
                                const compressedFile = new File([blob], file.name, {
                                    type: 'image/jpeg',
                                    lastModified: Date.now()
                                });
                                resolve(compressedFile);
                            },
                            'image/jpeg',
                            0.7 // 压缩质量,可调节 0.5 ~ 0.9
                        );
                    };
                    img.src = e.target.result;
                };
                reader.readAsDataURL(file);
            });
        }
    });
</script>
</body>
</html>

3. 常见错误

  • 访问图片出现 0003-00000001 You have no right to access this object because of bucket acl

建议用阿里云的自定义错误排查 OpenAPI自助诊断-阿里云OpenAPI开发者门户,通常是因为未授权读的权限,1.2中。

  • oss No 'Access-Control-Allow-Origin' header is present on the requested reso、Access to fetch at 'http://oss-cn-beijing.aliyuncs.com/' from origin 'null'

检查bucket跨域处是否配置正确

  • The OSS Access Key Id you provided does not exist in our records

这类问题要么是id和密钥填写错误,要么是x-oss-security-token传递的不对,我发现是因为我忘记维护角色roleArn导致获取token错误导致。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值