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

Vue3富文本安全渲染:VueDOMPurifyHTML指令实战指南

发布时间:2026/9/26 13:08:46

资讯中心
01
ARTICLE

Vue3富文本安全渲染:VueDOMPurifyHTML指令实战指南

Vue3富文本安全渲染:VueDOMPurifyHTML指令实战指南
刚接手一个 Vue3 后台管理系统运营那边要上富文本编辑器帖子内容支持视频、图片、超链接、加粗列表这些。功能倒是好说但提交到页面上渲染的时候我心里一直不踏实这段 HTML 是用户生产的直接塞进v-html里万一里面夹点script或者onerror挖的坑那整个页面就裸奔了。XSS 这词听起来像安全团队的事但前端如果不在渲染这层做过滤等漏洞真被捅出来背锅的跑不了。后来我把方案定成了VueDOMPurifyHTML也就是基于 DOMPurify 封装给 Vue 用的指令/组件用起来比手动调DOMPurify.sanitize顺手太多而且默认配置就能挡掉绝大多数恶意代码。这篇就详细聊聊它的基本用法、踩过的坑和实际项目里值得注意的细节。先说明一下适用对象如果你在 Vue2/Vue3 项目里需要渲染用户提交的 HTML 内容又被v-html的安全问题困扰这篇文章基本就是给你写的。哪怕你只是刚学 Vue 不久也能跟着步骤直接装起来跑通。1. 为什么富文本渲染必须做清洗而不是直接v-html1.1v-html的风险到底长什么样很多人觉得v-html只是“把字符串当 HTML 渲染”感觉没什么大不了的。直到你某天收到一条这样的内容img srcx onerroralert(document.cookie)就这么一小段如果塞进v-html页面加载时图片报错onerror立刻执行弹窗还是小事如果攻击者把alert换成向某个域名发送document.cookie的请求那用户的登录态可能就泄了。我们系统里用户的会话令牌就存在 Cookie 里还有一部分存在 localStorage。这类攻击现场我见过不止一次而且大多数发生在评论、富文本、公告这一类“谁都能填”的入口上。还有一种看起来更隐蔽的写法a hrefjavascript:alert(1)点我/a用户看着是个正常链接实际点击时执行了 JavaScript。再比如iframe srchttps://evil.example/iframe直接在页面里嵌入第三方网页可以做钓鱼也可以做点击劫持。v-html对这些没有任何防御它是“完全信任”输入的。而真实业务里前端最不该信任的就是用户输入。1.2 DOMPurify 的核心原理DOMPurify 是目前前端做 HTML 清洗最主流的库没有之一。它的核心做法并不是用正则去匹配标签而是借助浏览器提供的 DOM 解析能力把 HTML 字符串解析成真实的 DOM 节点然后遍历这棵 DOM 树把不允许的标签、属性、事件处理函数全部剥离掉再把剩余部分序列化输出。为什么不用正则因为 HTML 的容错性太强浏览器能解析出各种匪夷所思的写法。比如属性值里的字符实体、标签大小写混写、属性名和函数名之间的混淆写法正则很难全部处理干净。而 DOMPurify 直接在 DOM 层面清洗浏览器怎么解析它就怎么检查从根上规避了绕过问题。DOC 解析与清洗这套流程是业内大量项目验证过的所以我对它的信任度比较高。VueDOMPurifyHTML 本质上就是把这个清洗能力封装成了 Vue 的指令或者组件让我们不用在每个需要渲染的地方手动写import DOMPurify from dompurify const clean DOMPurify.sanitize(rawHtml)而是直接在模板里声明式地完成清洗和渲染。1.3 VueDOMPurifyHTML 解决的是“最后一公里”问题有人说那我手动每次调DOMPurify.sanitize()不就行了确实可以但实际写起来很容易忘记。尤其是模板里同时存在几十个v-html的时候你很难保证每一处都记得先清洗再渲染。而且清洗结果你还得用变量存起来或者写一个计算属性代码冗余不说还容易有意无意漏掉。VueDOMPurifyHTML 把“清洗后再渲染”这一步收敛到了指令本身。模板里写div v-dompurify-htmlcontent/div它内部先执行 DOMPurify 清洗再把清洗后的安全 HTML 注入到元素里。这样从代码审查的角度看只要看到v-dompurify-html就知道这处 HTML 渲染是经过清洗的看到赤裸的v-html就得停下来想想有没有问题。这种“代码即安全标识”的好处在团队协作里价值很大。2. 安装与基础接入从 npm 到第一个安全渲染2.1 安装依赖VueDOMPurifyHTML 对应的 npm 包名是vue-dompurify-html它依赖dompurify但安装的时候不需要你手动单独装dompurify因为包内部会依赖它。直接执行npm install vue-dompurify-html如果你用 pnpm我建议执行pnpm add vue-dompurify-html安装完成后检查一下package.json你会看到dompurify出现在 dependencies 里。我这里的版本是vue-dompurify-html5.x支持 Vue3如果你还在用 Vue2需要用旧版本vue-dompurify-html2.x或查看官方文档的兼容说明。这一点要注意我第一次在 Vue3 项目里直接装了旧版本结果插件注册时报错。2.2 在 main.js 里注册插件VueDOMPurifyHTML 提供了插件方式和指令方式两种用法我实际项目里用的是全局指令方式一次注册到处可用。新建src/main.js在创建应用后注册import { createApp } from vue import App from ./App.vue import VueDOMPurifyHTML from vue-dompurify-html const app createApp(App) app.use(VueDOMPurifyHTML) app.mount(#app)注册之后你的全局模板里就有了v-dompurify-html指令用法和v-html几乎一致template div classarticle-content v-dompurify-htmlarticle.content/div /templatearticle.content是什么类型它就渲染成什么类型不需要你额外写计算属性或者手动转换。清洗逻辑全部封装在指令内部。2.3 局部按需使用如果你不想全局注册只想在某个组件里临时用一下也可以按需引入。VueDOMPurifyHTML 本身导出了一个叫VueDOMPurifyHTML的组件你可以这样用script setup import { VueDOMPurifyHTML } from vue-dompurify-html /script template VueDOMPurifyHTML :contentarticle.content / /template组件模式会自己创建一个容器标签来承载渲染结果。我比较推荐在封装业务组件的时候用组件模式比如做一个RichTextContent.vue里面就用这套外部只传content进去职责清晰。2.4 一个完整的最小示例把上面串起来一个最小可跑的示例是这样的ArticleView.vuetemplate div h2{{ article.title }}/h2 div v-dompurify-htmlarticle.content/div /div /template script setup import { reactive } from vue const article reactive({ title: 测试文章, content: p正常段落/p img srcx onerroralert(xss) a hrefjavascript:alert(1)危险链接/a scriptalert(xss)/script }) /script渲染后打开页面你会发现onerror、javascript:协议、script标签全部被过滤掉了只剩正常的p正常段落/p。这就是 DOMPurify 的默认策略在起作用。注意默认情况下 DOMPurify 会移除所有的事件绑定属性和危险协议。这是好事但如果你业务上确实需要保留某些标签或属性比如要让富文本里的iframe嵌入视频就得靠配置项来放行别直接用默认配置硬扛否则会误杀功能。3. 配置文件与进阶用法放行标签、自定义钩子3.1 全局配置默认选项VueDOMPurifyHTML 在注册时可以传入全局配置配置对象会被透传给 DOMPurify 的sanitize方法。举个例子如果我们允许用户内容里嵌入iframe比如爱奇艺、B站视频并且允许保留class属性可以这样配置import VueDOMPurifyHTML from vue-dompurify-html const defaultConfig { ALLOWED_TAGS: [p, span, div, h1, h2, h3, h4, strong, em, u, s, ul, ol, li, a, img, blockquote, code, pre, br, hr, iframe, table, thead, tbody, tr, th, td], ALLOWED_ATTR: [href, src, alt, title, class, width, height, frameborder, allowfullscreen], ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto|tel|ftp):|[^a-z]|[a-z.-](?:[^a-z.-:]|$))/i } app.use(VueDOMPurifyHTML, defaultConfig)这样配置之后v-dompurify-html就会保留iframe标签和它的常用属性。但注意ALLOWED_TAGS是白名单机制你列了哪些才允许哪些没列的一律过滤。上面这段清单看起来已经不少了但如果你业务里需要支持更多标签比如video、audio、source、details、summary就得自己加进去否则会被吞掉。3.2 用 Hooks 扩展清洗逻辑DOMPurify 提供了一系列生命周期钩子VueDOMPurifyHTML 的配置里也能传 hooks。我在一个项目里遇到过一个需求用户提交的 HTML 里图片src可能指向站外或者外链但产品要求图片必须走自己的 CDN 域名白名单否则替换成默认占位图。这个用 hooks 实现非常方便import VueDOMPurifyHTML from vue-dompurify-html import DOMPurify from dompurify const config { hooks: { afterSanitizeAttributes: (node) { if (node.tagName IMG) { const src node.getAttribute(src) || if (!src.startsWith(https://cdn.example.com)) { node.setAttribute(src, /images/placeholder.png) } } } } } app.use(VueDOMPurifyHTML, config)afterSanitizeAttributes这个钩子在每个属性清洗后触发此时危险属性已经被 DOMPurify 过滤掉了我们在此基础上再做一层业务校验。要注意的是hooks 的 API 是 DOMPurify 本身提供的VueDOMPurifyHTML 只是透传所以配置结构跟 DOMPurify 一致这点在阅读文档时要心里有数。3.3 指令级覆盖全局配置全局配置能满足大部分场景但偶尔会遇到“这个页面要放行 video那个页面要放行 iframe”的差异需求。VueDOMPurifyHTML 在指令用法上可以通过传递一个对象作为指令值实现单次调用的配置覆盖template div v-dompurify-htmlrenderContent/div /template script setup import { computed } from vue const props defineProps({ content: String, allowVideo: Boolean }) const renderContent computed(() { if (props.allowVideo) { return { content: props.content, config: { ADD_TAGS: [video, source], ADD_ATTR: [controls, poster, src] } } } return props.content }) /script指令如果接收到的是一个对象并且对象里有content和config字段就会用config合并全局配置后执行清洗。这个设计很灵活但要注意别在模板里写太复杂的对象字面量不利于维护建议通过计算属性或者普通方法提前构造。从实际使用体验看指令的“对象模式”容易让人忽略但它确实解决了差异化渲染的痛点。如果你项目里碰到“同一个组件不同场景需要不同标签白名单”的情况优先考虑这个方案而不是在全局配置里把白名单无限扩大。白名单越大风险面越大。4. 实战案例富文本编辑器内容的安全渲染闭环4.1 需求背景我们的内容管理后台用的是wangeditor现在叫 wangEditor 5编辑器产出的是标准 HTML 字符串。用户在前台阅读页看到的文章详情、公告内容、帮助文档都来自这个字符串。后台编辑者是可以被信任的内部员工但内容本身可能被复制粘贴粘贴来源可能是其他网页。浏览器粘贴网页内容时会带上源页面的样式、事件脚本等这些属于“防御范围内的不可信输入”一样需要清洗。所以我对后台编辑器的“预览”功能和前台“阅读页”都统一使用了v-dompurify-html这样不管是用户主动写的还是粘贴进来的都走同一套清洗逻辑。4.2 编辑器输出到页面渲染的完整链路整个闭环差不多是这个流程编辑器初始化用户编辑内容。用户点击保存编辑器输出 HTML 字符串后端接口接收后存库。前端阅读页通过接口拿到 HTML 字符串。模板里使用v-dompurify-html渲染。链路里最容易出问题的是第 2 步有些编辑器输出的 JSON 结构里可能包含style属性、class属性、>const config { ALLOWED_ATTR: [style], ALLOWED_CSS_PROPERTIES: [color, background-color, text-align, line-height, font-size, font-weight, font-style] }这样既允许多行样式又只放行常见的文本格式属性避免被塞入position: fixed这类影响页面布局的样式。从我的实践看富文本编辑器的样式清洗比标签清洗更隐蔽好多团队只顾着管标签结果样式里被塞了恶意内容或者奇怪布局虽然不是脚本级风险但确实影响体验。4.3 图片懒加载与清洗结合的方案文章页面图片通常要做懒加载常见做法是把真实地址放进>ALLOWED_ATTR: [src, data-src, alt, title, class]但这么做有个隐患>template div v-dompurify-htmlarticle.content clickhandleContentClick /div /template script setup import { useRouter } from vue-router const router useRouter() function handleContentClick(event) { const anchor event.target.closest(a) if (!anchor) return const href anchor.getAttribute(href) if (href href.startsWith(/)) { event.preventDefault() router.push(href) } } /script这里有一个细节href如果是绝对地址http开头或者mailto:协议我们不做拦截正常交给浏览器处理只有站内相对路径才走前端路由。这个逻辑我们专门测试过清洗后javascript:协议已经被 DOMPurify 干掉了所以这里不用担心事件委托拿到的是危险链接。5. 常见问题与排查技巧实录5.1 内容突然少了一部分排查半天发现是白名单问题遇到最多的情况就是“我内容里明明有video标签渲染出来没了”。绝大多数是白名单里没有video。这时候千万不要想着“把白名单放开到允许所有标签”DOMPurify 提供了ALLOW_UNKNOWN_PROTOCOLS之类的配置但强烈不建议为了省事直接全部放行这样等于把安全防线拆了。正确做法是有针对性地加标签和属性并同步评估内容来源是否可信。我调试这类问题常用的办法是先把清洗后的字符串打印出来看看到底丢了什么import DOMPurify from dompurify console.log(DOMPurify.sanitize(video srcx/videoscriptalert(1)/script, config))这样能快速确认是“清洗环节丢的”还是“后端存的时候就没存上”。很多时候后端接口会做一层过滤前端又做一层查问题时容易互相推诿用这个办法可以明确责任边界。5.2 配置了ALLOWED_TAGS但是自定义元素还是被过滤现在的富文本编辑器偶尔会输出自定义元素比如mention、emoji。DOMPurify 默认只认识标准 HTML 标签你必须在ALLOWED_TAGS里显式加入自定义标签名ALLOWED_TAGS: [...defaultTags, mention, emoji]但就算加了标签名如果自定义标签内部还嵌套了复杂内容比如mention里包含一个img那img也需要在白名单里。嵌套关系不会自动继承“放行”状态每一层标签和属性都要单独允许这是我开发自定义业务组件时经常踩的坑。另外自定义元素如果在清洗后被浏览器解析成未知元素样式可能不是预期的效果。建议在全局 CSS 里给这些自定义元素加上display和相关样式防止它变成默认的内联元素导致布局错乱。5.3 编辑保存后的内容再编辑时“格式错乱”富文本编辑器二次编辑时通常会把存下来的 HTML 字符串重新导入编辑器。前端阅读页用的是清洗后的内容但编辑器里导入的如果是清洗后的版本有些编辑器私有属性比如>
02
RELATED NEWS

相关资讯

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

03
WHY YAOTU

想打造同款高转化官网?

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

◈

场景化定制

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

◐

营销型架构

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

▲

全周期服务

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

免费获取你的建站方案

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