当前位置: 首页 > 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.dtcms.com/a/266698.html

相关文章:

  • Spring Cloud网关与CI文件配置请求安全性对比
  • 基于二维码的视频合集高效管理与分发技术
  • monorepo + Turborepo --- 运行任务
  • MySQL ON DUPLICATE KEY UPDATE 用法详解
  • 鸿蒙开发List长按Item拖拽切换效果
  • 基于区块链的物联网(IoT)安全通信与数据共享的典型实例
  • JSONLines和JSON数据格式使用教程
  • AI大模型:(二)1.5 Stable Diffusion中文文生图模型部署
  • 30 秒锁定黑客攻击:SLS SQL 如何从海量乱序日志中“揪”出攻击源
  • 【C语言刷题】第十天:加量加餐继续,代码题训练,融会贯通IO模式
  • 短篇小说7.4
  • BM4 合并两个排序的链表
  • QT6 源(152)模型视图架构里的表格窗体视图 QTableWidget 篇二:学习本类的 protected 权限的成员函数,以及信号与槽函数
  • c语言中的函数IV
  • MCMC:高维概率采样的“随机游走”艺术
  • pybind11 导出 C++ map 在 Python 层 get 访问慢的优化方案
  • 区块链技术核心组件及应用架构的全面解析
  • python打卡day59@浙大疏锦行
  • 车载电子电气架构 --- OEM走向开放协同与敏捷迭代
  • 数据结构:队列的顺序存储实现
  • 【Linux 系统】基础IO——Linux中对文件的理解
  • 【深度学习新浪潮】如何使用大模型等技术基于序列预测蛋白质的结构,功能和靶点?
  • 【学习笔记】Lean4基础 ing
  • 邮科千兆8光8电工业级交换机互联网的脉搏
  • 洛谷刷题8
  • 云原生Kubernetes系列 | Ingress和Egress网络策略NetworkPolicy结合案例使用详解
  • 5060Ti安装黑屏问题一解
  • 【WIP】【VLAVLM——InternVL系列】
  • Maven编译和打包插件
  • cd-agent更换cd模型(自用)