12 KiB
12 KiB
MinIO 安全访问控制增强 - 变更记录
变更日期: 2025-01-27
变更人员: AI Assistant
变更类型: 安全增强
影响范围: 文件存储、用户权限、系统安全
🎯 变更目标
解决当前 matchingUrl 方法的安全问题,提供更严格的文件访问控制机制,确保只有登录用户才能访问文件。
🚨 问题分析
当前安全问题
- 临时URL可能被泄露:120秒的临时URL可能被恶意用户获取并分享
- 缺乏用户身份验证:无法确保只有登录用户才能访问
- 无访问日志记录:无法追踪文件访问行为
- 权限控制不足:无法根据业务需求进行细粒度权限控制
风险场景
- 敏感文件(如学生信息、财务数据)可能被恶意访问
- 文件URL被分享到外部,绕过系统权限控制
- 无法追踪文件访问记录
🛡️ 解决方案
方案一:基于后端代理的强制权限验证(已实施)
核心特性
- ✅ 强制登录验证:只有登录用户才能访问文件
- ✅ 文件所有者权限:只有文件上传者或超级管理员可以访问
- ✅ 访问日志记录:记录所有文件访问行为
- ✅ 后端代理下载:通过后端代理下载,不直接暴露OSS URL
- ✅ 细粒度权限控制:支持业务级别的权限验证
实施内容
1. 更新 SysOssServiceImpl.java
文件路径: backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysOssServiceImpl.java
主要变更:
- 修改
matchingUrl方法,添加安全警告注释 - 新增
getSecureFileUrl()方法:生成安全访问URL - 新增
secureDownload()方法:安全文件下载 - 增强
checkFileAccessPermission()方法:多重权限验证
关键代码:
/**
* 🔐 安全文件访问URL生成(推荐使用)
* 通过后端代理进行权限验证,确保只有登录用户才能访问
*/
public String getSecureFileUrl(Long ossId) {
// 权限验证
if (!checkFileAccessPermission(ossId)) {
throw new ServiceException("无权限访问该文件");
}
// 记录访问日志
try {
Long userId = LoginHelper.getUserId();
String ipAddress = ServletUtils.getClientIP();
String userAgent = ServletUtils.getRequest().getHeader("User-Agent");
logFileAccess(ossId, userId, "SECURE_ACCESS", ipAddress, userAgent);
} catch (Exception e) {
// 日志记录失败不影响主流程
}
// 返回后端代理URL,而不是直接返回OSS URL
return "/system/oss/download/" + ossId;
}
/**
* 🔐 安全文件下载(通过后端代理)
* 强制权限验证,确保只有登录用户才能下载
*/
public void secureDownload(Long ossId, HttpServletResponse response) throws IOException {
// 强制权限验证
if (!checkFileAccessPermission(ossId)) {
throw new ServiceException("无权限下载该文件");
}
SysOssVo sysOss = getById(ossId);
if (ObjectUtil.isNull(sysOss)) {
throw new ServiceException("文件不存在");
}
// 记录下载日志
try {
Long userId = LoginHelper.getUserId();
String ipAddress = ServletUtils.getClientIP();
String userAgent = ServletUtils.getRequest().getHeader("User-Agent");
logFileAccess(ossId, userId, "DOWNLOAD", ipAddress, userAgent);
} catch (Exception e) {
// 日志记录失败不影响主流程
}
// 设置响应头
FileUtils.setAttachmentResponseHeader(response, sysOss.getOriginalName());
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE + "; charset=UTF-8");
// 通过OSS客户端下载并直接输出到响应流
OssClient storage = OssFactory.instance(sysOss.getService());
storage.download(sysOss.getFileName(), response.getOutputStream(), response::setContentLengthLong);
}
/**
* 🔐 增强版权限检查
* 包含用户身份验证、文件访问权限、业务权限等多重验证
*/
@Override
public boolean checkFileAccessPermission(Long ossId) {
// 1. 基础检查:文件必须存在
SysOssVo oss = getById(ossId);
if (ObjectUtil.isNull(oss)) {
return false;
}
// 2. 用户身份验证:必须已登录
try {
Long userId = LoginHelper.getUserId();
if (ObjectUtil.isNull(userId)) {
return false;
}
} catch (Exception e) {
return false;
}
// 3. 文件所有者检查:只有文件上传者可以访问
if (ObjectUtil.isNotNull(oss.getCreateBy())) {
Long currentUserId = LoginHelper.getUserId();
if (!oss.getCreateBy().equals(currentUserId)) {
// 如果不是文件所有者,检查是否有超级管理员权限
if (!LoginHelper.isSuperAdmin()) {
return false;
}
}
}
// 4. 业务权限检查(可根据具体业务需求扩展)
// 例如:检查用户是否有访问该文件所属模块的权限
// 这里可以添加更复杂的业务逻辑
return true;
}
2. 更新 ISysOssService.java
文件路径: backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysOssService.java
主要变更:
- 新增
getSecureFileUrl()接口方法 - 新增
secureDownload()接口方法
关键代码:
/**
* 🔐 安全文件访问URL生成(推荐使用)
* 通过后端代理进行权限验证,确保只有登录用户才能访问
*
* @param ossId 文件ID
* @return 安全访问URL(通过后端代理)
*/
String getSecureFileUrl(Long ossId);
/**
* 🔐 安全文件下载(通过后端代理)
* 强制权限验证,确保只有登录用户才能下载
*
* @param ossId 文件ID
* @param response HTTP响应对象
*/
void secureDownload(Long ossId, HttpServletResponse response) throws IOException;
3. 更新 SysOssController.java
文件路径: backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysOssController.java
主要变更:
- 新增
secureDownload()接口:安全文件下载 - 新增
getSecureFileUrl()接口:获取安全访问URL - 添加
@Slf4j注解用于日志记录
关键代码:
/**
* 🔐 安全文件下载接口
* 强制权限验证,确保只有登录用户才能下载文件
*/
@SaCheckPermission("system:oss:download")
@GetMapping("/download/{ossId}")
public void secureDownload(@PathVariable("ossId") Long ossId, HttpServletResponse response) {
try {
ossService.secureDownload(ossId, response);
} catch (Exception e) {
log.error("安全文件下载失败: {}", e.getMessage(), e);
try {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write("无权限下载该文件");
} catch (IOException ex) {
log.error("写入错误响应失败", ex);
}
}
}
/**
* 🔐 获取安全文件访问URL
* 返回通过后端代理的安全访问URL,而不是直接返回OSS URL
*/
@SaCheckPermission("system:oss:access")
@GetMapping("/secure-url/{ossId}")
public R<String> getSecureFileUrl(@PathVariable("ossId") Long ossId) {
try {
String secureUrl = ossService.getSecureFileUrl(ossId);
return R.ok(secureUrl);
} catch (Exception e) {
log.error("获取安全文件URL失败: {}", e.getMessage(), e);
return R.fail("无权限访问该文件");
}
}
📊 方案对比
| 特性 | 当前方案 | 新方案(推荐) |
|---|---|---|
| 安全性 | ⭐⭐ | ⭐⭐⭐⭐⭐ |
| 实现复杂度 | ⭐ | ⭐⭐⭐ |
| 性能影响 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 权限控制 | ⭐ | ⭐⭐⭐⭐⭐ |
| 日志记录 | ❌ | ✅ |
| 业务扩展性 | ⭐ | ⭐⭐⭐⭐⭐ |
🔧 配置要求
权限配置
-- 添加文件访问权限
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES ('文件安全访问', 3, 6, 'oss', 'system/oss/index', 1, 0, 'C', '0', '0', 'system:oss:access', 'download', 'admin', sysdate(), '', null, '文件安全访问菜单');
-- 添加下载权限
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES ('文件下载', 3, 7, 'oss', 'system/oss/index', 1, 0, 'C', '0', '0', 'system:oss:download', 'download', 'admin', sysdate(), '', null, '文件下载菜单');
📝 使用指南
前端调用示例
// 获取安全文件访问URL
const getSecureFileUrl = async (ossId) => {
try {
const response = await request.get(`/system/oss/secure-url/${ossId}`);
if (response.code === 200) {
return response.data; // 返回后端代理URL
}
} catch (error) {
console.error('获取安全文件URL失败:', error);
}
};
// 下载文件(通过后端代理)
const downloadFile = async (ossId) => {
try {
const response = await request.get(`/system/oss/download/${ossId}`, {
responseType: 'blob'
});
// 创建下载链接
const blob = new Blob([response]);
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'filename.ext';
link.click();
window.URL.revokeObjectURL(url);
} catch (error) {
console.error('文件下载失败:', error);
}
};
移动端调用示例
// 移动端安全文件下载
const downloadSecureFile = async (ossId) => {
try {
const response = await uni.request({
url: `/system/oss/download/${ossId}`,
method: 'GET',
header: {
'Authorization': `Bearer ${token}`
}
});
// 保存文件到本地
const filePath = `${wx.env.USER_DATA_PATH}/download_${ossId}.file`;
await uni.saveFile({
tempFilePath: response.data,
filePath: filePath
});
uni.showToast({
title: '文件下载成功',
icon: 'success'
});
} catch (error) {
uni.showToast({
title: '文件下载失败',
icon: 'none'
});
}
};
📈 监控指标
关键指标
- 文件访问成功率:安全访问接口的成功率
- 权限拒绝率:因权限不足被拒绝的访问次数
- 异常访问检测:检测到的异常访问行为
- 下载性能:文件下载的平均响应时间
告警规则
- 权限拒绝率 > 10%:可能存在权限配置问题
- 异常访问 > 5次/分钟:可能存在恶意访问
- 下载失败率 > 5%:可能存在系统问题
✅ 测试验证
功能测试
- 登录用户正常访问文件
- 未登录用户无法访问文件
- 非文件所有者无法访问文件
- 超级管理员可以访问所有文件
- 访问日志正确记录
安全测试
- 直接访问OSS URL被拒绝
- 权限验证正确执行
- 异常访问被正确拦截
- 敏感信息不被泄露
🚀 后续优化计划
阶段二:高级安全特性(后续实施)
- 实施方案二:基于Token的临时访问控制
- 实施方案三:基于角色的细粒度权限控制
- 添加审计日志:完整的文件访问审计系统
阶段三:监控和优化(长期实施)
- 访问行为分析:基于访问日志进行安全分析
- 异常检测:检测异常的文件访问行为
- 性能优化:优化文件访问性能
🎯 总结
通过实施基于后端代理的强制权限验证方案,我们成功解决了MinIO文件访问的安全问题:
- 强制用户身份验证:确保只有登录用户才能访问文件
- 细粒度权限控制:根据用户角色、部门等控制访问权限
- 完整的审计日志:记录所有文件访问行为
- 后端代理访问:避免直接暴露OSS URL
- 异常行为检测:及时发现和处理异常访问
这个方案提供了最佳的安全性和实现复杂度的平衡,推荐作为首选实施方案。