一网打尽八大网盘:LinkSwift网盘直链下载助手完全指南
2026/5/1 16:10:24
在 IM 系统中,文件上传是一个常见需求。用户需要上传图片、音频、视频等文件。传统的做法是将文件先上传到应用服务器,再由服务器转发到云存储,这种方式存在性能瓶颈和安全风险。更优的方案是让客户端直接上传到云存储,但如何保证安全性?本文将介绍如何使用阿里云 OSS + STS 实现安全的文件上传方案
传统方案的问题:
如果使用永久 AccessKey(主账号密钥),通常有两种方式:
// ❌ 危险:密钥暴露在客户端代码中constclient=newOSS({accessKeyId:'LTAI5t...',// 永久密钥accessKeySecret:'xxx...',// 永久密钥bucket:'my-bucket',region:'oss-cn-shenzhen'});风险:
客户端 → 服务端 → OSS问题:
STS(Security Token Service) 是阿里云提供的临时访问凭证服务,具有以下优势:
| 对比项 | 永久密钥 | STS 临时凭证 |
|---|---|---|
| 安全性 | 低(永久有效) | 高(临时有效,自动过期) |
| 权限控制 | 主账号全部权限 | 可精确控制权限范围 |
| 泄露影响 | 严重(需更换密钥) | 轻微(自动过期) |
| 性能 | 需服务端中转 | 客户端直传,性能好 |
| 灵活性 | 低 | 高(可动态生成) |
核心优势:
场景:用户上传图片
使用永久密钥(不安全):
风险:密钥泄露 → 攻击者可以: - 上传任意文件 - 删除所有文件 - 修改文件权限 - 造成数据泄露和经济损失使用 STS 临时凭证(安全):
优势: - 凭证 1 小时后自动失效 - 只能上传到指定目录 - 只能执行上传操作 - 即使泄露,影响范围有限┌─────────┐ ① 请求临时凭证 ┌─────────┐ │ 客户端 │ ────────────────────────→ │ 服务端 │ │ │ │ │ │ │ ② 调用 STS API │ │ │ │ ←────────────────────────── │ │ │ │ │ │ │ │ ③ 返回临时凭证 │ │ │ │ ←────────────────────────── │ │ │ │ │ │ │ │ ④ 直接上传到 OSS │ │ │ │ ────────────────────────→ │ 阿里云OSS│ │ │ │ │ │ │ ⑤ 返回文件 URL │ │ │ │ ←────────────────────────── │ │ └─────────┘ └─────────┘// 客户端发送请求 message GetStsCmd { MsgType msgType = 1; // 消息类型(图片/音频/视频等) }服务端使用主账号的 AccessKey 调用 STS 服务,获取临时凭证:
@ComponentpublicclassAliOssProvider{publicAliOssStsDtogetAliOssSts(){// 1. 配置 STS 客户端Stringendpoint="sts.cn-hangzhou.aliyuncs.com";StringaccessKeyId="主账号AccessKeyId";StringaccessKeySecret="主账号AccessKeySecret";DefaultProfile.addEndpoint("","Sts",endpoint);IClientProfileprofile=DefaultProfile.getProfile("",accessKeyId,accessKeySecret);DefaultAcsClientclient=newDefaultAcsClient(profile);// 2. 构建 AssumeRole 请求AssumeRoleRequestrequest=newAssumeRoleRequest();request.setSysMethod(MethodType.POST);request.setRoleArn("acs:ram::123456789:role/oss-upload-role");// RAM 角色request.setRoleSessionName("upload-session");// 会话名称request.setDurationSeconds(3600L);// 有效期 1 小时// 3. 调用 STS APIAssumeRoleResponseresponse=client.getAcsResponse(request);// 4. 提取临时凭证AliOssStsDtostsDto=newAliOssStsDto();stsDto.setAccessKeyId(response.getCredentials().getAccessKeyId());stsDto.setAccessKeySecret(response.getCredentials().getAccessKeySecret());stsDto.setSecurityToken(response.getCredentials().getSecurityToken());stsDto.setBucket("my-bucket");stsDto.setRegion("oss-cn-shenzhen");stsDto.setOssEndpoint("oss-cn-shenzhen.aliyuncs.com");returnstsDto;}}// 服务端返回 message GetStsAck { string accessKeyId = 1; // 临时 AccessKeyId string accessKeySecret = 2; // 临时 AccessKeySecret string securityToken = 3; // 安全令牌 string region = 4; // 区域 string bucket = 5; // 存储桶名称 string uploadPath = 6; // 上传路径(如:image/2024/01/15) string endpoint = 7; // OSS 端点 }// 客户端使用临时凭证上传constclient=newOSS({accessKeyId:stsDto.accessKeyId,accessKeySecret:stsDto.accessKeySecret,stsToken:stsDto.securityToken,// 关键:临时令牌bucket:stsDto.bucket,region:stsDto.region,endpoint:stsDto.endpoint});// 上传文件constresult=await client.put(`${stsDto.uploadPath}/${fileName}`,// 完整路径file);// 获取文件 URLconstfileUrl=result.url;RAM 角色配置(在阿里云控制台):
{"Version":"1","Statement":[{"Effect":"Allow","Action":["oss:PutObject","oss:GetObject"],"Resource":["acs:oss:*:*:my-bucket/image/*","acs:oss:*:*:my-bucket/audio/*","acs:oss:*:*:my-bucket/video/*"]}]}权限说明:
格式:acs:ram::{账号ID}:role/{角色名称} 示例:acs:ram::123456789:role/oss-upload-role问题场景:
解决方案:
在 AQChat 项目中,我们使用 Redis 缓存临时凭证:
@ComponentpublicclassAliOssProvider{@ResourceprivateRedisCacheHelperredisCacheHelper;privatestaticfinallongCACHE_TIME=3600-60;// 缓存 59 分钟(比凭证有效期少 1 分钟)publicAliOssStsDtogetAliOssSts(){// 1. 先尝试从缓存获取AliOssStsDtocachedSts=getCacheAliOssSts();if(cachedSts!=null){LOGGER.info("从缓存获取 STS 凭证");returncachedSts;}// 2. 缓存未命中,调用 STS APIAliOssStsDtostsDto=callStsApi();// 3. 存入缓存if(stsDto!=null){cacheAliOssSts(stsDto);}returnstsDto;}/** * 从缓存获取临时凭证 */privateAliOssStsDtogetCacheAliOssSts(){returnredisCacheHelper.getCacheObject(AQRedisKeyPrefix.ALI_OSS_STS,AliOssStsDto.class);}/** * 缓存临时凭证 */privatevoidcacheAliOssSts(AliOssStsDtostsDto){redisCacheHelper.setCacheObject(AQRedisKeyPrefix.ALI_OSS_STS,stsDto,CACHE_TIME,TimeUnit.SECONDS);}}缓存 Key:
Key: AQChat:aliOssSts Value: AliOssStsDto (JSON 序列化) TTL: 3540 秒(59 分钟)缓存时间设计:
| 项目 | 时间 | 说明 |
|---|---|---|
| STS 凭证有效期 | 3600 秒(1 小时) | 阿里云 STS 返回的凭证有效期 |
| 缓存 TTL | 3540 秒(59 分钟) | 比凭证有效期少 1 分钟 |
| 安全边界 | 60 秒 | 确保凭证过期前缓存已失效 |
设计原因:
性能提升:
| 指标 | 无缓存 | 有缓存 | 提升 |
|---|---|---|---|
| STS API 调用 | 每次请求 | 每小时 1 次 | 减少 99%+ |
| 响应时间 | 150ms | < 1ms | 提升 150 倍 |
| 并发支持 | 受 STS 限流影响 | 不受限 | 显著提升 |
实际测试数据:
在 AQChat 项目中,我们采用以下路径组织策略:
@ComponentpublicclassGetStsCmdHandler{publicvoidhandle(ChannelHandlerContextctx,GetStsCmdcmd){// 1. 获取消息类型intmsgTypeValue=cmd.getMsgTypeValue();StringmsgType=getMsgTypeByCode(msgTypeValue);// msgType: "image" / "audio" / "video" / "file"// 2. 生成上传路径StringuploadPath=msgType+"/"+getFormatTime();// 示例:image/2024/01/15// 3. 设置到 STS 凭证中aliOssSts.setUploadPath(uploadPath);}privateStringgetFormatTime(){SimpleDateFormatsdf=newSimpleDateFormat("yyyy/MM/dd");returnsdf.format(newDate());}}路径结构:
bucket/ ├── image/ # 图片文件 │ ├── 2024/ │ │ ├── 01/ │ │ │ ├── 15/ │ │ │ │ ├── uuid1.jpg │ │ │ │ ├── uuid2.png │ │ │ │ └── ... │ │ │ └── 16/ │ │ │ └── ... │ │ └── 02/ │ │ └── ... │ └── ... ├── audio/ # 音频文件 │ ├── 2024/ │ │ └── ... │ └── ... ├── video/ # 视频文件 │ ├── 2024/ │ │ └── ... │ └── ... └── file/ # 其他文件 ├── 2024/ │ └── ... └── ...客户端上传时:
// 1. 获取 STS 凭证(包含 uploadPath)conststsResponse=awaitgetStsCredentials(msgType);// stsResponse.uploadPath = "image/2024/01/15"// 2. 生成唯一文件名constfileName=`${generateUUID()}.${getFileExtension(file)}`;// fileName = "550e8400-e29b-41d4-a716-446655440000.jpg"// 3. 组合完整路径constfullPath=`${stsResponse.uploadPath}/${fileName}`;// fullPath = "image/2024/01/15/550e8400-e29b-41d4-a716-446655440000.jpg"// 4. 上传到 OSSconstresult=await ossClient.put(fullPath,file);{"Statement":[{"Effect":"Allow","Action":["oss:PutObject"],// 只允许上传"Resource":["acs:oss:*:*:bucket/image/*"]// 只能上传到指定目录}]}// 服务端可以验证文件类型privatebooleanisValidFileType(StringfileName,MsgTypemsgType){Stringextension=getFileExtension(fileName);switch(msgType){caseIMAGE:returnArrays.asList("jpg","jpeg","png","gif").contains(extension);caseAUDIO:returnArrays.asList("mp3","wav","aac").contains(extension);caseVIDEO:returnArrays.asList("mp4","avi","mov").contains(extension);default:returnfalse;}}❌ 危险路径:../../../etc/passwd ✅ 安全路径:image/2024/01/15/uuid.jpg✅ 使用 UUID 作为文件名,避免冲突 ✅ 路径包含时间,进一步降低冲突概率// 可以在 RAM 策略中限制文件大小 { "Condition": { "NumericLessThan": { "oss:ContentLength": 10485760 // 限制 10MB } } }LOGGER.info("用户{}获取STS凭证,类型:{},路径:{}",userId,msgType,uploadPath);@ComponentpublicclassAliOssProvider{@ResourceprivateAQChatConfigconfig;@ResourceprivateRedisCacheHelperredisCacheHelper;privatestaticfinallongCACHE_TIME=3600-60;// 59 分钟/** * 获取临时凭证(带缓存) */publicAliOssStsDtogetAliOssSts(){// 1. 从缓存获取AliOssStsDtocached=getCacheAliOssSts();if(cached!=null){returncached;}// 2. 调用 STS APIAliOssStsDtostsDto=callStsApi();// 3. 缓存结果if(stsDto!=null){cacheAliOssSts(stsDto);}returnstsDto;}/** * 调用 STS API 获取临时凭证 */privateAliOssStsDtocallStsApi(){try{// 配置 STS 客户端DefaultProfile.addEndpoint("","Sts",config.getAliOssStsConfig().getEndpoint());IClientProfileprofile=DefaultProfile.getProfile("",config.getAliOssStsConfig().getAccessKeyId(),config.getAliOssStsConfig().getAccessKeySecret());DefaultAcsClientclient=newDefaultAcsClient(profile);// 构建请求AssumeRoleRequestrequest=newAssumeRoleRequest();request.setSysMethod(MethodType.POST);request.setRoleArn(config.getAliOssStsConfig().getRoleArn());request.setRoleSessionName(config.getAliOssStsConfig().getRoleSessionName());request.setDurationSeconds(config.getAliOssStsConfig().getDurationSeconds());// 调用 APIAssumeRoleResponseresponse=client.getAcsResponse(request);// 构建返回对象AliOssStsDtostsDto=newAliOssStsDto();stsDto.setAccessKeyId(response.getCredentials().getAccessKeyId());stsDto.setAccessKeySecret(response.getCredentials().getAccessKeySecret());stsDto.setSecurityToken(response.getCredentials().getSecurityToken());stsDto.setBucket(config.getAliOssStsConfig().getBucketName());stsDto.setRegion(config.getAliOssStsConfig().getRegionId());stsDto.setOssEndpoint(config.getAliOssConfig().getEndpoint());returnstsDto;}catch(ClientExceptione){LOGGER.error("获取STS凭证失败: {}",e.getErrMsg());returnnull;}}privateAliOssStsDtogetCacheAliOssSts(){returnredisCacheHelper.getCacheObject(AQRedisKeyPrefix.ALI_OSS_STS,AliOssStsDto.class);}privatevoidcacheAliOssSts(AliOssStsDtostsDto){redisCacheHelper.setCacheObject(AQRedisKeyPrefix.ALI_OSS_STS,stsDto,CACHE_TIME,TimeUnit.SECONDS);}}// 1. 获取 STS 凭证asyncfunctiongetStsCredentials(msgType){constresponse=awaitfetch('/api/sts',{method:'POST',body:JSON.stringify({msgType})});returnawaitresponse.json();}// 2. 上传文件asyncfunctionuploadFile(file,msgType){// 获取 STS 凭证conststs=awaitgetStsCredentials(msgType);// 初始化 OSS 客户端constclient=newOSS({accessKeyId:sts.accessKeyId,accessKeySecret:sts.accessKeySecret,stsToken:sts.securityToken,bucket:sts.bucket,region:sts.region,endpoint:sts.endpoint});// 生成文件名constfileName=`${generateUUID()}.${getFileExtension(file.name)}`;constfullPath=`${sts.uploadPath}/${fileName}`;// 上传文件constresult=awaitclient.put(fullPath,file);// 返回文件 URLreturnresult.url;}// 使用示例constfileInput=document.getElementById('fileInput');fileInput.addEventListener('change',async(e)=>{constfile=e.target.files[0];constfileUrl=awaituploadFile(file,'image');console.log('文件上传成功:',fileUrl);});使用阿里云 OSS + STS 实现文件上传方案,具有以下优势:
安全性:
性能:
可维护性:
关键要点:
希望本文能帮助大家更好地理解和实现安全的文件上传方案。在实际项目中,要根据具体业务需求调整配置,但核心安全原则是不变的。