☰
Word图片粘贴到UEditor后的权限分级改造:三层联动实现涉密内容控制
2026/10/26 5:54:53 网站建设 项目流程

前阵子有个做涉密OA定制的朋友问了我一个很实际的问题:他们用百度UMEDITOR做富文本编辑器,业务人员经常会把Word里的图纸、截图、表格图片直接粘到编辑框里,现在单位要求这些图片不能谁都看,至少要分几个可见级别。他当时第一反应是去查"UMEDITOR 图片权限"之类的配置项,结果自然是没找到。

这个问题的本质,并不在于UMEDITOR缺了某个开关,而是需要把"编辑器"理解成内容采集入口,"Word图片"是内容的一种形态,"权限分级管理"则是一整套内容安全机制。把三者串起来,才能把需求落地。这篇文章就围绕这套联动方案来写,适合做OA定制、内网内容管理、知识库系统的开发人员参考,尤其是对那些安全要求比较严格的单位场景。

1. 先认清一件事:UEditor不提供权限能力,权限分级要靠三层联动

1.1 为什么直接搜"UEditor 图片权限"搜不到答案

UMEDITOR是一个功能完整的富文本编辑器,它的核心职责是:编辑、粘贴、上传、排版、输出HTML。它不会替你判断"这张图片谁能看、谁不能看",也不会在存储层替你控制图片文件的访问权限。就像Word本身不会管你文档里的图片是否涉密一样,编辑器只说"我帮你把图片传到服务器,并且给你一个img标签",剩下的事全都交给业务系统。

所以如果带着"配置一个参数就能实现图片分级"的预期去搜,基本是无解的。技术人员首先要接受一个事实:权限分级是应用层和安全层的设计,不是编辑器的功能。

1.2 把问题拆成三层:存储层、内容层、展示层

我们在设计这类功能时,习惯上会把"图片权限"拆成三个层面,因为每个层面的管控手段完全不同。

存储层解决的是"图片文件本身是否会被任意下载"的问题。比如用户猜到一个图片直链,没登录就打开了怎么办?这需要在文件读取的入口做鉴权,不能让图片静态资源裸奔。

内容层解决的是"图片被关联到某篇文档/某条信息后,它的权限属性是什么"的问题。Word里粘过来的图片,最初并不带任何权限标记,我们需要在它进入系统的瞬间,给它打上密级标签和业务域标签,并且把它和用户、文档关联起来。

展示层解决的是"不同权限的用户看到的内容不一样"的问题。高级别用户看到原图,低级别用户看到占位图或模糊图,甚至根本看不到。这层还分前端渲染控制和后端渲染控制,后面会详细讲。

三层各管一段,缺一不可。很多人只做了前端展示层,以为把img标签换掉就完了,结果图片直链被人扒出来,照样泄露,这就是典型的没做存储层管控。

1.3 一个足够用的最小权限模型:级别维度 + 业务域维度

关于权限分级,我见过很多方案把级别定义得非常复杂。但实际在涉密单位或高保密要求的组织里,真正常用的模型是双维度:

  • 级别维度:用一个整数表示,数值越小越公开。比如0代表公开,1代表内部可见,2代表受控查看,3代表高密级。具体的等级命名各单位按自己的体系来定就好,系统只需要支持灵活配置。
  • 业务域维度:表示这张图片属于哪个项目、哪个方向、哪条业务线。这个维度特别重要,因为现实中往往不是"级别到了就都能看",而是"级别到了且属于我的业务域才能看"。比如同样是受控级别,A项目的人不能看B项目的受控图纸。

用表结构来表达就是这样:

CREATE TABLE sys_attachment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, file_name VARCHAR(255) NOT NULL, file_path VARCHAR(500) NOT NULL, file_md5 VARCHAR(64), data_level INT DEFAULT 0 COMMENT '密级数值,越大越敏感', business_domain VARCHAR(64) COMMENT '业务域/项目编码', upload_user VARCHAR(64), upload_time DATETIME, status TINYINT DEFAULT 1 COMMENT '1有效 0删除' );

用户表里也建议加上两个字段:user_level和domain_code。判断逻辑就是:图片.data_level <= 用户.user_level且图片.business_domain 匹配 用户.domain_code时,才允许查看。这个模型简单、直观,也容易扩展到人员调动或项目临时交叉的场景。

2. Word文档里的图片,进入UEditor时会留下哪些"权限缺口"

2.1 复制粘贴路径:base64图片悄悄绕过所有权限

这是最隐蔽的坑。很多业务人员的操作习惯是:在Word里全选内容,直接Ctrl+C,再跑到UEditor里Ctrl+V。这种方式对文字来说很顺畅,但图片会被浏览器转成base64编码嵌在HTML里。

base64图片意味着什么?意味着图片内容直接躺在HTML源码里。你在数据库里存的是这样一段内容:

<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..." />

这种图片没有独立路径,没有文件记录,自然也没有权限标记。只要谁能看到这篇文档的HTML源码,谁就能把这个base64解码成完整图片。而且如果文档存库后又被其他接口导出,base64图片会跟着正文一起泄露,存储层完全失控。

所以我们的做法是:在保存文章的时候,对所有base64图片做一次"清洗转存"。后端识别出data:image开头的img,把图片内容提取出来,调用文件服务落地为独立文件,分配一个fileId,然后替换HTML里的img标签为/file/preview?fileId=xxx的形式。这样图片就从"内容内嵌"变成了"受控资源"。

2.2 上传路径:图片直链固定,拿到URL就等于拿到图

UMEDITOR默认的上传功能,接口处理完后会返回图片的直链URL。比如返回:

{"state":"SUCCESS","url":"/ueditor/upload/image/2024/01/01/abc.png"}

如果富文本里的img直接引用这个地址,那么这张图片就是"挂在外头"的静态资源。只要URL不换、文件不删,任何人拿到链接都能打开,甚至可以直接用URL在浏览器里下载高清原图。权限分级形同虚设。

要堵住这个口子,思路很明确:不允许前端直接引用真实存储路径,所有图片访问都要经过一个带鉴权的代理接口。换句话说,即使你知道真实文件在服务器哪里,你也访问不了,只有通过业务接口校验身份和权限后,才能在浏览器里看到图。

2.3 Word整篇导入路径:图片的密级标签丢失

还有一种常见的导入方式:业务方把Word文件另存为HTML,或者用工具把Word拆成HTML再粘到UEditor里。这种方式会把Word里的图片以文件形式导出到HTML同级的目录里。图片的alt属性、位置信息可能有,但不会自带"密级"这个概念。

所以你会遇到一个问题:图片确实进来了,但它默认是"无级别"的,谁能看谁不能看完全没约束。更麻烦的是,Word里还有两类让人头疼的元素:

  • 浮动图片:Word中很多图片不是嵌入型,而是浮动在文本上。粘贴到UEditor时,浮动对象经常被丢弃,或者变成奇怪的空白占位。
  • OLE嵌入对象:比如嵌入的Excel图表、Visio示意图,粘贴过来后通常会变成一张截图或者一个图标,原始矢量信息全丢了,权限管理也无从谈起。

因此实际项目里,我不建议依赖"Word图片自动携带权限"这个想法,因为Word本身就没有这套机制。最可靠的做法是:在内容导入环节,由导入人统一指定这批图片的默认密级和业务域。比如导入向导里加一个下拉框:"本次导入图片的权限级别为:内部/受控/高密"、"所属项目:某某工程"。后续如果某张图需要特殊处理,编辑者可以在编辑器里逐个调整。

3. 落地实现:从上传接口到前端鉴权的完整改造

3.1 改造UEditor的imageUp接口:带上密级参数落库

UEditor在Java版本里的图片上传action是imageUp,对应的处理类会接收MultipartFile,然后调用存储服务保存文件。我们改造的核心目标是:上传成功后不要只返回URL,同时把fileId和level带出来,并且落库到sys_attachment。

改造后的关键逻辑像这样:

@PostMapping("/ueditor/controller") public String controller(String action, MultipartFile upfile, Integer dataLevel, String businessDomain, HttpServletRequest request, HttpServletResponse response, HttpSession session) { if ("uploadimage".equals(action)) { // 1. 解析当前登录用户,获取默认级别和业务域 User user = getCurrentUser(session); int level = (dataLevel != null) ? dataLevel : user.getDefaultLevel(); String domain = StringUtils.hasText(businessDomain) ? businessDomain : user.getDomainCode(); // 2. 保存图片文件,不要用可猜测路径 String realPath = fileStorageService.save(upfile, level, domain); String fileId = genFileId(); // 3. 图片信息落库 Attachment att = new Attachment(); att.setId(fileId); att.setFileName(upfile.getOriginalFilename()); att.setFilePath(realPath); att.setDataLevel(level); att.setBusinessDomain(domain); attachmentService.save(att); // 4. 返回结果,注意url指向鉴权代理接口 Map<String, Object> result = new HashMap<>(); result.put("state", "SUCCESS"); result.put("url", "/file/preview?fileId=" + fileId); result.put("fileId", fileId); result.put("level", level); return JsonUtils.toJson(result); } // 其他action按原逻辑处理 }

有几个细节值得注意:realPath不能让外部猜到,我个人的习惯是使用UUID做文件名,目录按年份月份分,少用业务含义太明显的路径。另外,假设上传者自己级别很低,他就不应该能传高密级图片,后端要校验dataLevel <= user.getUserLevel(),否则就是提权漏洞。

3.2 图片展示走鉴权代理接口,真实路径不外泄

接下来是最关键的一步:图片资源访问统一走/file/preview接口。这个接口并不是简单地读文件返回,而是先做权限校验。

@GetMapping("/file/preview") public ResponseEntity<Resource> preview(@RequestParam("fileId") Long fileId, HttpSession session) { // 1. 未登录直接拒绝 User user = getCurrentUser(session); if (user == null) { return ResponseEntity.status(401).build(); } // 2. 查出图片的权限属性 Attachment att = attachmentService.getById(fileId); if (att == null || att.getStatus() != 1) { return ResponseEntity.notFound().build(); } // 3. 校验级别和业务域 boolean canAccess = att.getDataLevel() <= user.getUserLevel() && domainMatches(att.getBusinessDomain(), user.getDomainCode()); if (!canAccess) { return ResponseEntity.status(403).build(); } // 4. 通过校验后,读取真实文件流返回 Resource resource = fileStorageService.loadAsResource(att.getFilePath()); String contentType = determineContentType(att.getFileName()); return ResponseEntity.ok() .contentType(MediaType.parseMediaType(contentType)) .body(resource); }

这里说一下Nginx层面的东西。如果你的图片静态目录直接被Nginx托管,比如/ueditor/upload/,那代理接口就形同虚设了,因为用户可以直接拼真实路径去访问。所以要把端口、目录权限收口:Web层只暴露代理接口,图片物理目录不允许外部HTTP直接访问。这一步不做,后面所有前端控制都等于白做。

前端富文本里的img标签,此时src值已经是/file/preview?fileId=12345,而不是某个物理路径。再加上用户浏览器里能看到的是cookie或token里携带的会话身份,没有合法会话的人连图都加载不出来。

3.3 富文本内容保存时附加权限元数据

光让文件走鉴权代理还不够,富文本正文里必须把每一张图片的权限属性也带上,否则展示端拿到HTML后会不知道该给谁看什么。

做法是在保存文章时,对正文HTML做一次预处理:遍历所有img标签,给每个img加上>// 前端在提交正文前,扫描所有img const imgs = contentDom.querySelectorAll('img'); imgs.forEach(img => { if (img.dataset.fileId) { // 如果图片是通过上传接口进来的,保留fileId即可 // 但如果没有level属性,后端会按默认级别兜底 } else if (img.src.startsWith('data:image')) { // base64图片,上传到临时文件并分配fileId // 这里用hidden input或异步上传处理 } });

更方便的做法是直接在后端保存接口里做清洗,因为后端有文件系统访问能力,也更安全。比如定义一个小工具类,专门负责扫描HTML中的base64图片并转存,再扫描所有img补全权限。这部分逻辑放在后端的好处是:前端脚本无法被绕过,任何人直接调保存接口都会经过同样的处理。

保存完成后,文章表、附件表、关联表的数据大致是这样:

  • 文章表:存正文HTML,img src指向鉴权代理接口,img带data-level和data-domain。
  • 附件表:存每个图片文件本身的权限信息。
  • 文章-附件关联表:记录"哪篇文章用了哪些图片",方便后续批量复核和清理。

3.4 前端按需渲染:可见图片正常显示,越权图片给占位图

展示层的目标很明确:同一篇文章,高密级用户看到完整内容,低密级用户看到图片位置出现"无权限查看"占位图,而不是一张裂掉的图标。

这里有一个决策点:是在后端渲染时就处理,还是在浏览器端处理。我的建议是以后端渲染为主,前端脚本为辅。原因很实际:如果完全依赖前端JS,用户关闭JS、或者把HTML源代码拉走,分级保护就没了。后端渲染时,根据当前用户的级别,直接把不可见的img替换成占位图,输出到前端的就是处理后的HTML。

但是后端渲染也有麻烦的地方:文章内容可能是静态化的、缓存的,没法每次请求都做个性化渲染。所以实际项目中,常见的是"前端按权限控制显示"加"后端接口按权限控制输出"双保险。

前端脚本的逻辑并不复杂,用原生JS就能实现:

document.querySelectorAll('.article-content img[data-level]').forEach(img => { const needLevel = parseInt(img.dataset.level, 10) || 0; const userLevel = window.currentUser ? parseInt(window.currentUser.level, 10) : 0; const userDomain = window.currentUser ? window.currentUser.domain : ''; const imgDomain = img.dataset.domain || ''; // 级别不够,或者业务域不匹配,换成占位图 if (needLevel > userLevel || (userDomain && imgDomain && userDomain !== imgDomain)) { img.dataset.originalSrc = img.src; img.src = '/static/images/no-permission.png'; img.classList.add('img-permission-denied'); } });

需要注意,替换成占位图后,用户虽然看不到原图,但可能还是会通过复制img的originalSrc去请求图片。此时我们的第二道防线就起作用了:/file/preview接口后端校验过权限,依然返回403,浏览器只能显示空白或裂图。这就是为什么一定要在后端把文件访问收口,不能光靠前端替换src。

4. 线上踩过的坑与验证方法

4.1 坑一:Nginx/CDN缓存把鉴权"绕过"了

这个坑特别典型。我们一开始用代理接口返回图片,但测试时发现一个诡异现象:用低权限账号访问某篇含受控图片的文章,图片居然正常显示了。排查下来是Nginx对静态图片做了缓存,代理接口第一次用高权限账号请求时,响应结果被Nginx缓存,后面低权限账号访问时,Nginx直接返回了缓存图片,根本没到后端。

解决思路有两个:一是对/file/preview接口禁用缓存,设置Cache-Control: no-store;二是更严谨地把鉴权判断放到URL参数里,即便要缓存,也是按用户维度隔离。我的建议是直接禁用缓存,管理后台的图片本来就不需要强缓存,而且能避免一个很严重的逻辑漏洞:一旦缓存系统把高权限用户的图片响应缓存住,低权限用户就可能看到越权内容。

另外,如果站点用了CDN,要特别注意CDN的缓存规则。凡是带鉴权语义的URL,一概不缓存。这条建议写进上线检查清单里,能在运维阶段省去很多麻烦。

4.2 坑二:Word粘贴的base64入库后无法单独控权

刚开始做这套系统时,我们把Word里粘贴到UEditor的base64图片原样存进了文章正文。结果上级来检查权限时发现,某篇高密级文章的正文HTML里,那张图纸的base64内容就明晃晃地躺在数据库里,导出备份文件一查就能看到完整图片。这就是典型的"文件级权限做了,内容级权限没做"。

后来我们在保存接口里加了清洗转换,遇到base64图片就先转存,然后在正文里替换成代理URL。这个操作必须放在后端,因为前端脚本完全有可能被绕过,而且一旦图片数量多,前端转换还会拖慢页面。

如果你是在存量系统上做改造,历史数据里可能已经积压了大量base64图片正文,我建议写一个离线清理任务,扫描历史文章,逐条把base64图片转存,并替换正文。转存后的图片权限先按"文档权限"批量打标,再安排人工复核。

4.3 坑三:用户通过开发者工具定位到真实图片直链

有同事问:既然图片经过代理接口,为什么客户端还能拿到原图?答案是:在浏览器里,只要能看自然就能下载。用户打开开发者工具,找到图片请求,拿到响应体里的图片二进制,照样能保存原图。

这其实不是权限设计的漏洞,而是Web应用本身的能力边界。真正要做的事是:敏感图片不要放在浏览器可访问的范围内,比如给关键图纸加水印、对图片做模糊化处理。我见过的一个方案是:级别最高的图片,在代理接口输出时动态叠加水印(当前用户ID和时间戳),这样即便有人截图或者保存图片,也能追溯泄露源头。UEditor和Word图片分级管理的结合点就在这里——你不可能阻止一个"有权限看"的人保存图片,但你可以有能力追责。

动态水印的方案很简单,用Java的Graphics2D在图片上叠加文字即可,开销不大,但对安全意识的威慑作用很强。高密级图片建议采用这个做法。

4.4 怎样做一轮完整的权限验收测试

验收环节容易被人忽视,但这类安全功能出问题往往是上线后才发现。我建议按下面的测试矩阵逐项过一遍:

测试场景预期结果
高密级用户查看含受控图片的文章图片正常显示
内部用户查看同一篇文章显示占位图,访问代理接口返回403
未登录直接访问图片代理接口返回401
猜解真实图片路径访问无权限访问,因为目录不对外
用curl直接请求代理接口并携带Cookie与登录状态一致,无法越权
文章正文里手动注入高密级img标签展示时被替换,接口校验失败
高密级图片访问接口响应头无缓存,禁用CDN缓存

验收完成后,再安排一次"安全走查",重点检查:编辑器上传接口是否允许低权限用户传高密级图片、保存接口base64转存逻辑是否可绕过、代理接口是否存在文件ID遍历问题(id自增的话可以加权限校验,或者改用UUID)。

5. 给高密级场景的额外建议

5.1 不要忽略Word源文件本身的安全

我们聊了这么多,都是围绕"Word里复制出来的图片进了网页系统后"的管控。但是别忘了,Word源文件本身可能有更高的密级。业务人员如果在本地把图纸粘贴到网页上,图片级别就算标得再高,源文件仍然躺在他的硬盘里和聊天记录里,那时候什么权限系统都拦不住。

所以我在给这类单位做方案时,通常会提醒:系统内的图片权限分级只是"最后一道防线",真正重要的是源头管理。比如UEditor粘贴Word内容前,能否让用户确认"本次粘贴内容属于什么级别",从行为上提高安全意识,减少无意识泄露。

5.2 建立图片权限清单与定期复核机制

管理后台建议加一个"附件权限管理"页面,把所有的图片附件按密级、业务域、上传人、关联文档列出来。运营人员可以定期核查:某张高密级图片是否还被某篇低密级文章引用,如果文章权限调整了、图片权限没跟上,要能及时预警。

这个复核流程在涉密环境里是刚需。因为业务人员在编辑文档时,很可能给整篇文章定了一个公开级别,但里面某张图是受控的,如果系统不能自动识别并提示,就会造成越权展示。所以我在保存接口里还加了一个校验:如果正文中某张图片的level高于文章默认level,必须提示用户确认,而不是静默放行。

5.3 关于UEditor生态:兼容旧版本与二开成本的取舍

最后说个冷知识:百度UMEDITOR虽然已经很少更新,但仍然是很多政企项目里的存量编辑器。它的插件机制、上传接口都能通过二开扩展,我们的权限改造完全不涉及编辑器源码改动,只是在上传Controller、保存接口和展示端做了处理。这一点很重要——意味着你不需要升级编辑器版本,也不需要担心改动影响原有的排版功能。

如果你们系统本来就有docx解析、word转html的流程,那还可以把权限标记前置到解析阶段,比如在Word的图片alt文本里约定#level2#domainA这样的标记,解析HTML时自动提取并写入data-level和data-domain。这个玩法适合批量导入的存量文档,一次性把几千张图片的密级属性补齐,比人工逐张点击设置高效得多。

从整条链路来看,权限分级管理的核心思路并不复杂:图片进入系统时,给它一个身份;图片展示时,校验这个身份;图片被访问时,再校验一次身份。三层各自守住一道关口,Word图片在UEditor里的权限分级才真正可落地、可验收、可追责。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询