尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Word图片粘贴到UEditor后的权限分级改造:三层联动实现涉密内容控制

Word图片粘贴到UEditor后的权限分级改造:三层联动实现涉密内容控制 前阵子有个做涉密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里全选内容直接CtrlC再跑到UEditor里CtrlV。这种方式对文字来说很顺畅但图片会被浏览器转成base64编码嵌在HTML里。base64图片意味着什么意味着图片内容直接躺在HTML源码里。你在数据库里存的是这样一段内容img srcdata:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA... /这种图片没有独立路径没有文件记录自然也没有权限标记。只要谁能看到这篇文档的HTML源码谁就能把这个base64解码成完整图片。而且如果文档存库后又被其他接口导出base64图片会跟着正文一起泄露存储层完全失控。所以我们的做法是在保存文章的时候对所有base64图片做一次清洗转存。后端识别出data:image开头的img把图片内容提取出来调用文件服务落地为独立文件分配一个fileId然后替换HTML里的img标签为/file/preview?fileIdxxx的形式。这样图片就从内容内嵌变成了受控资源。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指向鉴权代理接口 MapString, 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 ResponseEntityResource 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?fileId12345而不是某个物理路径。再加上用户浏览器里能看到的是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补全权限。这部分逻辑放在后端的好处是前端脚本无法被绕过任何人直接调保存接口都会经过同样的处理。保存完成后文章表、附件表、关联表的数据大致是这样文章表存正文HTMLimg 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里的权限分级才真正可落地、可验收、可追责。
返回列表