Spring Boot实战:使用腾讯云 COS 对象存储上传文件

阅读 17
标签: Java

在现代 Web 应用开发中,文件存储是一个绕不开的话题。

随着业务量的增长,传统的将文件存储在应用服务器本地(如硬盘)的方式慢慢会出现弊端:扩展性差、单点故障风险、占用宝贵的服务器磁盘空间,上传下载文件时带宽资源开销大。

对象存储(Object Storage Service, OSS)应运而生。它具有高可用、高可靠、低成本、海量存储等优势。

本篇介绍如何在Spring项目中使用腾讯云对象存储(Cloud Object Storage,COS)来处理文件的上传和下载。

准备工作

首先,需要在腾讯云中开通“对象存储COS”服务,然后,进入对象存储,点击创建存储桶(Bucket):

输入桶名称,建议选择:私有读取(根据业务需求),其他默认。记录下 Bucket名称存储桶地域

接着,获取密钥。在访问管理,API密钥管理中,新建或获取现有的 SecretIdSecretKey

建议使用子账号获取,因为主账号 API 密钥拥有对账号下所有云资源的完全控制权。一旦因保管不当导致泄露,可能会造成服务器被入侵、核心数据资产被加密勒索或删除等安全事件,且无法通过收敛权限快速止损。

OK,准备工作做好了,接下来就是在Spring Boot项目中引入。

Spring Boot 项目集成

首先,我们需要在项目的 pom.xml 文件中引入cos_api

<!-- COS -->
<dependency>
    <groupId>com.qcloud</groupId>
    <artifactId>cos_api</artifactId>
    <version>5.6.227</version>
</dependency>

它是操作 COS 存储桶的 HTTP 客户端 SDK,用于服务端直接读写 COS 里的文件(上传、下载、列举、删除、生成预签名 URL 等)。

另外,如果是前后端分离项目,常常需要由前端浏览器/小程序直传文件,而不是文件先通过服务器后,再由服务器进行上传到COS中。因此,服务端还需要给前端生成一个临时密钥前端用这个临时密钥作为凭证去进行上传。引入cos-sts_api

<!-- COS STS -->
<dependency>
    <groupId>com.qcloud</groupId>
    <artifactId>cos-sts_api</artifactId>
    <version>3.1.1</version>
</dependency>

其作用:调用腾讯云 STS(Security Token Service) 接口,用主密钥换取一份有时效、有权限限制的临时密钥(tmpSecretId + tmpSecretKey + sessionToken)。它不操作存储桶,只做“发凭证”这一件事。

也就是说,整个文件上传的实际过程如下所示:

配置文件编写

接下来,我们将 COS 的相关配置信息放在 application.ymlapplication.properties 中:

tencent:
  cos:
    secretId: # 部署时替换
    secretKey: # 部署时替换
    region: # 部署时替换
    bucket: # 部署时替换
    appId: # 部署时替换
    # 临时密钥有效期: 1分钟
    durationSeconds: 60

这样,在不同环境(开发、测试、生产)下可以轻松切换配置。

其中,appId可以在腾讯云的账号信息中获得。

为了在项目中方便使用,我们在项目的config目录下设置配置如下类:

@Data
@Configuration
@ConfigurationProperties(prefix = "tencent.cos")
public class CosConfig {
    private String appId;
    private String secretId;
    private String secretKey;
    private String bucket;
    private String region;
    private int durationSeconds;

    @Bean
    @Scope("singleton")
    public COSClient cosClient() {
        var cred = new BasicCOSCredentials(this.getSecretId(), this.getSecretKey());

        var config = new ClientConfig(new Region(this.getRegion()));
        config.setMaxConnectionsCount(120);
        config.setConnectionRequestTimeout(60000);

        return new COSClient(cred, config); // http 客户端
    }
}

万事俱备,接下来我们就测试下在服务端能不能正常上传文件到COS中了。

测试文件上传

我们在test目录下写一个测试方法,代码如下:

@Test
void oneUpload() throws IOException {
    Path filePath = Paths.get("uploads/轨道B.pdf");
    byte[] fileBytes = Files.readAllBytes(filePath);

    var key = "abc.pdf";

    var meta = new ObjectMetadata();
    meta.setContentLength(fileBytes.length);
    meta.setContentType("application/pdf");

    cosClient.putObject(
            new PutObjectRequest(
                    cosConfig.getBucket(),
                    key,
                    new ByteArrayInputStream(fileBytes),
                    meta
            )
    );
    System.out.println("upload success: " + key);
}

如果成功,在腾讯云cos页面的该存储桶下就会发现刚才上传的文件。

生成临时密钥

另外,我们还需要生成临时密钥给前端:

/**
 * 生成可以临时上传 excel 到腾讯云COS的临时密钥,返回给前端
 */
@Override
public CosCredentialRes getUploadCredential(ExcelUploadReq req) {
    // 1. 获取配置信息
    String appId = cosConfig.getAppId();
    String secretId = cosConfig.getSecretId();
    String secretKey = cosConfig.getSecretKey();
    String bucket = cosConfig.getBucket();
    String region = cosConfig.getRegion();
    int durationSeconds = cosConfig.getDurationSeconds();

    try {
        // config
        var config = new TreeMap<String, Object>();
        config.put("secretId", secretId);
        config.put("secretKey", secretKey);
        config.put("durationSeconds", durationSeconds);
        config.put("bucket", bucket);
        config.put("region", region);

        // allowPrefixes
        config.put("allowPrefixes", new String[]{"*"});

        // statement
        var statement = new Statement();
        statement.setEffect("allow");
        statement.addActions(new String[]{
                "name/cos:PutObject",
                "name/cos:PostObject",
        });

        // resource:资源级限制,授权前端只能访问 PutObject/PostObject 方法。
        var str = String.format("qcs::cos:%s:uid/%s:%s/*", region, appId, bucket);
        var resources = new String[]{str};
        statement.addResources(resources);

        // policy
        var policy = new Policy();
        policy.setVersion("2.0");
        policy.addStatement(statement);
        config.put("policy", Jackson.toJsonPrettyString(policy));

        // 获取临时密钥
        Response response = CosStsClient.getCredential(config);

        // key
        String excelCOSKey = generateExcelCosKey();

        // 返回凭证
        return new CosCredentialRes()
                .setTmpSecretId(response.credentials.tmpSecretId)
                .setTmpSecretKey(response.credentials.tmpSecretKey)
                .setSessionToken(response.credentials.sessionToken)
                .setStartTime(response.startTime)
                .setExpiredTime(response.expiredTime)
                .setBucket(bucket)
                .setRegion(region)
                .setKey(excelCOSKey);
    } catch (Exception e) {
        throw new IllegalArgumentException("cos-getUploadCredential: " + e.getMessage());
    }
}

上面代码约束了这个临时凭证只能进行PutObjectPostObject操作

STS 临时密钥本质上是 CAM (Cloud Access Management)体系下的“带过期时间的临时身份”。

CAM 是腾讯云统一的权限管理系统,相当于 AWS 的 IAM。所有腾讯云资源(COS、CVM、CDN……)的“谁能对什么资源做什么操作”都由它定义和校验。

文件下载

上传文件功能已经完成,接下来就是下载文件了。

下载文件非常简单:

// 生成COS文件临时访问地址:5分钟有效期
public String generatePreviewUrl(String objectKey) {
    final int MINUTE = 60 * 1000;

    var request = new GeneratePresignedUrlRequest(
            cosConf.getBucket(),
            objectKey,
            HttpMethodName.GET
    );
    var expiration = new Date(System.currentTimeMillis() + 5 * MINUTE);
    request.setExpiration(expiration);

    URL url = cosClient.generatePresignedUrl(request);
    return url.toString();
}

直接生成一个临时访问的URL,前端拿到这个URL后,发起请求即可。

常见错误

另外,在实际使用时,可能会遇到各种错误。

这里我踩到的坑主要有如下几个,附带解决方法:

  • CORS error:找到该存储桶 -> 安全管理 -> 跨域访问 CORS 设置即可。
  • Delete Object 403: 权限管理 -> 存储桶访问权限 -> 将子账号加进去(完全控制)
  • ACCESS Denyed:权限管理 -> Policy权限设置 -> 指定用户,整个存储桶,读写对象 (不含列出对象列表)
可能还有其他的坑,遇到什么,解决就好了。

参考

最后编辑于: 2026-09-03

评论(0条)

(必填)
复制成功