2026/10/25 23:22:52

JSP富文本编辑器ewebeditor拆解、部署与安全改造

JSP富文本编辑器ewebeditor拆解、部署与安全改造 简介eWebEditor在线文本编辑器 v0.1.4 For JSP 是面向 Java Web 开发者的富文本编辑组件适用于在 JSP 页面中集成可视化内容编辑、图片上传、超链接插入与 HTML 源码编辑等功能的项目场景尤其适合中小型后台管理系统与内容发布模块的搭建。压缩包共 264 个文件、约 796KB包含 186 个 gif 界面素材、27 个 css 样式、21 个 htm 示例页、11 个 class 编译类、6 个 jpg 图片、4 个 jsp 集成页、4 个 js 脚本以及 xml 配置、txt 说明和 jar 依赖基本覆盖前端资源、服务端调用与部署示例。已有 173 人学习下载可作为了解该编辑器目录结构和吕海鹏修改版改动的参考。读者可从中获得一套可直接部署到 JSP 工程的编辑器文件集合借助示例页与样式资源理解按钮布局、上传处理和接口调用方式并据 jar 与 class 文件排查依赖与兼容性问题为后续二次开发和功能定制提供基础。1. 一个 2005 年前后的 JSP 富文本编辑器今天拆它还有用前几天有个朋友丢过来一个压缩包文件名是 ewebeditor014jsp说后台的文章发布页要一个能直接排版的输入框最好别引 npm 那一套。我解压一看这就是当年装机量极大的那款在线文本编辑器v0.1.4 的 JSP 版本纯 JSP 加 JavaScript往 Tomcat 里一扔就能跑工具栏上的加粗、字号、插入图片、表格、源码切换一应俱全图片还能直接传到服务器目录。对还在维护老 Java Web 后台的人来说它是能直接救火的轮子对想搞明白「所见即所得编辑区到底怎么实现的」的人来说它没有构建步骤、没有依赖树出问题就集中在几个 JSP 和配置项上黑匣子程度很低是极好的解剖样本。2. 技术栈与目录结构iframe 编辑区是怎么搭起来的这个版本的技术选型放在今天看非常朴素服务端只有 JSP 和 Servlet 容器没有 Spring没有连接池框架甚至不要求数据库客户端是原生 JavaScript不用 jQuery不用模块打包。它的全部能力都建立在浏览器原生提供的一个能力上——iframe里的designMode。理解这一点后面所有看起来神奇的按钮就都能对上了。2.1 运行时依赖比想象中还要少把一个老 Java Web 项目跑起来最怕的是依赖链断在半路。这款编辑器的幸运之处在于它的依赖面窄得可怜整个运行时就三层层次依赖说明运行环境JDK 1.6 及以上老版本用commons-fileupload走 multipartJDK 版本不宜过低Web 容器Tomcat 6/7/8支持 JSP 2.x 的容器都行Servlet 3.0 之后上传可走原生 API第三方包commons-fileupload、commons-io只在图片/文件上传的 JSP 里用到缺失会直接 500它不需要数据库编辑器本身不存内容只负责把 HTML 片段交回给宿主页面落库是宿主业务的事。这一点很关键很多新手以为解压完要配数据源其实完全没有。真要接数据库是你在自己的发布页里接编辑器只吐出字符串。提示如果你的容器已经是 Tomcat 9 以上commons-fileupload依然能用但更推荐直接用request.getParts()少一个 jar 少一次踩坑。2.2 目录结构三个 JSP 加一份配置的分工解压后目录一般长这样名字可能略有出入但职责划分基本固定ewebeditor/ ├── ewebeditor.jsp # 编辑器外壳页iframe 加载的就是它 ├── ewebeditor.js # 工具栏、命令分发、取值赋值的核心脚本 ├── config.jsp # 样式、工具栏按钮、上传路径的配置入口 ├── upload.jsp # 图片/文件上传的处理端 ├── browse.jsp # 服务器端图片浏览与选择 ├── style/ # 各套皮肤每套一套 css 和图标 ├── uploadfile/ # 默认上传落地目录需要写权限 └── dialog/ # 插入表格、插入图片等弹窗页面这个分法很直白ewebeditor.jsp当壳负责渲染工具栏和那个可编辑的 iframeewebeditor.js是大脑所有按钮点击最后都归到execCommandconfig.jsp是配置中心改工具栏、改皮肤、改上传目录都在这里upload.jsp是唯一碰磁盘的地方。定位问题的时候按这条线走基本不会迷路。2.3 编辑区实现designMode 加 execCommand 的老套路真正让文字能加粗、能变色的不是编辑器自己写的算法而是浏览器内置的富文本编辑能力。它的初始化核心就几行// ewebeditor.jsp 内联脚本的典型初始化流程 window.onload function () { var doc document.getElementById(editorFrame).contentWindow.document; // 打开可编辑模式浏览器会接管光标、选区、撤销栈 doc.designMode on; // 塞入初始内容宿主页面传进来的值在这里落地 doc.body.innerHTML window.parent.document.getElementById(content1).value || ; // 统一基础样式避免粘贴进来的内容把编辑区撑变形 doc.body.style.fontSize 14px; doc.body.style.margin 8px; };designMode on这行是整个编辑器的地基。打开之后浏览器会自动维护一个可编辑文档模型光标、选区、回车换行、撤销重做都由它负责。工具栏上那些按钮做的事情就非常简单了// 工具栏按钮的统一入口cmd 是 execCommand 的命令名 function exec(cmd, param) { var win document.getElementById(editorFrame).contentWindow; win.focus(); // 必须先把焦点还给 iframe否则命令丢空 win.document.execCommand(cmd, false, param || null); syncToTextarea(); // 每次操作后回写防止提交时取到旧值 }execCommand的第二个参数是 UI 参数老代码里几乎永远是false让浏览器不要弹自己的对话框第三个参数随命令变化bold不需要参数fontsize传4forecolor传#ff0000。这套 API 现在已经被标记为废弃但主流浏览器仍在兼容这也正是这个老项目至今还能跑的原因。取值的方向反过来把 iframe 里的 HTML 抠出来塞进隐藏域function syncToTextarea() { var doc document.getElementById(editorFrame).contentWindow.document; // 取 body.innerHTML 而不是 documentElement避免带出多余的 html/body 标签 document.getElementById(content1).value doc.body.innerHTML; }参数说明content1是宿主页面上隐藏 textarea 的 id也是整个链路里唯一需要你和业务对齐的名字。表单提交时浏览器只看这个 textarea编辑器本身不参与提交所以只要保证提交前调用过一次同步数据就不会丢。3. 跑起来Tomcat 部署与最小可运行配置环境对了这个项目跑起来只需要几分钟环境不对会一直在 404 和 500 之间打转。这一章按「先跑通空白页再接通上传最后嵌进业务表单」的顺序走每一步都有可验证的结果。3.1 环境准备与部署步骤准备 JDK 1.6 和一个 Tomcat版本不必追新Tomcat 7 或 8 最稳因为老 JSP 里可能有对javax.servlet老包名的硬引用。部署动作很机械# 1. 解压后把整个 ewebeditor 目录丢进容器 unzip ewebeditor014jsp.rar -d /tmp/eweb cp -r /tmp/eweb/ewebeditor $CATALINA_HOME/webapps/ # 2. 上传目录必须可写Linux 下这一步最容易漏 chmod -R 755 $CATALINA_HOME/webapps/ewebeditor/uploadfile chown -R tomcat:tomcat $CATALINA_HOME/webapps/ewebeditor/uploadfile # 3. 启动后先访问编辑器自身的演示页 # http://localhost:8080/ewebeditor/ewebeditor.jsp?idcontent1第三步是关键的验证点直接打开ewebeditor.jsp能看到工具栏和一块可以打字的区域说明 JSP 编译通过、JS 加载正常、皮肤路径没写错。如果页面白屏但状态码是 200八成是皮肤目录没跟着复制或者config.jsp里的样式名和style/下的目录名对不上。如果直接 404检查webapps下是不是多套了一层目录。3.2 配置文件上传路径和工具栏裁剪config.jsp是这个项目里最值得先读一遍的文件业务适配基本都在这里改。核心几项含义如下配置项典型值作用stylecoolblue/standard皮肤名对应style/下的目录uploadDir/uploadfile/上传落地目录相对 Web 根allowExtjpg,jpeg,png,gif上传白名单老版本常常写得很宽松maxSize2048单文件大小上限单位 KBtoolbar按钮 id 串决定哪些按钮出现在工具栏上改工具栏是最常见的诉求比如业务只需要加粗、斜体、插入链接和图片其余全砍掉减少运营误操作%-- config.jsp 中裁剪工具栏的常见写法数组里是要保留的按钮 id --% % String[] toolbar new String[]{ bold, italic, underline, // 基础格式 forecolor, backcolor, // 颜色 link, unlink, img, // 链接与图片 source // 源码切换排障时很有用 }; request.setAttribute(toolbarItems, toolbar); %参数说明按钮 id 必须和ewebeditor.js里注册的命名一致拼错了不会报错只是那个按钮不显示这是排查时最容易浪费时间的点。建议改完先在源码视图里搜一遍 id确认脚本里存在同名注册。allowExt一定要收紧。老版本默认白名单里经常混着html、swf之类允许上传 HTML 等于给自己开了一个存储型 XSS 的口子后面避坑章节会细说。3.3 嵌进业务表单三个改动点编辑器真正投入使用是在你自己的发布页里嵌一个 iframe 指向它再把内容同步到一个隐藏域。最小集成代码如下% page contentTypetext/html;charsetUTF-8 % form idpostForm action/admin/save.jsp methodpost input typetext nametitle placeholder标题 / %-- 隐藏域真正参与提交的字段 --% textarea idcontent1 namecontent styledisplay:none/textarea %-- 编辑器外壳id 参数要和上面的 textarea 对应 --% iframe ideditorFrame src/ewebeditor/ewebeditor.jsp?idcontent1stylestandard stylewidth:100%;height:420px;border:1px solid #ccc/iframe button typebutton onclicksubmitPost()发布/button /form script function submitPost() { // 提交前强制同步一次这是最容易忘、也是丢内容最常见的原因 var doc document.getElementById(editorFrame).contentWindow.document; document.getElementById(content1).value doc.body.innerHTML; document.getElementById(postForm).submit(); } /script三个改动点分别是隐藏域的名字要和后端接收参数一致iframe 的id查询参数要和 textarea 的 id 一致提交前必须手动同步一次。第三点尤其重要因为designMode的内容不会自动流回表单只要漏掉这一步用户写多少丢多少而且不报错。注意iframe的src里带在 JSP 中建议写成amp;或放进变量里拼接某些容器对属性解析严格时会截断参数导致皮肤或 id 丢失。4. 核心链路拆解一次贴图从点击到落库编辑器日常最高频的两个动作是「插图片」和「提交内容」前者跨了浏览器与服务端后者决定了数据安不安全。把这两条链路拆开看整个项目的骨架就清楚了。4.1 上传接口multipart 请求怎么落到磁盘点击工具栏的图片按钮弹窗里选完文件表单会以 multipart 形式 POST 到upload.jsp。老版本用commons-fileupload解析处理逻辑大致如下% page importorg.apache.commons.fileupload.*,org.apache.commons.fileupload.disk.*,java.util.*,java.io.* % % // 只允许 POST避免 GET 被当成上传入口 if (!POST.equalsIgnoreCase(request.getMethod())) { response.sendError(405); return; } FileItemFactory factory new DiskFileItemFactory(); ServletFileUpload upload new ServletFileUpload(factory); upload.setSizeMax(2 * 1024 * 1024); // 与 config 中的 maxSize 保持一致 String saveDir application.getRealPath(/uploadfile/); new File(saveDir).mkdirs(); // 目录不存在就建避免首次上传 500 ListFileItem items upload.parseRequest(request); for (FileItem item : items) { if (item.isFormField()) continue; // 跳过普通字段只处理文件 String name new File(item.getName()).getName(); // 关键去掉客户端带过来的路径 String ext name.substring(name.lastIndexOf(.) 1).toLowerCase(); // 扩展名白名单别信前端 accept 属性 if (!Arrays.asList(jpg, jpeg, png, gif).contains(ext)) { out.print(scriptalert(不支持的格式);history.back();/script); return; } // 重命名避免中文名、重名、路径穿越三类问题 String newName System.currentTimeMillis() . ext; item.write(new File(saveDir, newName)); // 回调父窗口把 URL 插进编辑区 String url request.getContextPath() /uploadfile/ newName; out.print(scriptparent.insertImage( url );/script); } %逐段说明setSizeMax是服务端硬限制和配置页里的maxSize是两道闸改一处不改另一处就会出现「提示超限但仍传上去了」的怪现象。new File(item.getName()).getName()这一行是血泪经验早期浏览器会把完整本地路径一起提交不去掉会在服务器上建出诡异的目录。重命名用时间戳虽然原始但能一次性解决中文乱码、同名覆盖和../路径穿越三个问题比自己写校验省事。4.2 内容入库富文本的过滤比想象中麻烦编辑器交回来的是一段 HTML直接存库没问题直接回显就危险了。老项目最常见的翻车方式就是「后台编辑们互相插脚本」。落库前的处理建议分两步先做标签白名单再入库// 保存接口里的过滤片段思路是白名单放行 属性剥离 private static final SetString ALLOW new HashSet(Arrays.asList( p, br, strong, b, em, i, u, span, a, img, table, tr, td, ul, ol, li)); public static String clean(String html) { if (html null) return ; // 先干掉脚本、样式、iframe 整段内容避免残留 html html.replaceAll((?is)(script|style|iframe)[^]*.*?/\\1, ); // 再去掉所有 on 开头的事件属性和 javascript: 协议 html html.replaceAll((?i)\\son\\w\\s*\\s*(\[^\]*\|[^]*|[^\\s]), ); html html.replaceAll((?i)javascript\\s*:, ); // 最后按白名单剥离不认识的标签保留内部文本 html html.replaceAll((?is)/?([a-z0-9])[^]*, m - { String tag m.group(1).toLowerCase(); return ALLOW.contains(tag) ? m.group() : ; }); return html; }说明正则做 HTML 清洗永远不是最优解成熟做法是引jsoup的Safelist一行Jsoup.clean(html, Safelist.basicWithImages())就够。上面这段适合不想加依赖的老项目应急。a标签的href还要单独校验协议只放行http、https、mailto否则javascript:变个体就能绕过去。入库用预编译语句别拼字符串String sql INSERT INTO article(title, content, create_time) VALUES(?,?,?); try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, title); ps.setString(2, clean(content)); // 过滤后的 HTML 入库 ps.setTimestamp(3, new Timestamp(System.currentTimeMillis())); ps.executeUpdate(); }参数说明存的是过滤后的 HTML 原文不做 HTML 转义。转义是「回显时」的事——如果前端用innerHTML渲染就不转义用文本节点渲染才转义这一点搞反了要么样式全丢要么直接 XSS。4.3 回显与取值编辑已有文章时的两个坑编辑旧文章时要把库里的 HTML 塞回编辑区。常见写法是在 iframe 加载完成后赋值但顺序错了就会白屏// 正确顺序等 iframe 内部文档就绪再写 innerHTML document.getElementById(editorFrame).onload function () { var doc this.contentWindow.document; doc.designMode on; // 用 textarea 里预置的旧值回填注意不要用 innerHTML 做二次解析 doc.body.innerHTML document.getElementById(content1).value; };两个坑一是designMode必须在写innerHTML之前或之后立刻设置顺序颠倒会让光标无法定位二是旧内容里如果有script赋值时就会执行所以回显前一定要过一次 4.2 的过滤读和写走同一个清洗函数别只在保存时过滤。5. 避坑与排查老项目上线最容易翻车的五个点这类项目的坑有一个共同特征不报错只是功能静默失效。下面五条都是实际部署里反复出现的按「现象 → 原因 → 解决」记下来能省很多时间。现象一编辑区能打字但提交后内容是空的。原因提交前没有把 iframe 里的 HTML 同步回隐藏域表单提交的永远是空 textarea。 解决在提交按钮的处理函数里强制同步一次或者给表单绑onsubmit事件统一处理别指望编辑器自己回写。现象二上传图片提示成功但编辑区里是个裂图。原因upload.jsp回调里拼的 URL 少了上下文路径或者上传目录落在了 Web 根之外。 解决统一用request.getContextPath()拼前缀并在浏览器地址栏直接访问那张图确认可访问。运维把uploadfile挪到独立存储时这一处必须同步改。现象三上传大一点的图就 500日志里是SizeLimitExceededException。原因容器或commons-fileupload的大小限制小于配置页里写的maxSize。 解决三处对齐——config.jsp的maxSize、upload.jsp的setSizeMax、容器的maxPostSize。只改一处等于没改。现象四中文文件名上传后变成乱码或者图片存进去但打不开。原因请求编码没设commons-fileupload按平台默认编码解析文件名。 解决解析前upload.setHeaderEncoding(UTF-8)并且在文件落地时统一重命名成英文加时间戳从根上绕开编码问题。现象五改完config.jsp工具栏没变化清缓存也没用。原因JSP 编译产物缓存或者浏览器缓存了ewebeditor.js这个带版本号的静态资源。 解决先删容器work目录下的编译缓存重启再确认页面引用 JS 时带了?v时间戳。老项目里这类玄学问题九成是缓存。6. 二次开发把它改成能接现代后端的形态这份代码最值钱的地方不是功能而是它的结构足够松松到你可以只留前端那层壳把后端整个换掉。我的建议是分层改造别想着一次性重写。第一步把upload.jsp换成独立的文件服务接口。编辑器弹窗里上传地址是可配的改成一个返回 JSON 的接口即可返回体里给{url: ..., width: ..., height: ...}前端回调改两行就能接上。这样图片可以进对象存储不再依赖本地磁盘写权限。第二步把内容清洗从「保存时过滤」前移到「保存和回显都过滤」并换成jsoup白名单。这一步做完后台编辑插脚本的问题基本就绝迹了。第三步如果业务要求导出 Word 或做内容审核可以在保存接口后面挂一个异步任务把 HTML 转成结构化数据。编辑器本身不用动它只负责产出 HTML。验证改造是否成功按这张表逐项过一遍就行验证项操作通过标准基础编辑加粗、改色、插表格内容正确撤销可用图片链路上传 png 与超限文件正常图可插入超限有明确提示内容安全粘贴一段带onerror的 HTML保存后事件属性被剥离回显一致保存后重新打开编辑样式与首次编辑一致无脚本执行并发覆盖两个标签页同时编辑同一篇后提交者不静默覆盖有提示最后一条是我自己加的。早年改一个类似的老编辑器时两个运营同时开同一篇文章后保存的把前面的覆盖了追了两天才定位到是回显时没带版本号。从那以后我每次接这类「无状态编辑器 表单提交」的活都强制在隐藏域里塞一个内容版本号提交时比对不一致就拦住。这个习惯救过我不止一次希望你也能少走这一趟。本文还有配套的精品资源点击获取