尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

CKEditor解析Word样式:从粘贴乱码到金融站群内容安全清洗方案

发布时间:2026/9/26 8:00:32

资讯中心
01
ARTICLE

CKEditor解析Word样式:从粘贴乱码到金融站群内容安全清洗方案

CKEditor解析Word样式:从粘贴乱码到金融站群内容安全清洗方案
很多做金融内容平台的同学应该都有过这种经历运营从 Word 里写好了行业分析、产品公告复制粘贴进后台的 CKEditor 富文本编辑器一点保存出来的页面要么字体忽大忽小要么表格挤成一团极端情况下整篇内容直接变成一堆乱码标签。这个问题在“金融站群”这类多站点、多运营角色、强合规要求的场景里尤其头疼因为同样一份 Word 内容要分发到十几个子站每个站的模板样式还不一样稍有不慎就是线上事故。这篇内容就围绕 CKEditor 到底是怎么解析 Word 样式的、解析过程中哪些环节容易翻车、以及我在实际项目中总结出来的一套“前端拦截 服务端清洗 展示端兜底”方案展开。适合正在做内容中台、金融资讯类站点、或者任何需要处理大量 Word 粘贴内容的 B 端后台的读者参考。1. 内容整体设计与思路拆解1.1 Word 内容进编辑器到底发生了什么先明确一个容易被忽略的事实用户从 Word 复制的不是纯文本也不是独立图片而是一段被 Word 私有格式包裹的 HTMLOffice 的 Clipboard 格式之一。浏览器拿到这段内容后会同时提供 text/html 和 text/plain 两种数据。CKEditor 默认行为是取 text/html但这一步拿到的 HTML 里充满了mso-前缀的内联样式、o:p这类无意义标签、还有一堆style块定义。我第一次用开发者工具看这段粘贴数据时直接头皮发麻——一个三行两列的表格生成的 HTML 有上千行里面全是mso-ascii-font-family、mso-hansi-theme-font这类私有样式。如果什么都不做直接入库前端展示时字体、间距、对齐全都会被这些残留样式干扰而且每次从数据库读出来重新编辑内容还会继续膨胀。所以“解析 Word 样式”这件事本质不是把 Word 的样式全部保留而是把 Word 的排版意图翻译成 Web 端可控的样式语言同时把不安全、不兼容、冗余的东西全部丢掉。1.2 为什么偏偏是 CKEditor 来承担这个职责CKEditor 在金融站群这类场景里被广泛使用不是因为它功能最全而是因为它的过滤机制是开放且可编程的。很多国产富文本编辑器对粘贴内容的态度是“尽量原样保留”这短期看省事长期看就是给后面埋雷。CKEditor 的思路完全不同它内置了 ACFAdvanced Content Filter高级内容过滤机制允许你定义“哪些标签、哪些属性、哪些 class、哪些 style 允许存在”。凡是没在白名单里的内容在粘贴进入编辑器的那一刻就会被剥掉。这条特性对金融合规场景是致命的加分项。金融站群每天要发布的内容涉及到产品利率、净值、交易规则稍有不慎插入一段恶意脚本直接关系到用户资金安全。CKEditor 能做到“内容进编辑器的那一步就开始洗”而不是等提交到服务端再被动防御。另外CKEditor 的 paste 事件是暴露给开发者的。你可以在这个环节拿到原始的 clipboardData然后决定是清洗、转换还是丢弃。这种可插拔的设计让“Word 样式解析”从一句口号变成了可以落地的技术决策。1.3 解析方案选型背后的三个关键决策我在做金融站群内容平台时对 Word 粘贴解析做了三个核心决策这里展开讲讲理由。第一个决策前端轻清洗服务端重清洗。前端清洗的目的是让编辑体验接近 Word 原稿减少运营的心智负担——表格看起来正常、标题层级清楚、图片能显示出来。但前端清洗永远不能作为安全边界因为攻击者完全可以直接构造 HTTP 请求绕过编辑器提交恶意内容。所以服务端必须再做一次白名单校验清洗维度要按“标签 属性 协议”三个层级收紧。第二个决策图片转存而不是引用本地路径。Word 粘贴时图片通常是 base64 格式嵌在 HTML 里的一个 2MB 的截图 base64 化之后能膨胀到接近 3MB。如果站群有十几个分站共享一套数据库这种体积是灾难。所以我把粘贴的图片统一抽出来上传到对象存储返回 CDN 地址替换回内容。这个方案还能顺带解决图片防盗链和图片格式统一的问题。第三个决策所有样式尽量内联化。很多后端同学喜欢在输出端挂一个独立 CSS 文件来限定页面样式但金融站群的分站模板差异很大有的模板基于 Bootstrap有的基于自研 UI 库你用全局样式去压制 Word 残留样式很容易被模板自身的样式优先级覆盖掉。所以我把 Word 的排版意图在清洗阶段就转成内联样式保证后端渲染时不会因为外部样式表顺序不同而错乱。2. 核心细节解析与实操要点2.1 Word 样式在 HTML 里的“三种形态”要把解析做到位必须先认识 Word 粘贴出来的 HTML 里到底有哪几类东西。我把它分成三种形态分别对应不同的处理策略。第一类是行内样式。比如p styletext-align:center; font-size:14.0pt; font-family:宋体。这类样式直接挂在标签上优先级最高也是导致页面错乱的主要元凶。处理思路是不能直接删因为删了可能连排版意图都丢了也不能全保留因为mso-前缀的属性浏览器根本不认还会污染 HTML。正确做法是提取其中的 Web 可用样式如 text-align、font-size、color、margin把mso-*属性和 Word 私有的度量单位如pt在某些场景下转px处理掉。第二类是内嵌样式表。Word 有时会在粘贴内容的头部塞一段style定义一批类似.MsoChpDefault、p.MsoNormal的类。这些类名只在 Word 内部有意义到了浏览器环境就成了空壳。我的策略简单粗暴直接把style块整体剥离然后在清洗阶段把真正影响排版的规则比如默认字体、默认行距转成 CKEditor 编辑区的基础样式。第三类是无意义标签。o:p、v:shapetype、w:xxx这些命名空间声明下的标签既不能渲染也没有语义唯一的归宿就是删除。很多新手在这儿容易漏因为这些标签嵌套很深正则一把梭容易误伤正文。后面我会讲一个更稳妥的解析方式。2.2 为什么金融场景必须做白名单清洗而非黑名单过滤黑名单的思路是“我知道哪些东西坏我把它们过滤掉”。白名单的思路是“我只允许我知道的东西存在其余一律删除”。金融站群的富文本输入必须走白名单路线。原因有两点。第一黑名单永远是不完整的。攻击者有太多绕过的姿势大小写混写、属性名加空格、事件处理器藏在 SVG 标签里、用data:URI 协议嵌入脚本。你今天屏蔽了onerror明天可能就来一个onpointerrawupdate。与其疲于奔命地堵漏洞不如直接声明“老子只认 P、DIV、TABLE、SPAN、IMG 等二十个标签其他一律剥掉”。第二金融内容对准确性和可审计性要求极高。白名单清洗的结果是可预期的——什么样的标签进来经过清洗后一定变成什么样。黑名单则意味着输出的内容是不可枚举的你做不了代码审计也无法向合规团队证明“这里一定没有脚本”。实际操作中白名单清洗需要三条规则并行。标签层白名单确定哪些标签能过属性层白名单确定每个标签上能带哪些属性协议层白名单确定 href/src 属性值里的协议只能以 http、https、mailto、tel 开头。这三条缺一条都不完整。2.3 解析引擎的选择正则不如 DOMParser很多人的第一反应是用正则去替换style块、删除无用标签。这在小规模场景里勉强能跑但遇到 Word 产生的嵌套混乱的 HTML 时正则的局限性暴露得淋漓尽致HTML 不是正则语言复杂嵌套结构容易误匹配而且一旦用户提交的 HTML 本身有轻微不闭合的问题正则处理完的产物可能更加畸形。我的做法是完全基于浏览器/Node 的 DOM 解析器DOMParser把 HTML 转成文档树再通过遍历节点的方式逐层清洗。只有把 HTML 转成树形结构“哪些节点是冗余的、哪些属性是无效的”这个问题才变得可判定。CKEditor 本身也是这么做的它的htmlParser模块就是一个专门跑在编辑器内部的节点级解析器Payload 里的每个节点都会经过 filter 回调你可以拿到 node.name、node.attributes、node.children 做精细化处理。顺带提一个经验解析和转换两步必须分开。第一步先“识别”第二步再“改写”。不要在一棵树上既删节点又改属性否则调试时你根本分不清是这个节点的子节点被删了还是自己的属性被改了。把清洗函数拆成纯函数输入一个节点返回一个节点上层做递归遍历代码清晰度和可维护性能提升一个档次。2.4 样式归一化把 Word 的“意图”翻译成 Web 的“规则”Word 的排版逻辑是基于“节 - 段落 - 字符”这三级模型而 Web 的排版逻辑是基于“块级元素 - 行内元素 - 盒模型”。同一个排版意图两者表达方式完全不同。比如 Word 里的“多级标题”本质是段落样式里选择了“标题 1”“标题 2”但没有对应的标签结构粘贴到 Web 上以后只是一堆带着不同字号和加粗的p标签语义完全丢失。所以解析 Word 样式时我一定要做“语义还原”。我的规则是检测到段落里应用了 Heading 1 样式的通常特征是mso-outline-level:1或类名MsoToc1统一转成h2金融站群模板决定一级标题用 h2 展示。检测到段落里应用了 List Paragraph 样式且带编号的提取 Word 里的编号信息转成olli没有显式编号的转成ulli。检测到居中且字号特异性明显的段落转成p加内联样式而不是塞一个center标签——center标签早就废弃了。表格结构处理也有讲究。Word 表格的列宽依赖w:tblW和每个单元格的w:tcW来表达单位是 dxa。这个单位在 Web 端不存在必须换算成像素。换算规则很简单1 英寸 1440 dxa 96 px所以 1 dxa 96/1440 ≈ 0.0667 px。直接乘完取整就行。还有一个坑Word 表格里经常有空单元格td/tdCKEditor 默认对空 td 会显得表格高度塌陷需要给 td 一个最小高度或者插入一个零宽空格占位。3. 实操过程与核心环节实现3.1 前端第一道防线paste 事件里的拦截逻辑在 CKEditor 4 里我习惯直接监听paste事件在事件对象上取evt.data.dataValue这是 CKEditor 处理过的 HTML 字符串然后交给格式化函数。CKEditor 5 的路线不同它建议用editor.plugins.get(ClipboardPipeline)的contentInsertion事件并且要注册一个自定义的viewDocument的 downcast 转换器来干预插入流。这里分享一套 CKEditor 4 的简洁示例因为存量系统中 4 的使用率仍然很高CKEDITOR.on(instanceCreated, function (e) { e.editor.on(paste, function (evt) { var html evt.data.dataValue; var cleaned cleanWordHtml(html); evt.data.dataValue cleaned; }); }); function cleanWordHtml(input) { // 1. 剥离所有 style 块 input input.replace(/style[\s\S]*?\/style/gi, ); // 2. 删除 Word 命名空间标签 input input.replace(/\/?(?:o:|v:|w:)[^]*/gi, ); // 3. 删除注释 input input.replace(/!--[\s\S]*?--/g, ); return input; }这只是最基础的清理。真正可靠的清洗逻辑在下面的“节点级清洗”里。3.2 服务端第二道防线用 PHP/Python 实现节点级白名单清洗服务端清洗不能用字符串正则必须解析 DOM。我用 Python 版本举例因为它配合金融团队的现有技术栈很顺手lxml.html解析正文遍历所有节点做白名单过滤。from lxml import html from lxml.html import HtmlElement from typing import List, Dict, Set ALLOWED_TAGS: Dict[str, Set[str]] { p: {style, align, class}, div: {style}, span: {style, class}, strong: set(), em: set(), u: set(), br: set(), ul: {style}, ol: {style}, li: {style}, table: {style, width, border, cellpadding, cellspacing}, thead: set(), tbody: set(), tr: {style}, td: {style, colspan, rowspan}, th: {style, colspan, rowspan}, img: {src, width, height, alt, title}, a: {href, title, rel, target}, h2: {style}, h3: {style}, h4: {style}, blockquote: {style}, code: set(), pre: {style}, } ALLOWED_PROTOCOLS (http, https, mailto, tel) def clean_node(node: HtmlElement) - HtmlElement | None: # 该标签不在白名单内直接丢弃但保留子节点 if node.tag not in ALLOWED_TAGS: parent node.getparent() if parent is None: return None index parent.index(node) for child in reversed(node): node.remove(child) parent.insert(index, child) parent.remove(node) return None # 属性层过滤 allowed_attrs ALLOWED_TAGS.get(node.tag, set()) for attr in list(node.attrib.keys()): if attr.lower() not in allowed_attrs: del node.attrib[attr] # href/src 协议检测 for protocol_attr in (href, src): if protocol_attr in node.attrib: val node.attrib[protocol_attr].strip().lower() if not val.startswith(ALLOWED_PROTOCOLS): del node.attrib[protocol_attr] # style 属性内部做属性级过滤防止被塞进 expression() 或 url(javascript:) if style in node.attrib: node.attrib[style] filter_css(node.attrib[style]) return node def filter_css(style_string: str) - str: allowed_css_properties { text-align, font-size, font-family, font-weight, font-style, color, line-height, text-indent, margin, margin-top, margin-bottom, margin-left, margin-right, padding, padding-left, padding-right, width, height, background-color, border, border-collapse, vertical-align, white-space, text-overflow, overflow, display, position, } out [] for declaration in style_string.split(;): if : not in declaration: continue prop, value declaration.split(:, 1) prop prop.strip().lower() value value.strip().lower() if prop not in allowed_css_properties: continue # 屏蔽 CSS 注入特征 if expression in value or javascript in value or url( in value: continue out.append(f{prop}: {value}) return ; .join(out)这个清洗函数有很多细节值得展开。第一个细节是**“删除标签但保留子节点”**的逻辑。font、o:p这种标签直接删除会让里面的文字丢失所以正确做法是把它的子节点全部上提到父级再移除此标签。很多新手在这一步直接用drop_tag()导致文字凭空消失用户以为是自己操作失误实际上是清洗器把内容吞了。我踩过这个坑。第二个细节是style 属性内部的二次过滤。光过滤标签和属性还不够因为style属性内部仍然是个富文本阵地。攻击者可以在style里写background-image: url(javascript:...)或者利用 CSS 的expression()虽然 IE 才支持但金融合规体系不允许任何侥幸。所以 CSS 属性列表也必须白名单化甚至对属性值里的url(直接拒绝因为我们的业务内容根本不需要 CSS 引用外部资源。3.3 CKEditor 5 的配置差异与内容插入的改造如果你们的新系统切到了 CKEditor 5实现思路要跟着变。CKEditor 5 的架构是 MVVM 模式编辑器内部维护的不是 HTML 字符串而是一棵模型树Model再通过 View 层转换成 DOM。Paste from Word插件已经内置了 Word 清洗逻辑但默认策略偏保守很多mso-残留还是会进入编辑区。我的做法是在contentInsertion事件里做一道轻量修改import ClassicEditor from ckeditor/ckeditor5-build-classic; ClassicEditor.create(document.querySelector(#editor), { plugins: [/* ...现有插件 */], licenseKey: your-license-key, }) .then((editor) { const clipboardPipeline editor.plugins.get(ClipboardPipeline); const viewDoc editor.editing.view.document; viewDoc.on(clipboardInput, (evt, data) { if (data.content) { // data.content 是 ViewDocumentFragment需要转换成字符串清洗 const htmlString editor.data.processor.toData(data.content); const cleaned cleanWordHtml(htmlString); // 重新转回 document fragment 放回 data data.content editor.data.processor.toView(cleaned); } }); }) .catch((error) { console.error(error); });这里最重要的认知是CKEditor 5 的粘贴过程不是简单地把 HTML 塞进 DOM它会走一遍 Upcast 转换把 HTML 转成模型元素。如果我们在clipboardInput里把 HTML 清洗干净了后续 Upcast 的压力就小很多。相反如果等模型建好再去遍历模型树做过滤操作成本高得多因为涉及Schema校验、conversion回滚很容易弄巧成拙。3.4 表格列宽的收缩与“Exact”陷阱Word 里表格列宽无法拖动是一个在 Web 端高频出现的问题。根源在于Word 粘贴出来的每个td都带着一个精确到像素的width属性这些宽度值之和往往超过表格容器宽度。浏览器在渲染时如果表格没有设置table-layout: fixed或全局box-sizing: border-box就会出现“列实际宽度跟你的拖拽预期对不上”的诡异现象。我的解法是在清洗阶段做一次表格宽度归一化解析所有td的 width 属性换算成相对宽度。如果表格总宽度超过容器宽度比如 800px按下式缩放实际显示宽度 原始宽度 * (容器最大宽度 / 表格原始总宽度)清洗完的数据里table标签统一加width100%让表格自适应容器同时table-layout: fixed写进内联样式确保列的分配完全由colgroup或首行单元格的宽度决定。所有td的宽度只保留到两位数精度毕竟像素只有整数避免1.50000001px这种 Word 导出的浮点宽度造成渲染毛刺。这套方案落地后运营在 Word 里设定好的阅读宽度基本能在 Web 端一比一复现。顺带说一句如果你们内容是面向手机端为主的建议直接把所有表格的宽度策略从固定像素改成width: 100%毕竟移动端的屏幕宽度没法被一张 1000px 的死宽表格约束。3.5 图片抽离与公式图片转 Word 的处理Word 粘贴里的图片有三个来源直接粘贴的位图截图、嵌入的公式MathType 或 Word 公式编辑器生成、还有通过链接引用的远程图片。前两类在粘贴 HTML 时都会变成 base64 字符串。我清洗时统一把它们抽出来单独上传做成独立 CDN URL。这里有个环节容易被忽略base64 图片在 HTML 里的体积膨胀。一张原本 200KB 的截图base64 编码后大约膨胀 33%变成 266KB。如果一个编辑在一篇金融研报里粘贴了 15 张截图整个富文本字段会直接到 4MB 级别这对数据库、接口传输、页面加载都是三重压力。所以我在清洗阶段不仅抽图还会做一次图片压缩——判断如果图片是 PNG 格式且尺寸超过 1920px就先压缩到 1920px 宽、质量 80JPG再上传。关于 Word 公式转 Web我的建议是不要指望完美转换。Word 原生公式和 MathType 的 OLE 对象在 HTML 里都是o:OLEObject或者v:imagedata的形式本质上是一段二进制对象浏览器无法渲染。最务实的方案是在清洗阶段把这些对象统一替换成一个占位imgsrc 指向公式图片后端转换服务生成的 SVG 或 MathML。金融站群的内容里公式密集度高所以这个转换服务的稳定性直接影响编辑体验我当时是单独拉了一个 Python 服务来处理公式转图片公式缓存做 Redis key避免同一个公式反复转换。4. 常见问题与排查技巧实录4.1 标题居中后位置偏右问题不在样式而在标签结构“Word 标题居中后位置偏右”这个现象很多人的第一反应是改text-align改了还是偏。实际原因是 Word 粘贴出的“标题”自带首行缩进和左 padding这些样式隐藏在p的 style 里光改对齐属性不足以抵消。排查技巧用浏览器的 Elements 面板点选标题看右侧 Computed 样式里的margin-left和text-indent如果你看到非零值清楚地把这两个样式也一并清掉。按照我的经验Word 标题段落的残留样式里margin-left: -8.5pt、text-indent: 21.0pt出现的概率相当高。清洗时我要把所有标题类元素h2、h3、h4的text-indent强制清空margin-left统一归零或由模板决定才能彻底解决偏右问题。还有一个容易被忽略的点标题下方跟着一个mso-pagination: widow-orphan样式它在 Word 里控制孤行控制但在 Web 端会让标题和正文之间出现异常的空白间距。这个属性不属于我们 CSS 白名单直接会被剥掉但剥掉后标题默认的外边距会和正文间距叠加看起来好像“标题上面多了一段空隙”。遇到这类问题要检查清洗后 h2 的 margin 策略是否和你的模板一致不一致就统一清洗阶段注入模板指定的 margin 值。4.2 表格列宽无法拖动根源在固定宽度快照运营反馈“表格列宽无法拖动”很多时候不是真的不能拖而是拖完保存后重新进入编辑模式宽度又回到了 Word 粘贴时的快照值。原因是清洗阶段我把 width 转成了精确像素内联而 CKEditor 的表格 resize 功能TableResize 插件触发的列宽修改可能只作用于当前编辑会话的 DOM 属性并不一定能反向同步到原始的模型数据上。排查步骤建议按顺序走确认版本CKEditor 4 和 5 的 table 插件行为差异很大4 需要额外配置allowedContent里包含table[width]否则宽度属性在保存时被 ACF 剥掉。确认 ACF 规则table[width]、td[width]是否都加进了 config.allowedContent 或editor.filter.allow。确认内联样式优先级如果表格自身带了width: 900px !important这种有!important的 styleCKEditor 的拖拽修改会被!important压制。4.3 Word 和 WPS 生成的 HTML 差异别用一套规则硬套金融内容生产环境不可能统一所有人的 Office 版本Word 2016、Word 365、WPS 2019 生成的粘贴 HTML 差异很大。举个例子Word 365 生成的列表标签是标准ulliWPS 2019 有时会生成p classp01. 内容/p这种伪列表。如果用一套针对 Word 的清洗规则去清洗 WPS 内容会发现列表全部散架编号全部变成纯文本。处理思路是加一道“列表识别”环节遇到p里只有编号和文本、且文本以“数字 顿号/圆点”开头的尝试收敛为li。注意只做分类转换千万不要试图把正文里的所有数字都当成列表项否则会被精准误伤。这个识别规则我建议做成可配置项让不同站群根据内容风格决定要不要启用。4.4 标题多级样式丢失转为 h2/h3 后模板样式不生效Word 里设置好多级标题后粘贴进 CKEditor如果直接把段落标签转成 h3会发现金融站群模板里设定的 h3 颜色、字号、底部边距完全没有应用。原因是模板的 CSS 选择器通常依赖article .h3或.rich-content h3这类路径而清洗后存储的内容在模板里可能嵌在完全不同的容器中。我的做法是在服务端清洗阶段增加一个“模板感知”参数读取当前站点的模板标识把匹配的标题级别映射到该模板需要的 class 上。比如 A 站的 h3 对应的模板类名是.article-title-3B 站的则可能是.section-title-sm清洗器根据站点上下文输出不同的 class而不是一刀切地写h3。这个改造牵扯到清洗器与站群配置中心的联动看起来重但一旦做好运营内容在所有子站的标题样式就能统一受控。做金融站群内容样式的一致性直接和品牌形象挂钩这笔投入值得。4.5 公式图片转 Word 与 PDF 导出的兼容性如果金融站群的内容需要导出 Word 或 PDF监管报送、存档公式图片转换的问题就会浮出水面。方案是富文本里存一份 SVG 公式图片同时存一份 LaTeX 源码到扩展字段。导出 Word 时优先尝试把 LaTeX 转回 OMMLOffice Math Markup Language转不动就用 SVG 图片替换。导出 PDF 时直接使用 PNG 或 SVG 版本注意设置合适的 DPI 保证打印清晰度。顺带提醒一句公式和普通图片在清洗时不能一概而论。普通图片可以做压缩、转 JPG公式必须保留透明背景和矢量特性否则公式里的符号边缘会糊成一片。务必要在图片上传的元信息里标记imageTypeformula走独立处理管线。4.6 服务端清洗性能与超长文档的保险丝Word 粘贴的内容有可能非常大尤其是研报、招股说明书这类动辄几十页的文档。清洗函数在单条 HTML 超过 1MB 时DOMParser 的耗时可能飙升到一两秒以上加上图片抽离上传整体请求很容易超过网关超时。我在金融站群里做过的优化是限制单次粘贴大小前端在 paste 事件里判断dataValue.length超过 3MB约 300 万字符直接弹窗提示拆分粘贴。实践证明让运营把一个 50 页文档拆成 5 次粘贴对内容质量影响不大但对系统稳定性的改善是巨大的。清洗服务与主程序分离把服务端清洗做成一个独立 worker/服务图片上传并返回结果后由前端再发起一次正式的保存请求。这样清洗耗时 2 秒还是 5 秒都不影响主接口的响应时间。清洗超时熔断清洗服务内部对单条内容设置 5 秒硬超时超时则放弃正文清洗仅做白名单标签剥离宁可牺牲一部分样式也要保证内容能够正常入库。金融内容出错的容忍度低但不能因为清洗器超时导致编辑一整天的工作成果丢失。5. 安全加固与扩展实践5.1 富文本与 CSP 的结合不要让清洗成为唯一防线再认真的服务端清洗也不该成为唯一的防线。金融站群面对的内容生产链路很长编辑、运营、外部投稿都有可能输入内容。我建议在站点响应头里配置 CSP其中script-src self是底线。富文本编辑器本身需要运行 JavaScript但编辑后的内容渲染在用户端时页面里不应该存在任何由编辑内容注入的脚本。CSP 第一轮解析时很容易遇到问题编辑内容里的内联事件处理器比如img onerror...在 CSP 严格的页面里会被浏览器直接阻断不会执行这本身是好事。但如果你的渲染页面里误用了style-src unsafe-inline允许了内联样式那 CSS 注入的风险仍然存在。要把style-src也收紧为self或nonce-xxx虽然内联样式在富文本渲染时很重要但你可以选择让所有富文本内容渲染在 iframe 沙箱里或者用 CSS 白名单清洗保证内联样式里不会出现危险表达式。一个比较稳妥的配置如下add_header Content-Security-Policy script-src self nonce-${request_id}; object-src none; base-uri self; frame-ancestors self; img-src self https://cdn.example.com data:; style-src self unsafe-inline always;这里我把style-src保留为unsafe-inline是因为我们的清洗器已经过滤掉了所有非白名单 CSS 属性且禁止了url()表达式所以内联样式的风险被降到了可接受范围。但script-src和object-src必须严格这俩是脚本注入的入口。5.2 服务端白名单的配置化按站点调整清洗策略金融站群一个容易被忽略的坑是不同站点的模板能力不同对标签的宽容度也不同。比如主站可以渲染video但某个子站的模板根本不带 video 容器清洗器就应该直接把视频标签剥掉只留一个下载链接。所以不要把所有站点的清洗规则做成一套写死在代码里要做到按站点配置。我的项目里把白名单规则存到了配置中心结构大致是{ siteId: fund-news-01, allowedTags: { p: {style: true}, video: {src: true}, table: {width: true} }, imagePolicy: { maxWidth: 1920, quality: 80, uploadToCdn: true }, headings: { h2: article-h2, h3: article-h3 }, tablePolicy: { maxWidth: 800, normalize: true } }功能上线后运营团队要为一个新站点接入富文本只需要在配置中心加一条记录不需要改代码。这比“所有站点共用一套清洗规则”的思路灵活得多也方便后面接第三方投稿内容时单独给“外部来源”配置一套更严格的白名单。5.3 多端输出的一致性Markdown 与 Word 工作流最后聊一个在我这个项目里后期才做的扩展让富文本内容能同时输出为 Markdown 和 Word 文档。金融内容经常需要被复用进研报系统、合规存档系统这些系统大多只接受 Markdown 或 Word 格式。富文本清洗完成后的 HTML可以再通过mammoth或docx等工具反向转成 WordHTML 转 Markdown 在内容平台已经是成熟套路用turndown配合自定义规则就能处理标题、表格、列表。这里的关键是清洗阶段保存的数据结构最好是带语义的干净 HTML而不是带着各种模板 class 或内联样式的地狱产物。我在清洗器的最后一步会把内联样式择机“降级”成语义标签比如把stylefont-weight:bold变成strong这样后续转 Markdown 时就不至于丢失加粗语义。很多团队在做富文本时只考虑“渲染好看”不考虑数据复用我觉得这是把路走窄了——富文本内容是珍贵的结构化资产清洗的目标应当是“结构化优先样式其次”。我在实际项目里的体会是Word 样式解析没有“一次配好永久不管”的方案。Office 版本在变、粘贴格式在变、站群模板在变只有把清洗逻辑做成服务端的独立配置模块同时前端和后端各设一道过滤才能保证整个内容流转链路的质量和安全性。你如果正在做类似的事建议先把前端的干净粘贴体验做扎实再回头补服务端的白名单清洗这两步缺一不可。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。