资讯动态

老JSP后台集成eWebEditor:部署、图片上传与XSS防护

发布时间:2026/10/25 16:45:19 来源:尧图企业网站定制
简介面向JSP开发者的eWebEditor在线文本编辑器v0.1.4修改版适合在Java Web项目中快速集成可视化富文本编辑能力。它支持字体格式、图片与多媒体插入、超链接和邮件链接也可切换HTML源码编辑便于非专业用户完成内容排版同时为开发者保留API定制与JSP后台交互空间。压缩包共264个文件约796KB包含186个gif界面素材、27个css样式、21个htm示例页以及jsp、js、class、jar等运行与集成文件另有少量xml、jpg和txt说明目录按编辑器资源组织便于部署和二次调整。已有173人学习下载。对需要轻量级JSP富文本组件的开发者而言可借鉴其按钮布局、上传与远程图片处理、请求封装及兼容性处理思路也能据此排查XSS防护和部署配置问题。1. 一份 v0.1.4 的 JSP 压缩包为什么老后台还在用它上周去帮一个内网 OA 排查问题页面还是十年前的 JSPjQuery 版本停在 1.4工程里连 package.json 都没有。业务方要在公告表单里加图文混排和附件上传前端同事推过来一个现代富文本组件光打包配置和 Node 版本就够折腾一下午最后还卡在 IE 兼容上。这种场景下eWebEditor 在线文本编辑器 JSP 版这类老资源的价值就体现出来了压缩包解压丢进 webapp页面里一个 iframe 标签接上去浏览器里就能敲字、调字号、插图。它解决的不是炫技是在不改造老工程的前提下把富文本编辑塞进去。适合还在维护 JSP/Servlet 老系统、需要图文编辑加文件上传、又没条件引入前端构建链的人。下面从拆包、部署、上传配置一路讲到踩坑。2. 拆包看骨架JSP 版编辑器由哪几层撑起来2.1 目录结构与各文件的职责边界拿到压缩包先别急着往 Tomcat 里扔解出来看一眼层级。不同打包方式会有出入但这类老编辑器的骨架高度一致认准几个关键位置就行。下面这张表是我拆包时会先过一遍的清单路径名以压缩包内实际写法为准职责是固定的路径/文件职责动它的风险ewebeditor.jsp部分包叫 editor.jsp编辑器外壳页接收 id、style 等参数高参数名改动会断掉所有调用页dialog/ 目录插入图片、表格、颜色、特殊字符等弹窗页中改样式容易改逻辑容易崩style/ 目录样式配置standard 等多套皮肤与工具栏定义中工具栏按钮增删在这里配js/、css/、images/前端资源低uploadfile/ 或 upload/默认上传落地目录高权限和可执行性必须处理admin/如果打包里有后台配置管理页极高暴露在公网等于开门WEB-INF/lib/依赖 jar常见是老版本上传组件中和 JDK 版本强相关先说结论真正需要你动手改的是三个地方——外壳页的调用参数、style 目录下的工具栏配置、上传落地的目录与白名单。dialog 目录里的东西除非业务明确要求加自定义按钮否则不要碰那里面全是 DOM 操作和 document 层级的路由改一处牵动一串。另外要留意 WEB-INF/lib 下的 jar 清单。老 JSP 项目常见会带一些年代久远的上传组件和 JSON 库在 JDK 8 上还能跑JDK 11 之后可能因为内部 API 被移除直接编译失败。我一般会在解包后先jar tf扫一遍把明显过时的组件记下来后面部署时如果报 NoClassDefFoundError 或者 NoSuchMethodError第一时间往这个方向查比盲猜快得多。2.2 请求链路参数是怎么从 iframe 走到服务端的这一节是整篇的地基。搞不清楚数据怎么流动后面所有问题都是玄学。eWebEditor 这类编辑器的核心结构是「三层嵌套」最外层是你的业务页面页面里有一个iframe指向编辑器外壳页外壳页内部还有一个 iframe那个 iframe 的 document 被设置成可编辑模式老实现用designMode on新一点的用contenteditable用户实际敲字的地方是最里层。中间层负责工具栏、弹窗、按钮事件最外层负责和你页面的表单对接。关键点在于编辑器本身不提交表单。它的做法是在你点提交的瞬间用脚本把最里层 document 的 HTML 取出来写回外层页面里一个隐藏的 textarea 或 input然后由你的表单正常 POST 到服务端。控制这个回写目标的就是外壳页 URL 上的id参数——id的值必须和你页面上那个 textarea 的 name/id 对得上否则内容会写丢。所以完整链路是用户编辑 → 里层 document.innerHTML → 脚本回写到 id 指定的表单域 → 你的 form 提交 → 服务端request.getParameter(content)拿到一大串 HTML 字符串。服务端拿到的就是纯文本它不知道这是编辑器产出的也不做任何校验这一点后面讲 XSS 时还会回头说。理解了这条链路很多现象就能自己解释了。比如「编辑器里明明有内容提交后是空的」八成是 id 对不上或者回写脚本没触发「内容提交了但库里是乱码」问题在服务端取值的编码上「第二次打开编辑内容全变成源码了」是回填时没做转义。这些在第 5 章会逐条展开。提示调试这条链路最快的办法是在外层页面按 F12在 Console 里手动执行一次「取里层 document 内容」的那行脚本看能不能拿到 HTML。能拿到说明编辑器正常问题在回写或提交拿不到就往 iframe 的 src 和跨域上查。2.3 选它还是选现代编辑器三条判断线不是所有项目都该用它。我给三条线属于老工程的现实取舍第一条工程有没有构建链。没有 Webpack/Vite纯 JSP 加 script 标签的组织方式那这种「丢目录就能跑」的资源是最省事的反过来如果项目本身已经是模块化前端硬塞一个全局变量式的编辑器进来反而要写一堆适配层。第二条浏览器基线在哪。内网项目大量还在 IE11 甚至 IE8 兼容模式现代编辑器基本放弃这条路。老编辑器在这块是优势但它用的也是老 API在现代浏览器里偶尔会有选区、粘贴方面的小毛病得接受。第三条安全兜底谁来做。这类老资源在内容过滤上几乎是裸奔的服务端拿到什么就存什么。你要自己加白名单过滤、自己处理上传目录的可执行性、自己给后台页面加鉴权。愿意做这三件事它就能用得很稳不愿意做那就是给系统埋雷。3. 落到 Tomcat三步把编辑器跑起来3.1 环境准备与那个必须避开的版本分界线环境这块有一条硬线Tomcat 9 及以下。原因是这类 JSP 资源编译出来的类引用的是javax.servlet.*包而 Tomcat 10 开始整体迁移到jakarta.servlet.*包名对不上部署上去就是 ClassNotFoundException 一片。我见过有人图省事直接上了最新版 Tomcat然后花半天时间怀疑是压缩包坏了血泪经验就是先看 Tomcat 大版本。推荐组合是 JDK 8 加 Tomcat 8.5 或 9.0这两个版本的 JSP 编译器和 Servlet API 都能兼容老代码。JDK 别上 17一是模块系统会把一些反射调用挡掉二是老 jar 里如果有字节码增强或者用了内部 API启动阶段就可能抛异常。部署动作本身很简单# 1. 解压到 Tomcat 的 webapps 下目录名就是访问路径 unzip ewebeditor014jsp.rar -d /opt/tomcat/webapps/ # 假设解出来是 webapps/ewebeditor/ # 2. 上传目录先建好并给足权限不要用 777按运行用户来 mkdir -p /data/upload/news chown -R tomcat:tomcat /data/upload/news chmod 755 /data/upload/news # 3. 启动并看日志重点盯 ClassNotFound 和 JSP 编译错误 tail -f /opt/tomcat/logs/catalina.out第一段是解压-d指定目标目录解出来的文件夹名会直接变成 URL 里的上下文路径所以如果你想要/ewebeditor/这个访问前缀就别随手改名成中文或者带空格的名字。第二段建上传目录一定要放在 webapps 之外理由在第 6 章讲。第三段启动后先别开浏览器先看日志——JSP 的编译错误只在第一次访问时抛出日志里能看到具体的行号和缺失的类比在页面上看 500 白屏强太多。部署完先访问一下外壳页本身比如http://内网地址:8080/ewebeditor/ewebeditor.jsp?idcontentstylestandard。能看到工具栏就说明资源文件路径没问题如果是一片空白或者样式全丢检查一下是不是被部署到了带 context path 的子目录下而页内用了绝对路径这类老资源里写死/ewebeditor/...的地方不少改上下文名的时候要全局搜一遍。3.2 页面接入初始化参数怎么传接入本身只有几行但每一行参数都有讲究。下面是我在一个公告编辑页里的实际写法% page contentTypetext/html;charsetUTF-8 languagejava % % // 编辑场景从库里取出已有内容回填新增场景为空串 String content (String) request.getAttribute(content); if (content null) content ; % form idnoticeForm action${pageContext.request.contextPath}/notice/save methodpost accept-charsetUTF-8 !-- 隐藏域编辑器的回写目标name 必须与 iframe src 里的 id 参数一致 -- textarea namecontent idcontent styledisplay:none;% content %/textarea iframe ideditorFrame src${pageContext.request.contextPath}/ewebeditor/ewebeditor.jsp?idcontentstylestandard frameborder0 scrollingno width100% height420 styleborder:1px solid #d0d0d0;/iframe button typesubmit保存/button /form第一段是 JSP 声明编码这行必须在文件最前面contentType里的 charset 决定了页面本身的输出编码漏了就是整页乱码。第二段取回填内容注意这里是直接输出到 textarea 里编辑场景下内容里含 HTML 标签如果不对、、做转义页面结构会被内容撑坏——这一点第 5 章会给出正确的写法。第三段是核心iframe 的src里两个参数idcontent对应下面 textarea 的 namestylestandard对应 style 目录下的那套配置。style换成别的值就能切换工具栏布局业务上如果需要「简版」和「全功能」两套这是最省事的做法不用改代码。height建议留够 400 以上工具栏加编辑区在这个尺寸下才不憋屈。scrollingno是必须的否则会出现内外双滚动条体验很差。3.3 表单提交与服务端接值服务端接值这一步最容易翻车因为编码问题几乎必现WebServlet(/notice/save) public class NoticeSaveServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 必须在第一次 getParameter 之前设置否则不生效 req.setCharacterEncoding(UTF-8); String content req.getParameter(content); if (content null) content ; // 长度兜底老编辑器不做限制用户能贴进来一整篇带样式的文档 if (content.length() 200_000) { resp.sendError(400, content too large); return; } // 这里落库content 是一段完整 HTML 字符串 noticeService.save(content); resp.sendRedirect(req.getContextPath() /notice/list); } }setCharacterEncoding那一行的位置是死规定必须在任何getParameter之前调用。很多人把它写在方法中间前面已经调过一次读参数后面再设就没用了——因为参数解析在第一次访问时就已经完成编码定死了。这是我在老项目里排查乱码时最先看的一行。长度校验那一段不是可选项。这类编辑器允许直接粘贴 Word 内容粘一篇带表格和样式的文档进来HTML 能有几百 KB数据库字段如果还是varchar(4000)会直接截断或者报错。常见做法是把存储字段改成TEXT/LONGTEXT同时在入口处加长度上限。至于落库的content是什么要心里有数它是一段带p、span style、img src的完整 HTML不是一个纯文本。所以展示端不能直接innerHTML输出到页面上得先过滤这个在第 6 章展开。4. 图片上传打通目录、白名单、回写三段式4.1 上传配置项与每个参数的实际含义编辑器能敲字只是第一步图文混排的难点全在上传。这类老资源的配置一般放在配置文件或者后台管理页里键名各版本有出入但语义是固定的配置项语义典型键名建议值说明允许的扩展名allowExt / uploadExtjpg,jpeg,png,gif,webp白名单绝不写*单文件大小上限maxSize / uploadSize20971522MB与 Nginx 的 client_max_body_size 对齐上传落地物理路径uploadDir/data/upload/news放在 webapps 之外对外访问前缀urlPrefix/upload/news与 Nginx 静态映射一致是否保留原文件名keepNamefalse关掉统一用时间戳随机串图片是否压缩若有则开宽度上限 1200手机直出照片动辄 4MB这张表里最容易被忽略的是「单文件大小上限」和 Nginx 那一行。编辑器配置里写了 2MB但前端 Nginx 默认client_max_body_size是 1MB用户传一张 1.5MB 的图请求在 Nginx 层就被 413 挡掉了浏览器控制台看到的是一个莫名其妙的错误编辑器只弹一句「上传失败」。所以两边必须对齐我的习惯是 Nginx 设 4MB编辑器设 2MB留一倍余量。「保留原文件名」这一项关掉的理由不只是重名覆盖。中文文件名在跨系统传输时经常变成乱码配置里如果没有正确的编码处理落地后的文件名是一串问号图片能显示但没法管理。统一用「日期目录 时间戳 4 位随机数 扩展名」是最稳的方案。配置改完记得清一次浏览器缓存再看效果。这类老资源的前端 JS 文件名固定浏览器会把旧的 JS 缓存住你改完配置刷新页面发现没变化很可能根本不是配置没生效。4.2 服务端接收用 Servlet 3.0 的 Part 接住文件如果打包里的上传处理跑不通或者你想把它迁到统一的存储路径上自己写一个接收端最直接。Servlet 3.0 之后用Part就行不用引第三方上传组件WebServlet(/ewebeditor/upload) MultipartConfig( fileSizeThreshold 1024 * 1024, // 超过 1MB 落到临时文件 maxFileSize 2 * 1024 * 1024, // 单文件上限与配置对齐 maxRequestSize 4 * 1024 * 1024 // 整个请求上限多图场景要放宽 ) public class EditorUploadServlet extends HttpServlet { private static final SetString ALLOW new HashSet(Arrays.asList(jpg, jpeg, png, gif, webp)); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); resp.setContentType(application/json;charsetUTF-8); // 字段名要和编辑器实际提交的一致抓一次请求看 FormData 里的 key Part part req.getPart(file); if (part null || part.getSize() 0) { resp.getWriter().write({\ok\:false,\msg\:\empty file\}); return; } String origin part.getSubmittedFileName(); String ext origin.substring(origin.lastIndexOf(.) 1).toLowerCase(); if (!ALLOW.contains(ext)) { resp.getWriter().write({\ok\:false,\msg\:\ext not allowed\}); return; } // 按日期分目录避免单目录文件过多 String day new SimpleDateFormat(yyyyMMdd).format(new Date()); Path dir Paths.get(/data/upload/news, day); Files.createDirectories(dir); String name System.currentTimeMillis() _ (int) (Math.random() * 9000 1000) . ext; part.write(dir.resolve(name).toString()); String url /upload/news/ day / name; resp.getWriter().write({\ok\:true,\url\:\ url \}); } }第一段注解的三个参数要和 4.1 的配置表对上maxRequestSize尤其容易漏多图一次提交时它才是真正卡住请求的那道闸。第二段取Part时的字段名file是猜测值正确做法是在浏览器里点一次上传F12 的 Network 面板看那条请求的 FormData里面 key 是什么就填什么这一步不做就纯靠试。第三段扩展名校验用的是白名单注意getSubmittedFileName在部分容器版本上会返回带路径的完整名所以要取最后一个点之后的部分lastIndexOf不能换成indexOf。第四段用时间戳加随机数命名避免中文名和重名同时按日期分目录单目录文件数控制在几千以内否则文件系统层面ls都会变慢。第五段返回 JSON。这里的返回格式必须和编辑器前端的解析逻辑对齐——老版本可能要求的不是 JSON 而是特定分隔符的纯文本。判断办法只有一个改之前先让原版上传成功一次抓包看它返回什么照着那个格式写。4.3 回写与展示路径前缀那一层别写死上传成功后返回的 URL 会被编辑器写进img src...。这里有个很隐蔽的问题如果你返回的是相对路径而文章最终展示在另一个层级的页面上图片就 404。我的做法是统一返回以/开头的根相对路径然后在 Nginx 层做映射# Nginx 静态资源映射与 Java 里的 urlPrefix 保持一致 location /upload/news/ { alias /data/upload/news/; expires 30d; add_header Cache-Control public; # 关键禁止该目录下的脚本被解析执行 location ~* \.(jsp|jspx|java|class)$ { deny all; } }alias和root的区别要分清alias是直接替换匹配到的前缀root是拼接完整路径。这里用alias访问/upload/news/20240101/a.jpg会落到/data/upload/news/20240101/a.jpg。expires 30d让图片走浏览器缓存编辑器里反复预览同一张图时不会每次都回源。里面那个嵌套的location是安全兜底虽然文件是服务端写进去的、理论上不会有脚本但万一上传白名单被人绕过这行能挡住执行。这个思路放到 Tomcat 里同样适用第 6 章给具体做法。5. 避坑与排查五个翻车现场5.1 现象、原因、解决一条条对第一条编辑器里显示正常提交后打开详情页是乱码。现象是中文变成问号或者「锟斤拷」。原因通常有两层一是 JSP 页面顶部漏了contentTypetext/html;charsetUTF-8二是服务端setCharacterEncoding调用位置在getParameter之后。还有一种容易被忽略的情况数据库连接串里没带useUnicodetruecharacterEncodingutf8前面两步都对存进去还是坏的。解决顺序是先看页面编码、再看取参位置、最后查连接串三层逐个排除别一上来就改数据库。第二条点上传图片进度条走完弹「上传失败」服务端日志什么都没有。日志为空说明请求根本没到应用。先看 Nginx 的 access log 和 error log如果是 413就是client_max_body_size太小如果是 404就是你配的上传 Servlet 路径和编辑器里写死的提交地址对不上。这类老资源的提交地址常常硬编码在 JS 里改上下文路径时要全局搜一遍。还有一种极端情况是路径里带了中文导致 Nginx 解码异常把目录名换成纯英文即可。第三条上传成功编辑器里图片能显示发布后详情页图片全部裂开。这就是 4.3 说的问题。原因是你返回的是相对路径编辑器所在页面和详情页不在同一层级。也可能是详情页部署在不同的 context path 下相对路径解析出来的地址完全不对。解决办法是返回根相对路径以/开头或者干脆在配置里写死完整域名加协议头。前者更灵活但要求 Nginx 的映射路径前后一致。第四条编辑已有内容时整篇文章变成了一堆 HTML 源码。现象是打开编辑页编辑区里显示的是pxxx/p这样的字面文本而不是排版后的效果。原因是回填时把 HTML 直接塞进了 textarea编辑器初始化时按纯文本读取了。正确做法是初始化后通过编辑器提供的设置内容的入口写进去或者在最外层页面里对内容做一次 HTML 实体转义再输出到 textarea 里让编辑器自己解码。两条路选一条别混用。第五条列表页多条记录打开第二条编辑内容串成了第一条的。这是id冲突。编辑器回写时按id找表单域如果列表页每行都渲染了一个编辑器而id全是content脚本会找到第一个匹配的内容自然全写进第一行了。解决办法是给每行生成唯一的id比如content_${record.id}iframe 的id参数同步改。这个坑在批量编辑场景下几乎必现早点按唯一 id 来设计能省很多返工。5.2 一套固定的排查顺序出问题时的排查顺序比技术点本身更重要。我的一般流程是四步从外往里收第一步浏览器侧。F12 打开 Network看请求发没发出去、状态码是多少、请求体里的 FormData 长什么样、响应体返回的是什么。这一步能定位掉一半的问题尤其是上传类故障很多时候编辑器只是把服务端的错误原样弹出来具体原因在响应体里写着。第二步应用日志。catalina.out里搜异常堆栈重点看ClassNotFoundException依赖 jar 没带全或者 JDK 版本不匹配、NoSuchMethodErrorjar 冲突、FileNotFoundException上传目录没建或者权限不对。JSP 的编译错误只在首次访问抛出所以排查前先清一次 Tomcat 的 work 目录让它重新编译。第三步配置一致性。把编辑器配置里的路径、大小、扩展名和 Nginx 配置、Java 代码里的常量、数据库字段长度全部拉平对一遍。老项目的配置散落在四五个地方任何一处不一致都会表现为「玄学」问题。第四步权限与安全策略。上传目录的属主、SELinux 状态、容器里挂载的只读文件系统这些都会让写入静默失败。part.write()在某些容器实现里失败是不抛异常的返回码还是 200只是文件没落盘这种最难查。注意清 work 目录这个动作要养成习惯。JSP 源文件改过但页面行为没变化八成是容器还在用旧的编译结果尤其是你只改了 JSP 里的一行 Java 代码时。6. 收口把它塞进鉴权链路并做输出过滤6.1 上传目录的三道闸第 4 章把上传跑通了但生产环境还要补三件事。第一上传目录不能在 webapps 里必须在容器外面通过 Nginx 或 Tomcat 的虚拟目录映射出去理由是 webapps 下的文件默认由容器负责解析一个伪装成 jpg 的 jsp 一旦落进去就可能被执行。第二如果受限于部署条件必须放在 webapps 下那就在web.xml里加一段安全约束禁止该目录下任何脚本扩展名被访问security-constraint web-resource-collection web-resource-nameupload-deny-script/web-resource-name url-pattern/ewebeditor/uploadfile/*/url-pattern http-methodGET/http-method http-methodPOST/http-method /web-resource-collection auth-constraint/ /security-constraintauth-constraint/留空表示谁都不许访问配合 URL 模式把脚本类扩展名一起挡掉。代价是这个目录下的图片也不能直接访问了所以更推荐第一种方案。第三上传接口本身要校验登录态和权限。老资源的默认实现常常是匿名可传任何知道路径的人都能往你服务器上塞文件。在 Servlet 的doPost开头加一段会话校验检查当前用户是否有发稿权限没有就返回 403这行代码比什么防护都实在。6.2 输出侧的 HTML 白名单内容存进去是一段 HTML展示时直接输出就是标准的存储型 XSS。过滤放在写入前还是展示前我的习惯是两边都做写入前用 jsoup 做一次白名单清洗展示前再做一次转义兜底。public static String clean(String html) { // 注意jsoup 新版本把 Whitelist 改名为 Safelist按实际版本调整 Whitelist wl Whitelist.basicWithImages(); wl.addTags(table, tbody, tr, td, th, p, div, span, h2, h3); wl.addAttributes(table, border, cellpadding, cellspacing, width); wl.addAttributes(td, colspan, rowspan, width); wl.addAttributes(p, style); wl.addProtocols(img, src, http, https); // 不放行 data: 和 javascript: return Jsoup.clean(html, wl); }basicWithImages已经包含常用标签和img在此基础上加表格相关的标签和属性是因为公告类内容经常需要排版表格。addProtocols那一行是重点img的src里塞javascript:或者data:text/html是常见的绕过手法把协议限定在 http/https 能挡掉绝大部分。样式属性只对p放开是有意的style里的expression()在老 IE 上能执行脚本如果业务确实需要富样式得对 style 内容再做一次正则校验。过滤之后要跑一轮回归粘一段带表格的 Word 内容、插入一张图、加粗标题、插入超链接看清洗后排版有没有被破坏。白名单太严会把正常样式吃掉太松又挡不住攻击这个平衡点只能靠实际内容样本试出来。我一般会攒十几篇真实公告做样本每次调整白名单都过一遍。验收项通过标准编辑回填有内容的文章打开后是排版效果不是源码图片上传2MB 内图片成功超过上限有明确提示图片展示详情页和列表页缩略图都能正常显示匿名上传未登录直接 POST 上传接口返回 403XSS 样本script、onerror、javascript:三类样本入库后被清除后台页面admin 目录不可匿名访问默认口令已改最后一行的默认口令是我每次接手这类老资源第一件要做的事。从那次在一个内网系统里翻出可以直接改站点配置的后台入口之后我每回部署完都会先跑一遍上面这张表尤其是上传和后台这两项宁可多花二十分钟也不想半夜被叫起来处理问题。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑