ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Java Web云存储系统:Spring Boot实现文件上传下载与权限控制

Java Web云存储系统:Spring Boot实现文件上传下载与权限控制 简介本资源为面向计算机相关专业本科生的毕业设计项目——Web云存储硬盘系统的设计与实现适用于课程设计、大作业及毕设参考尤其适合软件工程、计科、信息安全等方向的学习者开展全栈开发实践。项目已通过完整测试功能稳定涵盖需求分析、系统设计、数据库建模、前后端实现与文档撰写全流程具备扎实的工程示范价值。压缩包共125个文件约127.02MB包含33份Word设计文档含开题报告、论文终稿、登记表等、23个Visio系统架构与流程图、20张PNG界面与ER模型图、10份PDF技术说明与规范、8份补充文档及少量Vue前端代码、SQL建表脚本、PowerDesigner数据模型.cdm/.cdb等结构完整、资料翔实。目前已有114人学习下载读者可直接复用系统设计方案、参考标准文档模板、理解云存储核心模块如文件上传/下载、用户权限、目录树管理的实现逻辑并基于现有模型快速拓展二次开发。1. 这不是又一个“网盘前端”而是一套可落地的 Web 云存储硬盘系统骨架你打开浏览器输入http://localhost:8080看到的不是一个静态页面而是一个带用户登录、文件上传/下载/删除/重命名、目录树展开、分页列表、基础权限控制普通用户 vs 管理员的真实 Web 存储界面——它背后没有调用七牛云或阿里 OSS SDK所有文件元数据存 MySQL物理文件落本地磁盘/data/storage/uid/上传限速、断点续传、文件类型白名单、MD5 校验、路径遍历防护全部写死在 Controller 层。这不是 Demo是本科毕设里少有的、能直接部署到实验室服务器上跑一学期的完整闭环系统。它不追求高并发或分布式但把 Web 云存储最核心的 7 个能力模块身份认证、会话管理、文件元数据建模、物理存储路由、HTTP 文件流处理、前端交互契约、安全边界控制全用 Java Spring Boot Thymeleaf MySQL 实现了一遍。适合计科、软工、信安专业同学做课程设计复刻也适合作为大作业答辩时展示“我真写过服务端逻辑”的证据链。2. 从概念模型到数据库表结构为什么用 CDM 而不是直接手写 SQL2.1 系统概念模型 CDM 文件的逆向工程价值项目包里的系统概念模型.cdm是 PowerDesigner 导出的逻辑模型文件不是草图而是经过三轮 ER 图推演后的产物。它定义了 4 张核心表user用户主表、storage_file文件元数据主表、file_chunk分片上传记录表、user_role角色映射表。关键在于字段设计逻辑storage_file.path不存绝对路径而是相对路径uid/2023/09/abc.jpgstorage_file.size类型为BIGINT而非INT因为毕业设计明确要求支持单文件 ≤2GBfile_chunk表中chunk_index和total_chunks组合构成幂等上传依据而非依赖前端传参。这些细节在.doc论文中只提结论但在 CDM 里能反推出作者对 Web 文件上传协议的理解深度。2.2 MySQL 建表语句必须匹配 CDM 的约束条件直接执行以下建表脚本前请确认 MySQL 版本 ≥5.7因用到JSON类型存储扩展属性CREATE TABLE user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password_hash CHAR(64) NOT NULL COMMENT SHA-256 hex, email VARCHAR(100), role ENUM(USER,ADMIN) DEFAULT USER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_login DATETIME NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE storage_file ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, filename VARCHAR(255) NOT NULL, path VARCHAR(512) NOT NULL COMMENT relative path under /data/storage/, size BIGINT NOT NULL, md5_hash CHAR(32) NOT NULL, mime_type VARCHAR(100), is_deleted TINYINT(1) DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_user_path (user_id, path), FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;提示storage_file.path字段必须用VARCHAR(512)不能简化为TEXT。因为后续SELECT * FROM storage_file WHERE path LIKE uid/%查询会走索引而TEXT类型无法在前缀索引外高效匹配。2.3 文件分片表file_chunk的设计意图与使用场景该表解决的是毕业设计任务书里明确要求的“支持大文件断点续传”。其结构如下字段名类型说明idBIGINT PK主键upload_idVARCHAR(64)前端生成的唯一上传 ID如a1b2c3d4e5f6chunk_indexINT当前分片序号从 0 开始total_chunksINT总分片数md5_chunkCHAR(32)本分片 MD5用于校验statusENUM(PENDING,SUCCESS,FAILED)分片状态created_atDATETIME创建时间当用户上传 500MB 视频时前端按 5MB 分片共 100 片每片 POST 到/api/upload/chunk接口后端校验upload_id chunk_index是否已存在避免重复写入。最终合并逻辑在POST /api/upload/merge中触发查询file_chunk中upload_id ? AND status SUCCESS的记录数是否等于total_chunks全部成功才写入storage_file主表并拼接物理路径。3. Spring Boot 后端核心逻辑文件流处理与安全边界控制3.1 文件上传接口的三层校验链毕业设计文档中强调“防止恶意文件上传”实际代码通过以下三重过滤实现3.1.1 HTTP 层Spring MVC 参数绑定拦截PostMapping(/api/upload) public ResponseEntityUploadResponse uploadFile( RequestParam(file) MultipartFile file, AuthenticationPrincipal UserPrincipal user) { // 1. 文件名合法性检查防 ../ 路径遍历 String cleanFilename FilenameUtils.getName(file.getOriginalFilename()); if (!cleanFilename.matches(^[a-zA-Z0-9._\\-\\s()]$)) { return ResponseEntity.badRequest().body(new UploadResponse(非法文件名)); } // 2. MIME 类型白名单非仅靠前端传参 String mimeType URLConnection.guessContentTypeFromName(cleanFilename); if (!Set.of(image/jpeg, image/png, text/plain, application/pdf).contains(mimeType)) { return ResponseEntity.badRequest().body(new UploadResponse(不支持的文件类型)); } // 3. 文件大小硬限制Spring Boot 配置已设 max-file-size2MB此处再校验 if (file.getSize() 2 * 1024 * 1024) { return ResponseEntity.badRequest().body(new UploadResponse(文件超过 2MB)); } // ... 后续存储逻辑 }注意URLConnection.guessContentTypeFromName()仅作辅助判断真实 MIME 校验需读取文件头字节见 3.2.2。此处先拦住明显异常请求降低服务器负载。3.1.2 业务层物理路径构造与权限隔离// UserService.java public String buildStoragePath(Long userId, String filename) { String safeFilename FilenameUtils.getName(filename); // 再次清理 String userDir String.format(%d, userId); // 用户 ID 作为子目录 String dateDir LocalDate.now().format(DateTimeFormatter.ofPattern(yyyy/MM/dd)); return String.format(%s/%s/%s, userDir, dateDir, safeFilename); } // FileStorageService.java public void savePhysicalFile(String relativePath, InputStream inputStream) throws IOException { Path targetPath Paths.get(/data/storage/, relativePath); Files.createDirectories(targetPath.getParent()); // 自动创建多级目录 Files.copy(inputStream, targetPath, StandardCopyOption.REPLACE_EXISTING); }关键点relativePath由buildStoragePath()生成绝不拼接用户原始输入Files.createDirectories()确保uid/2023/09/目录自动创建避免NoSuchFileExceptionStandardCopyOption.REPLACE_EXISTING允许同名覆盖符合毕业设计需求。3.2 文件下载接口的流式响应与防盗链GET /api/download/{fileId}接口不返回 JSON而是StreamingResponseBodyGetMapping(/api/download/{fileId}) public ResponseEntityStreamingResponseBody downloadFile( PathVariable Long fileId, AuthenticationPrincipal UserPrincipal user) { StorageFile file fileService.findByIdAndUser(fileId, user.getId()); if (file null || file.getIsDeleted() 1) { return ResponseEntity.notFound().build(); } Path filePath Paths.get(/data/storage/, file.getPath()); HttpHeaders headers new HttpHeaders(); headers.setContentDisposition(ContentDisposition.attachment().filename(file.getFilename()).build()); headers.setContentType(MediaType.parseMediaType(file.getMimeType())); headers.setContentLength(Files.size(filePath)); StreamingResponseBody stream outputStream - { try (InputStream is Files.newInputStream(filePath)) { is.transferTo(outputStream); } }; return ResponseEntity.ok() .headers(headers) .body(stream); }提示fileService.findByIdAndUser()方法内部执行SELECT * FROM storage_file WHERE id ? AND user_id ? AND is_deleted 0强制绑定当前登录用户 ID杜绝越权下载。这是毕业设计答辩时评委必问的安全点。3.3 权限控制的细粒度实现基于PreAuthorize的方法级注解在FileController中删除操作要求管理员或文件所有者DeleteMapping(/api/file/{id}) PreAuthorize(filePermissionService.canDelete(#id, authentication)) public ResponseEntityVoid deleteFile(PathVariable Long id) { fileService.markAsDeleted(id); return ResponseEntity.noContent().build(); }对应FilePermissionService.canDelete()实现public boolean canDelete(Long fileId, Authentication auth) { Long currentUserId ((UserPrincipal) auth.getPrincipal()).getId(); StorageFile file fileRepository.findById(fileId).orElse(null); if (file null) return false; // 普通用户只能删自己的管理员可删所有 return file.getUserId().equals(currentUserId) || auth.getAuthorities().stream() .anyMatch(a - a.getAuthority().equals(ROLE_ADMIN)); }4. Thymeleaf 前端交互契约如何让页面与后端 API 严丝合缝4.1 文件列表页的分页与状态同步/templates/file/list.html使用 Thymeleaf 的th:each渲染文件列表关键在于th:if控制按钮显隐tr th:eachfile : ${page.content} td th:text${file.filename}filename/td td th:text${#numbers.formatInteger(file.size, 0)}size/td td a th:href{/api/download/{id}(id${file.id})} classbtn btn-sm btn-primary下载/a button th:if${file.userId #authentication.principal.id or #authentication.principal.admin} th:data-id${file.id} classbtn btn-sm btn-danger delete-btn删除/button /td /tr注意th:if中的#authentication.principal.admin对应后端UserPrincipal类的isAdmin()方法该方法读取authorities列表判断是否含ROLE_ADMIN。前端不信任user.role字段而是通过#authentication对象实时获取权限避免前端篡改 JS 变量绕过控制。4.2 上传组件的分片逻辑与错误重试毕业设计要求“支持断点续传”前端使用axios分片上传核心逻辑在upload.jsasync function uploadByChunk(file, uploadId) { const chunkSize 5 * 1024 * 1024; // 5MB const totalChunks Math.ceil(file.size / chunkSize); for (let i 0; i totalChunks; i) { const start i * chunkSize; const end Math.min(start chunkSize, file.size); const chunk file.slice(start, end); let retry 0; while (retry 3) { try { await axios.post(/api/upload/chunk, { uploadId, chunkIndex: i, totalChunks, chunk }, { headers: { Content-Type: multipart/form-data } } ); break; // 成功则跳出重试 } catch (e) { retry; if (retry 3) throw e; await sleep(1000 * retry); // 指数退避 } } } // 所有分片成功后合并 await axios.post(/api/upload/merge, { uploadId }); }4.3 安全敏感操作的二次确认弹窗删除文件前弹出 Modal且提交时携带 CSRF Token!-- 删除确认 Modal -- div classmodal fade iddeleteModal tabindex-1 div classmodal-dialog div classmodal-content form th:action{/api/file/{id}(id${deleteTargetId})} th:methoddelete th:object${_csrf} input typehidden th:name${_csrf.parameterName} th:value${_csrf.token} / div classmodal-body p确定要删除 strong th:text${deleteTargetName}文件名/strong 吗/p /div div classmodal-footer button typebutton classbtn btn-secondary># 创建存储根目录Linux/macOS sudo mkdir -p /data/storage sudo chown -R $USER:$USER /data/storage # Windows 下需在 application.yml 中修改路径为 D:/data/storage/5.2 启动与基础功能验证表启动后访问http://localhost:8080按顺序验证以下 5 个关键链路步骤操作预期结果故障排查点1注册新用户邮箱可填 fakeexample.com收到Registration success提示MySQLuser表新增记录检查application.yml中spring.mail.host是否配置若禁用邮件需注释掉Async发送逻辑2登录后上传一个2MB的 PNG 文件页面显示上传成功/data/storage/{uid}/2023/xx/xxx.png物理文件存在查看logs/app.log中FileStorageService.savePhysicalFile是否打印Saved file to ...3点击下载按钮浏览器弹出保存对话框文件名与上传时一致抓包确认响应头含Content-Disposition: attachment; filenamexxx.png4用管理员账号初始密码见src/main/resources/application.yml登录删除他人文件返回 403 Forbidden检查FilePermissionService.canDelete()中file.getUserId().equals(...)是否执行5上传一个.jsp文件返回不支持的文件类型错误确认MultipartFile.getContentType()是否为text/x-java-source白名单是否包含该类型5.3 生产部署关键参数调整若需部署到实验室服务器修改application-prod.ymlserver: port: 8080 compression: enabled: true mime-types: text/html,text/css,application/javascript,image/svgxml spring: datasource: url: jdbc:mysql://prod-db:3306/cloud_storage?useSSLfalseserverTimezoneAsia/Shanghai servlet: context-path: /cloud-disk # 避免根路径冲突 # 文件上传最大限制生产环境建议 10MB spring: servlet: multipart: max-file-size: 10MB max-request-size: 10MB # 日志级别调优 logging: level: com.example.cloud: INFO org.springframework.web.servlet.DispatcherServlet: WARN提示context-path: /cloud-disk可使系统挂载在https://lab-server.edu.cn/cloud-disk/下避免与学校其他 Web 服务端口冲突。此配置在毕业设计答辩演示时能体现工程化思维。本文还有配套的精品资源点击获取
返回列表