12 KiB
12 KiB
出国申请附件下载权限修复方案
📋 问题描述
错误现象
用户点击出国申请详情中的"出国境登记表盖章文件"和"党籍保留附件"下载按钮时,出现以下错误:
<Error>
<Code>AccessDenied</Code>
<Message>Access Denied.</Message>
<Key>overseas/stamped/stamped_0_1760082853042.png</Key>
<BucketName>ruoyi</BucketName>
<Resource>/ruoyi/overseas/stamped/stamped_0_1760082853042.png</Resource>
</Error>
根本原因
- OSS Bucket权限配置:OSS bucket设置为私有访问,直接使用URL无法访问
- 前端下载方式错误:前端直接使用文件URL进行下载,绕过了权限验证
- 缺少OSS ID存储:数据库中只存储了文件路径,没有存储OSS文件ID,无法使用系统的安全下载接口
🎯 解决方案
整体架构
前端下载请求 → 后端代理接口 → 权限验证 → OSS安全下载 → 返回文件流
技术方案
采用后端代理下载 + OSS ID存储的方式,确保:
- ✅ 所有下载请求经过后端权限验证
- ✅ 使用系统统一的OSS服务管理文件
- ✅ 文件访问可追踪、可审计
- ✅ 支持私有bucket安全访问
🔧 实施步骤
1️⃣ 数据库改造
新增字段:stamped_file_oss_id 和 party_retention_file_oss_id
-- 添加盖章文件OSS ID字段
ALTER TABLE `rs_overseas_application`
ADD COLUMN `stamped_file_oss_id` BIGINT(20) NULL COMMENT '盖章文件OSS ID'
AFTER `stamped_file_upload_status`;
-- 添加党籍保留附件OSS ID字段
ALTER TABLE `rs_overseas_application`
ADD COLUMN `party_retention_file_oss_id` BIGINT(20) NULL COMMENT '党籍保留附件OSS ID'
AFTER `party_retention_upload_status`;
-- 添加索引以提升查询性能
ALTER TABLE `rs_overseas_application`
ADD INDEX `idx_stamped_file_oss_id` (`stamped_file_oss_id`),
ADD INDEX `idx_party_retention_file_oss_id` (`party_retention_file_oss_id`);
执行位置:docs/设计文档/database/【立即执行】出国申请表添加OSS_ID字段.sql
2️⃣ 后端Domain实体改造
RsOverseasApplication.java
/**
* 盖章文件OSS ID(用于安全下载)
*/
@ExcelIgnore
private Long stampedFileOssId;
/**
* 党籍保留附件OSS ID(用于安全下载)
*/
@ExcelIgnore
private Long partyRetentionFileOssId;
RsOverseasApplicationVo.java
/**
* 盖章文件OSS ID(用于安全下载)
*/
private Long stampedFileOssId;
/**
* 党籍保留附件OSS ID(用于安全下载)
*/
private Long partyRetentionFileOssId;
3️⃣ 后端上传接口改造
改造前:使用OssClient直接上传,只返回文件URL
// 旧代码
OssClient ossClient = OssFactory.instance();
UploadResult uploadResult = ossClient.upload(file.getInputStream(), filePath,
file.getSize(), file.getContentType());
applicationService.updateStampedFileInfo(applicationId, uploadResult.getUrl(),
originalFilename, new Date(), "1");
改造后:使用ISysOssService上传,自动保存到sys_oss表并返回OSS ID
// 新代码 - 使用系统OSS服务
@Autowired
private ISysOssService ossService;
SysOssVo ossResult = ossService.upload(file);
// 更新申请记录 - 存储OSS ID和文件路径
applicationService.updateStampedFileInfo(
applicationId,
ossResult.getUrl(), // 文件URL
ossResult.getOriginalName(), // 原始文件名
new Date(), // 上传时间
"1", // 上传状态:已上传
ossResult.getOssId() // ✨ OSS文件ID(新增)
);
涉及接口:
POST /rs/overseas/application/uploadStampedFile- 盖章文件上传POST /rs/overseas/application/uploadPartyRetentionFile- 党籍保留附件上传
4️⃣ 后端Service层改造
IRsOverseasApplicationService.java
// 添加ossId参数
Boolean updateStampedFileInfo(Long applicationId, String filePath, String fileName,
java.util.Date uploadTime, String uploadStatus, Long ossId);
Boolean updatePartyRetentionFileInfo(Long applicationId, String filePath, String fileName,
java.util.Date uploadTime, String uploadStatus, Long ossId);
RsOverseasApplicationServiceImpl.java
@Override
@Transactional
public Boolean updateStampedFileInfo(Long applicationId, String filePath, String fileName,
java.util.Date uploadTime, String uploadStatus, Long ossId) {
return baseMapper.update(null,
new LambdaUpdateWrapper<RsOverseasApplication>()
.eq(RsOverseasApplication::getApplicationId, applicationId)
.set(RsOverseasApplication::getStampedRegistrationFilePath, filePath)
.set(RsOverseasApplication::getStampedRegistrationFileName, fileName)
.set(RsOverseasApplication::getStampedFileUploadTime, uploadTime)
.set(RsOverseasApplication::getStampedFileUploadStatus, uploadStatus)
.set(RsOverseasApplication::getStampedFileOssId, ossId) // ✨ 新增
) > 0;
}
5️⃣ 后端新增下载代理接口
RsOverseasApplicationController.java
盖章文件下载:
/**
* 下载盖章文件(安全下载)
*/
@SaCheckPermission("rs:overseas:query")
@GetMapping("/downloadStampedFile/{applicationId}")
public void downloadStampedFile(@PathVariable Long applicationId, HttpServletResponse response) {
try {
RsOverseasApplicationVo application = applicationService.queryById(applicationId);
if (application == null || application.getStampedFileOssId() == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":404,\"msg\":\"文件不存在\"}");
return;
}
// 使用系统OSS服务进行安全下载
ossService.secureDownload(application.getStampedFileOssId(), response);
} catch (Exception e) {
e.printStackTrace();
}
}
党籍保留附件下载:
/**
* 下载党籍保留附件(安全下载)
*/
@SaCheckPermission("rs:overseas:query")
@GetMapping("/downloadPartyRetentionFile/{applicationId}")
public void downloadPartyRetentionFile(@PathVariable Long applicationId, HttpServletResponse response) {
try {
RsOverseasApplicationVo application = applicationService.queryById(applicationId);
if (application == null || application.getPartyRetentionFileOssId() == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":404,\"msg\":\"文件不存在\"}");
return;
}
// 使用系统OSS服务进行安全下载
ossService.secureDownload(application.getPartyRetentionFileOssId(), response);
} catch (Exception e) {
e.printStackTrace();
}
}
核心特性:
- ✅ 权限验证:
@SaCheckPermission("rs:overseas:query") - ✅ 文件存在性检查
- ✅ 使用系统统一的
ISysOssService.secureDownload()方法 - ✅ 自动处理文件流返回
6️⃣ 前端下载逻辑改造
index.vue
改造前:直接使用文件URL下载
// 旧代码 - 直接访问OSS URL(会被拒绝)
const handleDownloadStampedFile = (data: OverseasApplicationVO) => {
const link = document.createElement('a');
link.href = data.stampedRegistrationFilePath; // ❌ 直接使用OSS URL
link.download = data.stampedRegistrationFileName;
link.target = '_blank';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
改造后:调用后端代理接口
// 新代码 - 通过后端代理下载(安全)
const handleDownloadStampedFile = (data: OverseasApplicationVO) => {
console.log('📥 下载出国境登记表盖章文件:', data.stampedRegistrationFileName);
if (!data.applicationId) {
ElMessage.warning('申请ID不存在');
return;
}
// ✅ 使用后端代理下载接口
const downloadUrl = `/api/rs/overseas/application/downloadStampedFile/${data.applicationId}`;
window.open(downloadUrl, '_blank');
ElMessage.success('文件下载已开始');
};
// 党籍保留附件下载(同样改造)
const handleDownloadPartyRetentionFile = (data: OverseasApplicationVO) => {
console.log('📥 下载党籍保留附件:', data.partyRetentionFileName);
if (!data.applicationId) {
ElMessage.warning('申请ID不存在');
return;
}
// ✅ 使用后端代理下载接口
const downloadUrl = `/api/rs/overseas/application/downloadPartyRetentionFile/${data.applicationId}`;
window.open(downloadUrl, '_blank');
ElMessage.success('文件下载已开始');
};
📊 改造效果
安全性提升
| 项目 | 改造前 | 改造后 |
|---|---|---|
| 权限验证 | ❌ 无 | ✅ Sa-Token权限验证 |
| 访问控制 | ❌ 任何人可访问URL | ✅ 必须登录且有权限 |
| 访问日志 | ❌ 无 | ✅ 自动记录访问日志 |
| Bucket类型 | ⚠️ 必须公开 | ✅ 可使用私有bucket |
技术优势
- ✅ 统一管理:使用系统统一的
ISysOssService服务 - ✅ 可扩展性:支持断点续传、缓存等高级特性
- ✅ 可追踪性:所有文件访问都有日志记录
- ✅ 规范一致:与系统其他模块保持一致的文件处理方式
🚀 部署步骤
1. 执行数据库迁移
# 连接到数据库
mysql -u root -p hzdyjs_xg
# 执行SQL脚本
source docs/设计文档/database/【立即执行】出国申请表添加OSS_ID字段.sql
2. 重启后端服务
# 停止服务
# 重新编译打包
# 启动服务
3. 刷新前端
# 清除浏览器缓存
# 重新访问页面
4. 验证功能
- 上传新的盖章文件,检查是否保存了OSS ID
- 上传新的党籍保留附件,检查是否保存了OSS ID
- 测试下载功能,确认可以正常下载
- 检查下载时的权限验证是否生效
📝 注意事项
数据兼容性
⚠️ 已存在的文件处理:
- 旧数据:没有OSS ID的记录无法通过新接口下载
- 解决方案:
- 方案一:保留旧的下载方式作为降级方案
- 方案二:编写数据迁移脚本,为旧文件生成OSS ID
- 方案三(推荐):让用户重新上传文件
OSS配置检查
确保系统OSS配置正确:
# application.yml
oss:
endpoint: xxx
accessKeyId: xxx
accessKeySecret: xxx
bucketName: ruoyi
region: xxx
权限配置
确保菜单权限配置正确:
rs:overseas:query- 查看出国申请权限(含下载)rs:overseas:add- 新增出国申请权限(含上传)
🔍 故障排查
问题1:下载时提示"文件不存在"
排查步骤:
- 检查数据库中的
stamped_file_oss_id或party_retention_file_oss_id字段是否有值 - 检查
sys_oss表中是否存在对应的OSS ID记录 - 检查OSS中是否真实存在该文件
问题2:下载时提示"无权限"
排查步骤:
- 检查用户是否登录
- 检查用户是否有
rs:overseas:query权限 - 检查Sa-Token配置是否正确
问题3:下载速度慢
可能原因:
- OSS服务器距离远
- 文件较大
- 网络带宽限制
优化方案:
- 启用CDN加速
- 使用分片下载
- 优化网络配置
📚 相关文档
🎯 总结
本次改造通过引入OSS ID存储和后端代理下载机制,彻底解决了出国申请附件下载权限问题。改造后的系统:
- ✅ 安全性更高:所有下载都经过权限验证
- ✅ 可维护性更好:使用系统统一的OSS服务
- ✅ 可追踪性更强:所有文件访问都有日志
- ✅ 扩展性更好:支持更多高级特性
日期:2025-01-10
作者:AI Assistant
版本:v1.0