1. 项目概述为什么一个富文本编辑器的本地图片上传值得花三天重搭整套SSM环境“SSMMAVEN整合百度富文本编辑器Ueditor含后台代码上传图片保存到本地”——这个标题看起来像一段被复制粘贴进招聘JD的技术栈组合但如果你真在某高校实验室或中小公司接手过一个老系统维护任务就会明白它背后藏着的是真实业务场景里最棘手的三重断层——前端交互、后端权限与文件系统落地。我去年帮某高校教务处升级课程申报系统时就卡在这个环节整整72小时前端Ueditor界面一切正常点击上传按钮后控制台返回200但服务器磁盘上连个.jpg文件的影子都没有日志里只有一行模糊的io exception: null连报错堆栈都截断了。后来发现问题既不在SpringMVC的ResponseBody配置也不在MyBatis的事务管理而是在Maven依赖树里ueditor-jsp模块悄悄引入了commons-fileupload 1.2.2而项目主POM里又强制指定了1.3.3——两个版本的FileItemIterator类签名不兼容导致Ueditor的BinaryUploader在解析multipart请求时直接静默失败。这正是本项目的核心价值它不是教你怎么把Ueditor拖进HTML页面而是用可复现、可审计、可交付的完整链路打通从浏览器点击“上传”到硬盘生成/upload/image/20240521/abc123.jpg的全部技术关节。关键词“SSM”代表你必须面对XML配置与注解混用的历史包袱“MAVEN”意味着你要亲手梳理依赖冲突“百度富文本编辑器Ueditor”是那个文档稀疏、社区支持断档的“半开源”组件“上传图片保存到本地”则直指Java Web最易被忽视的底层能力——文件I/O权限、路径拼接安全、MIME类型校验、并发写入保护。适合三类人正在赶毕设答辩的计算机专业学生需要能直接打包部署的war包、接手遗留系统的初级开发要避开我踩过的17个坑、以及负责技术选型的小组长需评估Ueditor在现代Spring Boot项目中的替代成本。接下来所有内容都基于我在CentOS 7 Tomcat 8.5 JDK 1.8环境下实测通过的完整流程代码无删减配置无美化错误日志原样保留。2. 整体架构设计与方案选型逻辑为什么坚持用原始JSP版Ueditor而不是VueElement-Plus2.1 技术栈锁定的必然性SSM不是选择而是约束条件看到标题里的“SSM”很多年轻开发者第一反应是“这太老了应该用Spring Boot”。但现实是某高校教务处的课程系统运行在2016年部署的Tomcat集群上数据库是Oracle 11g中间件是WebLogic 12c所有运维脚本、监控探针、备份策略都围绕这套体系构建。强行升级框架意味着要协调3个部门签字、申请2周停机窗口、重新做等保测评——而业务方只给了3天解决“教师无法上传教学大纲图片”的紧急需求。因此“SSMMAVEN”不是技术偏好而是生产环境的物理边界。我们所有设计必须在这个边界内完成Spring负责IOC和事务SpringMVC处理HTTP请求路由MyBatis操作数据库虽然本项目暂不涉及DB读写但后续扩展必须预留接口Maven管理依赖并保证构建可重现。2.2 Ueditor版本抉择为什么放弃官方GitHub上的Java版死磕百度2015年发布的JSP包百度Ueditor官网早已停止更新其GitHub仓库fex-team/ueditor最后提交是2019年且明确标注“Java后端示例仅作参考不保证可用”。我们测试了三个主流分支master分支的ueditor-java依赖json-lib与SpringMVC的jackson-databind冲突启动时报NoSuchMethodError: net.sf.json.JSONObject.fromObjectdev分支的ueditor-spring-boot-starter需要Spring Boot 2.3与SSM的web.xml初始化机制不兼容官网下载的ueditor1_4_3-utf8-jsp.zip2015年发布虽陈旧但结构清晰——jsp/controller.jsp是唯一入口所有上传逻辑集中在com.baidu.ueditor.upload.BinaryUploader类中源码可调试错误可追踪。最终选择JSP版是因为它把复杂度锁死在可控范围内没有Spring Boot自动配置的黑盒没有Starter的隐式依赖所有路径、参数、响应格式都明文写在config.json和controller.jsp里。当BinaryUploader抛出异常时你能直接在IDE里打断点看到request.getInputStream()返回的字节数组是否为空而不是在层层代理中迷失。2.3 “保存到本地”的深层含义不是FileOutputStream而是安全沙箱“上传图片保存到本地”常被误解为“用Java写文件”。但真实生产环境要求远不止于此路径安全必须防止../../etc/passwd路径遍历攻击Ueditor默认的imagePathFormat配置若设为/upload/{yyyy}{mm}{dd}/{filename}攻击者可构造filename../../../webapps/ROOT/shell.jsp磁盘隔离不能将图片存到webapps/ROOT/upload/下否则用户可通过http://domain.com/upload/xxx.jpg直接访问绕过权限校验并发保护多教师同时上传头像时SimpleDateFormat(yyyyMMdd)在多线程下会返回错误日期导致文件覆盖存储冗余单机磁盘故障即丢失所有图片但本项目暂不引入分布式存储故采用“本地存储定时rsync备份”折中方案。因此我们的“本地保存”实现包含四层防护① 路径白名单校验只允许/upload/image/前缀② 文件名UUID重命名杜绝中文乱码与特殊字符③java.nio.file.Files.createDirectories()确保目录原子创建④StandardOpenOption.CREATE_NEW标志强制文件不可覆盖。2.4 Maven依赖治理一场与传递性依赖的战争Maven的scopeprovided/scope常被滥用。Ueditor JSP包自带lib/目录包含json-lib-2.4-jdk15.jar、commons-fileupload-1.3.1.jar等。若直接将这些JAR丢进WEB-INF/lib/会与Maven主POM中声明的spring-webmvc依赖commons-fileupload 1.3.3产生版本冲突。解决方案是在pom.xml中显式排除Ueditor的传递依赖dependency groupIdcom.baidu.ueditor/groupId artifactIdueditor/artifactId version1.4.3/version scopesystem/scope systemPath${project.basedir}/src/main/webapp/WEB-INF/lib/ueditor-1.4.3.jar/systemPath exclusions exclusion groupIdcommons-fileupload/groupId artifactIdcommons-fileupload/artifactId /exclusion /exclusions /dependency单独声明commons-fileupload 1.3.3为compile作用域确保类加载器优先使用此版本将Ueditor的config.json从WEB-INF/jsp/移至src/main/resources/通过Spring的ResourceBundleMessageSource统一管理避免JSP中硬编码路径。这看似繁琐却是保障controller.jsp中new BinaryUploader().exec()能正确解析request对象的唯一方式——因为BinaryUploader内部调用的FileItemIterator必须与SpringMVC的CommonsMultipartResolver使用同一份commons-fileupload字节码。3. 核心细节解析与实操要点从config.json到Controller.jsp的每一行代码3.1 config.json的致命陷阱四个必须修改的字段Ueditor的config.json是后端配置核心但官网文档对关键字段语焉不详。以下是我们实测必须修改的四项基于ueditor1_4_3-utf8-jsp.zip解压后的jsp/config.json字段名原始值修改后值为什么必须改实测后果imagePathFormat/ueditor/jsp/upload/image/{yyyy}{mm}{dd}/{time}{rand:6}/upload/image/{yyyy}{mm}{dd}/{filename}原始值用{time}{rand:6}生成无扩展名文件Nginx无法识别MIME类型且{filename}需经后端重命名不能直接信任前端传参浏览器显示“网络错误”Chrome DevTools Network标签页显示Failed to load resourceimageUrlPrefixhttp://your-domain.com空值导致Ueditor生成src/upload/image/20240521/abc.jpg而实际图片存于/var/www/upload/image/...需通过Nginx反向代理映射富文本中图片显示为红叉控制台报404imageAllowFiles[.png, .jpg, .jpeg, .gif, .bmp][.png, .jpg, .jpeg, .gif, .bmp, .webp]新版Chrome默认导出webp格式截图不加此项会导致教师上传截图失败Ueditor提示“文件类型不允许”但未告知具体不支持类型imageMaxSize20480002MB1048576010MB高清教学图表、扫描版PDF转图常超2MB上传进度条卡在99%无错误提示实为BinaryUploader内部if (file.getSize() maxSize)拦截提示imageUrlPrefix必须带协议http://或https://若填//your-domain.comUeditor会将其与当前页面协议拼接导致HTTPS页面加载HTTP资源被浏览器阻止。3.2 Controller.jsp的改造从“胶水代码”到“安全网关”官方controller.jsp本质是Servlet的JSP化封装但存在严重安全隐患。我们重写了/jsp/controller.jsp注意不是/jsp/upload/controller.jsp后者是旧版路径% page languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-8% % page importcom.baidu.ueditor.ActionEnter % % page importcom.baidu.ueditor.PathFormat % % page importcom.baidu.ueditor.Uploader % % page importcom.baidu.ueditor.define.AppInfo % % page importcom.baidu.ueditor.define.BaseState % % page importcom.baidu.ueditor.define.State % % page importcom.baidu.ueditor.upload.BinaryUploader % % page importjava.io.File % % page importjava.text.SimpleDateFormat % % page importjava.util.Date % % page importjava.util.UUID % % request.setCharacterEncoding(UTF-8); response.setContentType(application/json;charsetUTF-8); // 1. 路径白名单校验只允许访问 /upload/ 下的资源 String rootPath application.getRealPath(/); String uploadBasePath rootPath upload/; if (!request.getRequestURI().contains(/upload/)) { out.write({\state\:\拒绝访问\}); return; } // 2. 初始化Ueditor ActionEnter关键必须传入真实路径 String actionType request.getParameter(action); String configPath application.getRealPath(/WEB-INF/jsp/config.json); ActionEnter enter new ActionEnter(request, configPath); // 3. 图片上传专项处理替换BinaryUploader的savePath if (uploadimage.equals(actionType)) { // 获取原始配置中的imagePathFormat String imagePathFormat enter.getConfig().get(imagePathFormat).toString(); // 构建绝对路径/var/www/tomcat/webapps/ROOT/upload/image/20240521/ String datePath new SimpleDateFormat(yyyyMM).format(new Date()); String saveDir uploadBasePath image/ datePath /; File dir new File(saveDir); if (!dir.exists()) { dir.mkdirs(); // 注意必须用mkdirs()而非mkdir()确保多级目录创建 } // 重命名文件UUID 原扩展名 String originalName request.getParameter(fileName); String ext ; if (originalName ! null originalName.lastIndexOf(.) 0) { ext originalName.substring(originalName.lastIndexOf(.)); } String newFileName UUID.randomUUID().toString().replace(-, ) ext.toLowerCase(); // 强制设置savePath为绝对路径绕过Ueditor的相对路径解析bug enter.getConfig().put(savePath, saveDir); enter.getConfig().put(fileName, newFileName); } // 4. 执行动作并输出JSON out.write(enter.exec()); %这段代码解决了五个关键问题字符编码request.setCharacterEncoding(UTF-8)必须在ActionEnter初始化前执行否则中文文件名乱码路径越权通过request.getRequestURI().contains(/upload/)拦截非上传请求防止controller.jsp?actionconfig被恶意探测绝对路径application.getRealPath(/)获取Tomcat部署根路径避免Ueditor默认的../相对路径在不同容器中解析失败线程安全SimpleDateFormat未被共享每次新建实例杜绝多线程日期解析错误扩展名标准化ext.toLowerCase()强制小写避免.JPG被imageAllowFiles中的.jpg拒绝。3.3 SpringMVC的multipart配置为什么CommonsMultipartResolver必须与Ueditor同频Ueditor JSP版依赖commons-fileupload解析multipart请求而SpringMVC也需此库处理RequestParam MultipartFile。若两者版本不一致request.getInputStream()会被某一方提前消费导致另一方读取空流。我们在spring-mvc.xml中配置!-- 必须与Ueditor使用的commons-fileupload版本严格一致 -- bean idmultipartResolver classorg.springframework.web.multipart.commons.CommonsMultipartResolver property namemaxUploadSize value10485760/ !-- 10MB -- property namemaxInMemorySize value40960/ !-- 40KB超过则写入临时文件 -- property namedefaultEncoding valueUTF-8/ !-- 关键指定临时文件目录避免Linux系统/tmp空间不足 -- property nameuploadTempDir valuefile:///var/tmp/ueditor-upload// /bean注意uploadTempDir必须是绝对路径且Tomcat进程用户如tomcat对该目录有读写权限。我们创建目录并授权sudo mkdir -p /var/tmp/ueditor-upload sudo chown tomcat:tomcat /var/tmp/ueditor-upload sudo chmod 755 /var/tmp/ueditor-upload3.4 Nginx反向代理配置让“本地存储”对外表现为“静态资源服务”图片存于/var/www/upload/但Tomcat默认不提供静态文件服务。我们用Nginx暴露该目录# /etc/nginx/conf.d/ueditor.conf server { listen 80; server_name your-domain.com; # 将 /upload/ 请求代理到本地文件系统 location /upload/ { alias /var/www/upload/; # 禁止执行脚本防止上传webshell location ~ \.(php|jsp|asp|sh|pl|py)$ { deny all; } # 添加缓存头减少重复请求 expires 1h; add_header Cache-Control public, no-transform; } # 其他请求代理到Tomcat location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }重启Nginx后Ueditor生成的图片URL如http://your-domain.com/upload/image/20240521/abc123.jpg即可被浏览器直接加载。此配置比“将图片存入webapps”更安全因为/var/www/upload/完全独立于Tomcat应用目录即使Tomcat被攻破攻击者也无法通过Web路径写入该目录。4. 实操过程与核心环节实现从零开始搭建可运行环境的完整步骤4.1 环境准备CentOS 7 Tomcat 8.5 JDK 1.8的最小化安装我们摒弃Docker等抽象层采用最接近生产环境的裸机配置。所有命令均在root用户下执行# 1. 安装JDK 1.8必须Ueditor 1.4.3不兼容JDK 11 wget --no-cookies --no-check-certificate --header Cookie: gpw_e24http%3A%2F%2Fwww.oracle.com%2F; oraclelicenseaccept-securebackup-cookie https://download.oracle.com/otn-pub/java/jdk/8u202-b08/1961070e4c9b4e26a04e7f5a083f551e/jdk-8u202-linux-x64.rpm rpm -ivh jdk-8u202-linux-x64.rpm # 2. 安装Tomcat 8.5.94官网最新8.5.x cd /opt wget https://downloads.apache.org/tomcat/tomcat-8/v8.5.94/bin/apache-tomcat-8.5.94.tar.gz tar -xzf apache-tomcat-8.5.94.tar.gz ln -s apache-tomcat-8.5.94 tomcat # 3. 创建专用用户禁止root运行Tomcat useradd -r -s /bin/false tomcat chown -R tomcat:tomcat /opt/tomcat chown -R tomcat:tomcat /opt/apache-tomcat-8.5.94 # 4. 配置Tomcat启动脚本/etc/systemd/system/tomcat.service [Unit] DescriptionApache Tomcat Web Application Container Afternetwork.target [Service] Typeforking EnvironmentJAVA_HOME/usr/java/jdk1.8.0_202 EnvironmentCATALINA_PID/opt/tomcat/temp/tomcat.pid EnvironmentCATALINA_HOME/opt/tomcat EnvironmentCATALINA_BASE/opt/tomcat EnvironmentCATALINA_OPTS-Xms512M -Xmx1024M -server -XX:UseParallelGC EnvironmentJAVA_OPTS-Djava.awt.headlesstrue -Djava.security.egdfile:/dev/./urandom ExecStart/opt/tomcat/bin/startup.sh ExecStop/opt/tomcat/bin/shutdown.sh Usertomcat Grouptomcat UMask0007 RestartSec10 Restartalways [Install] WantedBymulti-user.target启用服务systemctl daemon-reload systemctl enable tomcat systemctl start tomcat # 检查端口netstat -tuln | grep :80804.2 Maven项目初始化用archetype快速生成SSM骨架避免手动创建XML配置使用Maven Archetype生成标准结构mvn archetype:generate \ -DgroupIdcom.example.ueditor \ -DartifactIdssm-ueditor-demo \ -DarchetypeArtifactIdmaven-archetype-webapp \ -DinteractiveModefalse cd ssm-ueditor-demo # 创建标准目录结构 mkdir -p src/main/{java,resources,webapp/WEB-INF} mkdir -p src/test/{java,resources}pom.xml关键依赖精简版已排除冲突properties spring.version4.3.30.RELEASE/spring.version mybatis.version3.4.6/mybatis.version mysql.version5.1.47/mysql.version slf4j.version1.7.32/slf4j.version commons-fileupload.version1.3.3/commons-fileupload.version /properties dependencies !-- Spring Core -- dependency groupIdorg.springframework/groupId artifactIdspring-core/artifactId version${spring.version}/version /dependency !-- Spring MVC -- dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency !-- MyBatis -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency !-- Commons FileUpload (必须与Ueditor一致) -- dependency groupIdcommons-fileupload/groupId artifactIdcommons-fileupload/artifactId version${commons-fileupload.version}/version /dependency !-- Ueditor JSP包本地引用 -- dependency groupIdcom.baidu.ueditor/groupId artifactIdueditor/artifactId version1.4.3/version scopesystem/scope systemPath${project.basedir}/src/main/webapp/WEB-INF/lib/ueditor-1.4.3.jar/systemPath /dependency /dependencies4.3 Ueditor集成五步完成前端嵌入与后端联通步骤1部署Ueditor文件# 解压ueditor1_4_3-utf8-jsp.zip到src/main/webapp/ unzip ueditor1_4_3-utf8-jsp.zip -d src/main/webapp/ # 移动JSP文件到标准位置 mv src/main/webapp/jsp/ src/main/webapp/WEB-INF/ # 复制lang目录多语言支持 cp -r src/main/webapp/lang/ src/main/webapp/步骤2配置web.xml关键注册Ueditor的JSP Servlet!-- src/main/webapp/WEB-INF/web.xml -- servlet servlet-nameUeditorController/servlet-name jsp-file/WEB-INF/jsp/controller.jsp/jsp-file /servlet servlet-mapping servlet-nameUeditorController/servlet-name url-pattern/ueditor/jsp/controller.jsp/url-pattern /servlet-mapping步骤3编写前端HTMLsrc/main/webapp/index.jsp% page contentTypetext/html;charsetUTF-8 languagejava % html head titleUeditor SSM集成示例/title script typetext/javascript src/ueditor/ueditor.config.js/script script typetext/javascript src/ueditor/ueditor.all.min.js/script link relstylesheet href/ueditor/themes/default/css/ueditor.css/ /head body h2课程简介编辑器/h2 script idcontainer namecontent typetext/plain stylewidth:100%;height:500px; lt;pgt;请输入课程简介...lt;/pgt; /script script typetext/javascript var ue UE.getEditor(container, { // 指向我们重写的controller.jsp serverUrl: /ueditor/jsp/controller.jsp, // 配置图片上传路径必须与config.json的imageUrlPrefix一致 imageUrlPrefix: http://your-domain.com, // 启用图片压缩教师上传高清图时自动缩放 imageCompressEnable: true, imageCompressBorder: 1600, // 自定义工具栏隐藏不需要的功能 toolbars: [[ fullscreen, source, |, undo, redo, |, bold, italic, underline, fontborder, strikethrough, superscript, subscript, removeformat, formatmatch, autotypeset, blockquote, pasteplain, |, insertimage, emotion, scrawl, insertvideo, music, attachment, map, gmap, insertframe, insertcode, webapp, pagebreak, template, background, |, rowspacingtop, rowspacingbottom, lineheight, |, customstyle, paragraph, fontfamily, fontsize, |, directionalityltr, directionalityrtl, indent, |, justifyleft, justifycenter, justifyright, justifyjustify, |, touppercase, tolowercase, |, horizontal, date, time, spechars, snapscreen, wordimage, |, inserttable, deletetable, insertcaption, inserttitle, insertcell, mergecells, splittocells, deletecell, insertcol, deletecol, insertrow, deleterow, alignleft, aligncenter, alignright, cellaligncenter, cellalignright ]] }); /script /body /html步骤4构建WAR包并部署# 清理并打包 mvn clean package -Dmaven.test.skiptrue # 复制WAR到Tomcat cp target/ssm-ueditor-demo.war /opt/tomcat/webapps/ # Tomcat自动解压访问 http://localhost:8080/ssm-ueditor-demo/步骤5验证上传流程浏览器开发者工具全程跟踪打开Chrome DevTools → Network标签页在Ueditor中点击“插入图片” → “本地上传”选择一张test.jpg大小5MB观察Network中controller.jsp?actionuploadimage请求Headers → Request Payload确认Content-Type: multipart/form-data; boundary----WebKitFormBoundary...Response应返回{state:SUCCESS,url:/upload/image/20240521/abc123.jpg,...}SSH登录服务器检查文件ls -lh /var/www/upload/image/20240521/ # 输出-rw-r--r-- 1 tomcat tomcat 4.8M May 21 14:22 abc123.jpg4.4 生产环境加固三道防线保障图片服务稳定防线1磁盘空间监控脚本/opt/scripts/check-upload-disk.sh#!/bin/bash # 检查 /var/www/upload 分区使用率 USAGE$(df /var/www/upload | awk NR2 {print $5} | sed s/%//) if [ $USAGE -gt 85 ]; then echo $(date): /var/www/upload usage is ${USAGE}% | mail -s Ueditor Disk Alert adminexample.com # 自动清理30天前的图片 find /var/www/upload/image/ -type f -mtime 30 -delete fi添加到crontab每小时执行0 * * * * /opt/scripts/check-upload-disk.sh防线2Nginx访问日志分析统计异常上传# 在Nginx配置中添加日志格式 log_format ueditor_upload $remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent rt$request_time uct$upstream_connect_time uht$upstream_header_time urt$upstream_response_time; access_log /var/log/nginx/ueditor-upload.log ueditor_upload;用awk分析高频错误# 统计413错误请求体过大 awk $9 413 {count} END {print 413 errors:, count} /var/log/nginx/ueditor-upload.log防线3Ueditor前端二次校验防绕过在ueditor.config.js中添加// 前端文件大小校验与后端imageMaxSize一致 UE.registerUI(custom-upload, function(editor, uiName) { editor.addListener(beforeInsertImage, function(e, data) { for (var i 0; i data.length; i) { if (data[i].src data[i].src.size 10485760) { alert(图片大小不能超过10MB请压缩后重试); e.preventDefault(); return false; } } }); });5. 常见问题与排查技巧实录那些让你凌晨三点还在看日志的坑5.1 问题速查表按现象分类的解决方案现象可能原因排查命令/步骤解决方案上传按钮无响应控制台无任何日志Ueditor JS未加载成功curl -I http://domain.com/ueditor/ueditor.config.js检查HTTP状态码检查web.xml中servlet-mapping路径是否与HTML中script src路径一致确认ueditor/目录在webapps/ROOT/下上传后提示“网络错误”Network中controller.jsp返回空白controller.jsp中out.write()被缓冲未刷新在controller.jsp末尾添加out.flush()在out.write(enter.exec());后增加out.flush();强制输出缓冲区图片上传成功但Ueditor预览显示红叉URL为/upload/image/xxx.jpgNginx未配置/upload/反向代理curl -I http://domain.com/upload/image/xxx.jpg检查Nginx配置中location /upload/块是否生效alias路径是否指向正确目录上传大文件2MB时Tomcat报java.lang.OutOfMemoryError: Java heap spaceTomcat JVM内存不足ps aux | grep tomcat查看-Xmx参数编辑/opt/tomcat/bin/catalina.sh增加JAVA_OPTS-Xms1024m -Xmx2048m多用户同时上传部分图片文件名重复导致覆盖SimpleDateFormat非线程安全在controller.jsp中打印new Date().getTime()改用Instant.now().toString().replace([^a-zA-Z0-9], )生成时间戳或使用ThreadLocalSimpleDateFormat5.2 深度排查案例一次诡异的FileNotFoundException现象教师上传图片后Ueditor返回{state:SUCCESS,url:/upload/image/20240521/abc123.jpg}但访问该URL返回404。SSH检查文件存在Nginx日志显示200却无法加载。排查过程ls -l /var/www/upload/image/20240521/abc123.jpg→ 权限为-rw-------属主rootps aux \| grep nginx→ Nginx主进程由root运行worker进程由nginx用户运行sudo -u nginx ls /var/www/upload/image/20240521/→Permission denied。根本原因Ueditor的BinaryUploader在savePath不存在时调用new File(saveDir).mkdirs()创建目录但该方法创建的目录权限为700仅属主可读写而Nginx worker进程以nginx用户运行无权读取。解决方案方案A推荐在controller.jsp中创建目录后显式设置权限if (!dir.exists()) { dir.mkdirs(); // 设置目录权限为755 Runtime.getRuntime().exec(chmod 755 dir.getAbsolutePath()); }方案B更优修改Linux umask使新创建目录默认为755# 编辑 /etc/init.d/tomcat添加 umask 0225.3 实操心得来自17次失败的经验总结心得1永远不要信任Ueditor的config.json示例值官网示例中imagePathFormat写成/upload/{yyyy}{mm}{dd}/{filename}但{filename}会被Ueditor原样使用若教师上传病毒.jsp则直接生成/upload/20240521/病毒.jsp。我们必须在controller.jsp中强制重命名这是最后一道防线。心得2application.getRealPath(/)在Linux容器中可能返回null若Tomcat以-Dcatalina.base/opt/tomcat启动且webapps目录是软链接getRealPath会失效。此时应改用ServletContext.getResourceAsStream(/WEB-INF/jsp/config.json)读取配置