当前位置: 首页 > news >正文

springboot整合腾讯云cos对象存储,获取临时密钥,前端直传图片文件

文件上传有两种,一种是给后端传腾讯云cos,另一种是前端值传腾讯云cos,后端相对安全,密钥不会被盗取,前端就直接暴露了。那么可以考虑通过永久性密钥申请临时密钥给前端,让前端通过临时密钥直传文件,这样能极大减轻服务器压力

在这里插入图片描述
springboot的yml配置(假的参数,可以根据自己的修改)

# 腾讯云COS
tencent:cos:# 密钥idsecretId: AKID4zn3rsmSvGjisSDuOehPyRjfIsoY4M# 密钥keysecretKey: 96qMeaFjz2qQun5ThjXAZkm2PYSsi# 所属区域region: ap-guangzhou# 存储桶名称bucketName: jrauto-1331731126# COS存储文件夹folder: /cars/# 访问地址webUrl: https://jrauto-1331731126.cos.ap-guangzhou.myqcloud.comdurationSeconds: 1800 # 签名有效时间

创建 STS 服务类

创建一个服务类来封装获取临时密钥的逻辑。

package com.jrauto.CarAppBackend.service;
/*** @author : huiMing* Date : 2025年07月04日 15:16* @version V1.0*/
@Service
@Slf4j
public class StsService {@Value("${tencent.cos.secretId}")private String secretId;@Value("${tencent.cos.secretKey}")private String secretKey;@Value("${tencent.cos.bucketName}")private String bucketName;@Value("${tencent.cos.region}")private String region;@Value("${tencent.cos.durationSeconds}")private int durationSeconds;public Response getTempCredentials() {TreeMap<String, Object> config = new TreeMap<>();try {config.put("secretId", secretId);config.put("secretKey", secretKey);config.put("durationSeconds", durationSeconds);config.put("bucket", bucketName);config.put("region", region);Policy policy = new Policy();Statement statement = new Statement();statement.setEffect("allow");// 权限列表statement.addActions(new String[]{"cos:PutObject","cos:PostObject","cos:InitiateMultipartUpload","cos:ListMultipartUploads","cos:ListParts","cos:UploadPart","cos:CompleteMultipartUpload","ci:CreateMediaJobs","ci:CreateFileProcessJobs"});// 资源表达式,允许访问所有对象statement.addResources(new String[]{String.format("qcs::cos:%s:uid/%s:%s/*", region, bucketName.split("-")[1], bucketName),String.format("qcs::ci:%s:uid/%s:bucket/%s/*", region, bucketName.split("-")[1], bucketName)});policy.addStatement(statement);config.put("policy", Jackson.toJsonPrettyString(policy));return CosStsClient.getCredential(config);} catch (Exception e) {
//            log.error("获取临时密钥失败: {}", e.getMessage(), e);throw new RuntimeException("Failed to get temporary credentials: " + e.getMessage());}}
}

创建 REST 控制器

创建一个 REST 控制器来暴露获取临时密钥的接口

@RestController
@RequestMapping("/cos")
@CrossOrigin(origins = "*") // 允许所有源进行跨域请求,实际项目中请限制为你的前端域名
public class CosController {@Resourceprivate StsService stsService;@GetMapping("/getTempCredentials")public Map<String, Object> getTempCredentials() {Map<String, Object> result = new HashMap<>();try {Response response = stsService.getTempCredentials();result.put("code", 0);result.put("message", "Success");Map<String, String> credentials = new HashMap<>();credentials.put("tmpSecretId", response.credentials.tmpSecretId);credentials.put("tmpSecretKey", response.credentials.tmpSecretKey);credentials.put("sessionToken", response.credentials.sessionToken);credentials.put("startTime", String.valueOf(response.startTime));credentials.put("expiredTime", String.valueOf(response.expiredTime));result.put("data", credentials);} catch (RuntimeException e) {result.put("code", -1);result.put("message", e.getMessage());}return result;}
}

api测试工具
在这里插入图片描述
例如新建一个 utils/cos.js,项目内全局使用:

const util = require('./cos-wx-sdk-v5.min.js'); // 开发时使用
// const COS = require('./lib/cos-wx-sdk-v5.min.js'); // 上线时使用压缩包const cos = new util({SimpleUploadMethod: 'putObject', // 强烈建议,高级上传、批量上传内部对小文件做简单上传时使用putObject,sdk版本至少需要v1.3.0getAuthorization: function (options, callback) {// 初始化时不会调用,只有调用 cos 方法(例如 cos.putObject)时才会进入// 异步获取临时密钥// 服务端 JS 示例:https://github.com/tencentyun/cos-js-sdk-v5/blob/master/server/// 服务端其他语言参考 COS STS SDK :https://github.com/tencentyun/qcloud-cos-sts-sdk// STS 详细文档指引看:https://cloud.tencent.com/document/product/436/14048const stsUrl = 'http://127.0.0.1:8089/api/cos/getTempCredentials'; // stsUrl 替换成您自己的后端服务wx.request({url: stsUrl,data: {bucket: 'jrauto-13638',region: 'ap-guangzhou',},dataType: 'json',success: function (result) {// console.log(result.data);const credentials = result.data.data;// const credentials = data && data.credentials;// if (!data || !credentials) return console.error('credentials invalid');// 检查 credentials 格式console.log(credentials);callback({TmpSecretId: credentials.tmpSecretId,TmpSecretKey: credentials.tmpSecretKey,// v1.2.0之前版本的 SDK 使用 XCosSecurityToken 而不是 SecurityTokenSecurityToken: credentials.sessionToken,// 建议返回服务器时间作为签名的开始时间,避免用户浏览器本地时间偏差过大导致签名错误StartTime: credentials.startTime, // 时间戳,单位秒,如:1580000000ExpiredTime: credentials.expiredTime, // 时间戳,单位秒,如:1580000900});}});}
});
export default cos;

uniapp批量上传源码

<template><view class="upload-container"><button class="upload-button" @click="selectAndUploadImages" :disabled="isUploading">{{ isUploading ? '正在上传...' : '选择图片并上传' }}</button><view v-if="globalUploadProgress > 0 && globalUploadProgress < 100" class="progress-bar-container"><view class="progress-bar" :style="{ width: globalUploadProgress + '%' }"></view><text class="progress-text">总进度: {{ globalUploadProgress.toFixed(2) }}%</text></view><view v-if="isUploading && globalUploadProgress === 100" class="upload-status">处理中...</view><view class="image-list"><view v-for="(image, index) in uploadedImages" :key="index" class="image-item"><image :src="image.url" mode="aspectFill" class="uploaded-image"></image><view v-if="image.progress !== 100" class="image-progress-overlay"><text>{{ image.progress.toFixed(0) }}%</text></view><text v-if="image.error" class="image-status-error">失败</text><text v-else-if="image.progress === 100 && !image.error" class="image-status-success">完成</text></view></view><view v-if="message" :class="['message-box', messageType]">{{ message }}</view></view>
</template>
<script setup>
import { ref } from 'vue';
import cos from '@/utils/cos.js'; // 导入你封装好的 cos 实例// --- 响应式数据 ---
const isUploading = ref(false); // 是否正在上传
const globalUploadProgress = ref(0); // 整体上传进度(0-100)
const uploadedImages = ref([]); // 存储每张图片的状态 { id, url, progress, error }
const uploadedUrls = ref([]); // 存储所有上传成功的图片地址
const message = ref(''); // 提示信息
const messageType = ref(''); // 提示类型: 'success', 'error', 'info'// --- 方法 ---// 选择图片并上传多图
const selectAndUploadImages = () => {if (isUploading.value) {return; // 避免重复点击}wx.chooseImage({count: 9, // 最多选择9张图片sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图sourceType: ['album', 'camera'], // 可以指定来源success: async (res) => {// 重置状态isUploading.value = true;globalUploadProgress.value = 0;uploadedImages.value = [];uploadedUrls.value = [];message.value = '';messageType.value = '';const filesToUpload = res.tempFiles.map((file, index) => {const fileName = file.path.substr(file.path.lastIndexOf('/') + 1);const uniqueKey = `uploads/${Date.now()}_${index}_${fileName}`; // 保证 Key 的唯一性// 为每张图片初始化状态uploadedImages.value.push({id: uniqueKey, // 用 Key 作为唯一标识url: file.path, // 初始显示本地路径progress: 0,error: false,isFinished: false,});return {Bucket: 'jrauto-1363555616', // **替换成你的实际 Bucket 名称**Region: 'ap-guangzhou', // **替换成你的实际地域,例如 ap-guangzhou**Key: uniqueKey,FilePath: file.path,// onTaskReady 和 Headers 可以在这里定义,或者使用 uploadFiles 的全局 onFileFinish};});try {await cos.uploadFiles({files: filesToUpload,SliceSize: 1024 * 1024 * 5, // 超过5MB使用分块上传onProgress: (info) => {// 整体上传进度globalUploadProgress.value = parseFloat((info.percent * 100).toFixed(2));console.log('总进度:', globalUploadProgress.value, '%');},onFileFinish: (err, data, options) => {// 单个文件上传完成/失败const fileKey = options.Key;const index = uploadedImages.value.findIndex(item => item.id === fileKey);if (index !== -1) {if (err) {uploadedImages.value[index].error = true;console.error(`文件 ${options.Key} 上传失败:`, err);} else {uploadedImages.value[index].url = data.Location; // 更新为上传后的 COS 地址uploadedImages.value[index].progress = 100; // 确保显示100%uploadedImages.value[index].isFinished = true;uploadedUrls.value.push(data.Location); // 将成功地址添加到列表console.log(`文件 ${options.Key} 上传成功:`, data.Location);}}},});// 所有文件处理完毕isUploading.value = false;const failedCount = uploadedImages.value.filter(item => item.error).length;if (failedCount === 0) {message.value = `所有图片上传成功!共 ${uploadedUrls.value.length} 张。`;messageType.value = 'success';console.log('所有上传成功的图片地址:', uploadedUrls.value);} else {message.value = `部分图片上传失败。成功 ${uploadedUrls.value.length} 张,失败 ${failedCount} 张。`;messageType.value = 'error';}} catch (e) {isUploading.value = false;message.value = `上传过程中发生错误: ${e.message || '未知错误'}`;messageType.value = 'error';console.error('批量上传发生异常:', e);}},fail: (err) => {isUploading.value = false; // 用户取消也算上传结束if (err.errMsg === 'chooseImage:fail cancel') {message.value = '您取消了图片选择。';messageType.value = 'info';} else {message.value = `选择图片失败: ${err.errMsg}`;messageType.value = 'error';}console.error('选择图片失败:', err);}});
};// 你也可以添加其他操作,例如暂停、重启、取消单个上传任务(需要单个任务ID)
// 注意:cos.uploadFiles 的 onFileFinish 不直接提供 taskId,
// 如果你需要细粒度控制,可以考虑在 files 数组的每个对象里定义 onTaskReady 回调来获取 taskId。
// 或者使用 cos.uploadFile 单个调用并管理多个 Promise。
</script>
<style>
.upload-container {padding: 20px;display: flex;flex-direction: column;align-items: center;
}.upload-button {width: 80%;padding: 10px 0;margin-bottom: 20px;background-color: #007aff;color: white;border-radius: 5px;font-size: 16px;text-align: center;transition: background-color 0.3s ease;
}.upload-button[disabled] {background-color: #a0cfff;cursor: not-allowed;
}/* 全局进度条样式 */
.progress-bar-container {width: 90%;height: 8px;background-color: #e0e0e0;border-radius: 5px;overflow: hidden;margin-bottom: 15px;position: relative;
}.progress-bar {height: 100%;background-color: #4cd964; /* 绿色 */width: 0%;border-radius: 5px;transition: width 0.3s ease-out; /* 动画效果 */
}.progress-text {position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);font-size: 12px;color: #666;text-shadow: 0 0 2px white; /* 增加可读性 */
}.upload-status {margin-bottom: 15px;font-size: 14px;color: #666;
}/* 图片列表样式 */
.image-list {display: flex;flex-wrap: wrap;gap: 10px; /* 图片之间的间距 */justify-content: flex-start;width: 100%;
}.image-item {position: relative;width: calc(33.33% - 7px); /* 每行3个,减去间距 */height: 100px; /* 固定高度 */border: 1px solid #ddd;border-radius: 8px;overflow: hidden;display: flex;align-items: center;justify-content: center;box-sizing: border-box; /* 包含 padding 和 border */
}.uploaded-image {width: 100%;height: 100%;object-fit: cover; /* 保持图片比例覆盖整个区域 */
}.image-progress-overlay {position: absolute;top: 0;left: 0;width: 100%;height: 100%;background-color: rgba(0, 0, 0, 0.5); /* 半透明背景 */color: white;display: flex;align-items: center;justify-content: center;font-size: 14px;z-index: 10;
}.image-status-success {position: absolute;bottom: 5px;right: 5px;background-color: #4cd964;color: white;padding: 2px 5px;border-radius: 3px;font-size: 10px;z-index: 10;
}.image-status-error {position: absolute;bottom: 5px;right: 5px;background-color: #ff3b30; /* 红色 */color: white;padding: 2px 5px;border-radius: 3px;font-size: 10px;z-index: 10;
}/* 提示消息样式 */
.message-box {width: 90%;padding: 10px;margin-top: 20px;border-radius: 5px;text-align: center;font-size: 14px;
}.message-box.success {background-color: #d4edda;color: #155724;border: 1px solid #c3e6cb;
}.message-box.error {background-color: #f8d7da;color: #721c24;border: 1px solid #f5c6cb;
}.message-box.info {background-color: #d1ecf1;color: #0c5460;border: 1px solid #bee5eb;
}
</style>

在这里插入图片描述

http://www.lryc.cn/news/580229.html

相关文章:

  • web网页,在线%电商,茶叶,商城,网上商城系统%分析系统demo,于vscode,vue,java,jdk,springboot,mysql数据库
  • 数据结构---线性表理解(一)
  • JAVA-springboot 整合Redis
  • 本地搭建区块链服务的详细指南
  • csgo道具整理
  • 网闸内部架构设计:分层与微服务的生死博弈
  • 浅层神经网络:原理与Python实现
  • Android Studio-Git的使用指南
  • 指数分布的Python计算与分析
  • CMD 实用命令大全
  • 【网络与系统安全】强制访问控制——Biba模型
  • Assistant API的原理及应用
  • 深入MIPI DSI显示技术栈 (四) DSI物理层——高速传输的基石
  • HLS基础(1):循环展开与存储分块
  • 深入剖析MYSQL MVCC多版本并发控制+ReadView视图快照规避幻读问题
  • Kuberrnetes 服务发布
  • AI领域新趋势:从提示(Prompt)工程到上下文(Context)工程
  • Spring Boot + 本地部署大模型实现:优化与性能提升
  • 【排序算法】
  • 模型部署与推理--利用libtorch模型部署与推理
  • 前端捕获异常的全面场景及方法
  • MYSQL 服务正在启动或停止中,请稍候片刻后再试一次。
  • Java 与 MySQL 性能优化:MySQL全文检索查询优化实践
  • 在 Ubuntu 22.04 上使用 Minikube 部署 Go 应用到 Kubernetes
  • 微服务架构下的抉择:Consul vs. Eureka,服务发现该如何选型?
  • 本地部署Dify并结合ollama大语言模型工具搭建自己的AI知识库
  • 软件反调试(4)- 基于IsDebuggerPresent的检测
  • Docker学习笔记:Docker网络
  • LDO VS DCDC
  • Redis的缓存击穿和缓存雪崩