
附件删除权限这件事在EOS8.3.3上做过的朋友应该都懂那种纠结需求一句话“附件允许多选不是同一个人上传的只能删除自己上传的”写起来却要拆出一整套逻辑。多附件、多上传人、前端的删除入口、后端的接口校验哪一个环节漏了都会出大问题。这篇文章我就把完整的实现思路、关键代码、以及我在实际项目里踩过的几个坑一次性讲清楚给正被这个需求折磨的兄弟做个参考。1. 场景还原谁在什么情况下会碰到附件删除权限问题1.1 多附件多上传人的典型场景这种需求在OA审批流、协同办公、项目管理系统里特别常见。我举个例子你就明白了某个项目立项审批单上挂了一个“补充材料”的附件区域这个区域配置了多选上传权限。前期申请人先传了营业执照、项目方案审批过程中市场部经理又补充传了一份市场调研报告财务审批岗再传了一个预算表。这样一个业务单据上就挂了三个不同人上传的附件。接下来问题就来了。附件区域是多选权限意味着在界面上看到的附件列表里每一行都有一个删除操作。如果系统不区分上传人那么任何一个有删除权限的人打开这个单据都能把别人上传的附件删掉。一旦有人误操作或者有意为之轻则审核材料缺失要重新上传重则直接影响审批流程的存档完整性和审计追溯。这个需求在国产化、信创类项目里出现频率极高因为这类项目对操作留痕和权限边界卡得很严。功能验收的时候如果测试人员同时上传一个附件再删除发现能删掉别人传的文件这个问题就会被记为严重缺陷甚至高危漏洞。所以别小看这一个小小的附件删除控制在正式验收面前它就是一条硬指标。1.2 需求拆解三个必须达成的点把这句话“附件允许多选有多个附件不是同一个人上传的需要判断只能删除自己上传的”拆开看其实包含三个独立的约束条件。第一个条件是系统必须能识别“当前操作人是谁”。这个通常依赖登录会话中的用户信息但在EOS8.3.3的工程实践中不同项目获取用户信息的方式差异不小单点登录、统一身份认证、session共享某个环节没处理好当前用户就取不到值后面的判断全部无从谈起。第二个条件是系统必须能查到“这条附件是谁上传的”。注意这里的“谁”需要精确到用户唯一标识而不是显示用的姓名。很多项目在附件表里只存了一个personName字段看起来能显示但一旦系统里有两个同名的人权限判断就会出现“张三能删张三的附件”这种说不清的尴尬。严格的做法是同时保留用户编码和用户姓名两个字段判断时以编码为准。第三个条件才是“删除动作的权限判定”。也就是说只有上传人本人操作时删除接口才放行其他人操作时要么根本不展示删除入口要么展示但点击后提示“无权删除”。这三个条件不是并列的关系而是一条链路上的三道关卡用户身份识别、附件归属识别、权限校验与执行。后面的实现方案实际上就是围绕这三步把代码落下去。1.3 影响面评估不只是个删除按钮的事我最初接手这个需求时以为改一个按钮的显隐就够了后来发现完全不是那么回事。这个需求牵扯到的面至少有三个层级。数据层面附件删除涉及物理删除和逻辑删除两种模式。物理删除直接把文件从磁盘上抹掉操作日志里只留下一条删除记录逻辑删除则是把附件表里的状态字段置为无效文件还留在存储里。在“只能删除自己上传的”这个前提下逻辑删除反而更有审计价值因为保留了上传人、删除人、删除时间等完整链路。这一点在方案设计时就得想清楚。服务层面删除接口不能只接收一个附件ID做delete操作它必须有“批量接收多个附件ID、逐条校验归属、返回每条删除结果”的能力。因为前端多选删除一次勾选的可能有自己传的也有别人传的接口返回必须能区分哪些删掉了、哪些没删掉、为什么没删掉。前端层面删除入口的展示策略要跟后端判定保持一致。如果后端限制了删除权限但前端每个附件后面还挂着删除按钮那用户一点按钮就弹一个“无权删除”体验会非常糟糕。好的做法是在附件列表加载完成后在当前用户已确定的前提下不渲染属于其他上传人的删除操作按钮或者把它置灰让用户不需要点到后端才知道“这不能删”。2. 原理拆解EOS8.3.3附件删除为什么会串权限2.1 附件控件的数据组织逻辑EOS8.3.3的附件控件本质上是一个“业务表 加 附件元数据表”的两层结构。业务表里的某一列存的是业务单据的主键ID而真正的文件信息也就是文件名、存储路径、大小、上传时间、上传人等都挂在附件信息表里通过serviceId或者billId这个外键关联回业务单据。在EOS平台上常见的附件信息表会设计成类似这样主键attachId关联业务ID的serviceId再配fileName、filePath以及uploadUser、uploadUserName这两个我们关心的归属字段。这个结构的本质含义是一份业务单据可以有多个附件每个附件都有独立的上传人信息。数据本身是有能力回答“这条附件属于谁”的。但问题恰恰出在附件控件的默认删除行为上。很多系统最初实现删除功能时写的是“传入附件ID数组按照ID直接执行DELETE”。这种写法完全没有考虑附件归属问题它只验证了“操作人能不能打开这个单据”而没有验证“操作人能不能删除这条附件”。这就是越权删除问题的根源所在。2.2 默认逻辑为什么拦不住跨用户删除我见过很多项目里的删除逻辑长这样前端从附件列表里勾选几条记录把attachId拼成一个数组POST到后端删除服务后端服务里一条update或者delete语句就处理完了。这中间缺了什么缺了“当前登录用户”与“附件上传用户”的比对环节。因为在EOS8.3.3的逻辑流开发方式里很多服务节点配置起来太方便了把“删除附件”和“保存操作日志”两个节点一连完事。开发同学很容易顺着这种思路直接把功能做完等到测试提出“能删别人的附件”时才意识到原来这个删除服务根本没做过鉴权。另外默认的附件列表查询往往不返回uploadUser字段到前端或者返回了但没有跟当前用户比对。前端拿到附件列表后每一行都是统一模板渲染出来的删除按钮人人都有点击删除就调接口接口又不管归属那权限自然就失控了。所以“为什么默认逻辑拦不住”本质上是两个原因后端删除接口的开发路径太短容易跳过校验前端列表对不同上传人的附件没有做差异化的操作入口渲染。2.3 控制方案设计前端交互 与 后端兜底正确做法是“前端做体验后端做兜底”的双保险思路。我不建议只在某一边做控制原因很简单只做前端用户根本不调用界面就调接口一样能删别人的文件只做后端用户会看到所有删除按钮点击后一个接一个弹权限错误提示体验极差。前端要做的是让“当前用户不能删的附件”不再出现可操作的删除按钮。这个判断属于展示层优化它在附件列表数据加载后执行核心是比较当前登录用户的身份标识和每行附件的uploadUser字段。后端要做的是对“任何到达删除接口的请求”都执行归属校验。即使在界面上看不到删除按钮攻击者或者普通用户通过改造请求、模拟接口调用依然可能把delete请求发送过来。后端如果直接按ID删那就等于把安全防线完全暴露了。后端删除逻辑的推荐形态是接收一个附件ID数组服务内部逐条判断归属能删的进可删除列表不能删的进无权限列表最后统一返回处理结果。前端拿到这个结果后根据情况刷新列表并给出清晰提示。这个方案从架构上看很朴素但确实是把权限边界守住了。前端管显示后端管安全二者缺一不可。3. 实操落地手写删除校验服务的完整步骤3.1 附件表结构设计要点先解决表结构的问题。如果项目还在设计阶段我强烈建议附件信息表里至少包含这几个字段attachId主键、serviceId业务关联ID、fileName文件名、filePath存储路径、uploadUser上传人用户编码、uploadUserName上传人姓名、uploadTime上传时间、fileSize文件大小、state状态Mark正常或已删除、deleteUser删除人编码、deleteTime删除时间。很多项目用到后面才发现只存uploadUserName不存uploadUser编码是给自己埋雷。用户编码是系统内唯一的姓名是可以重复的。删除权限判断时要用编码比对不要用姓名比对。如果项目表结构已经定型没有uploadUser字段那就必须加一个。我遇到过改造老表的情况历史数据里上传人是空值这种时候代码里要兼容上传人为空或者查不到上传人信息的附件默认只允许管理员删除普通用户无权删除。下面是一个参考的建表SQL字段粒度以实际工程为准create table APP_ATTACHMENT_INFO ( ATTACH_ID varchar2(32) primary key, SERVICE_ID varchar2(32), BIZ_TYPE varchar2(20), FILE_NAME varchar2(200), FILE_PATH varchar2(500), FILE_SIZE number, UPLOAD_USER varchar2(50), UPLOAD_USER_NAME varchar2(100), UPLOAD_TIME date, STATE varchar2(1) default 1, DELETE_USER varchar2(50), DELETE_TIME date );3.2 获取当前登录人的正确姿势在EOS8.3.3的Java服务开发里获取当前登录人没有统一的标准答案各项目差异挺大。常见的方式是通过会话对象取或者通过用户上下文工具类取。这里我给出一个相对通用的写法思路// 示意代码通过上下文或session获取当前登录用户编码 // EOS不同版本获取方式不同请结合项目实际用户体系调整 public String getCurrentLoginUser(SessionContext context) { // 方式一从session中取用户对象 Object userObj context.getUser(); if (userObj instanceof UserBaseInfo) { UserBaseInfo user (UserBaseInfo) userObj; return user.getUserId(); } // 方式二从线程上下文取 // return UserContextHelper.getCurrentUserId(); // 方式三从请求参数或会话属性取 Object userId context.getAttribute(currentUserId); return userId null ? : String.valueOf(userId); }这里有个很重要的经验不要相信前端传过来的“当前用户”也不能完全依赖前端传什么userId进来。正确做法是从后端会话或后端上下文中获取。前端传的值很容易被伪造一旦这个值被改整个权限判断就等于形同虚设。还有一个实际问题单点登录场景下用户信息可能不在session里而是通过加密票据在每次请求头中携带这个时候后端服务就需要从request中解析用户信息。我遇到过一个项目同一个系统内部两个子系统之间相互调用附件删除服务服务间调用不传用户信息结果所有操作都变成了系统管理员身份权限判断全部失效。后来我们在服务之间加了用户上下文传递机制才算彻底解决。3.3 删除权限校验Java实现后端校验的核心流程就三步批量查出附件归属、分组对比当前用户、只对归属于当前用户的附件执行删除。第一步不要用循环单条查询。如果前端一次勾选了上百条附件你循环里一条一条查数据库性能会很差而且会拖垮数据库连接。正确做法是一次性用IN查询列表把所有附件拿出来在内存里处理。第二步遍历附件列表把当前用户的userId跟每条附件的uploadUser比较。同时得考虑数据库中uploadUser存的是编码前端列表展示时拿到的也必须是编码。这里统一用字符串比较即可注意去除空格和大小写问题。第三步可删除的附件统一执行删除并记录操作日志。无权限的附件不删除组装到返回结果里。一个完整的Java实现示例import java.util.List; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; public class AttachmentDeleteService { private AttachmentInfoMapper attachmentInfoMapper; // 按权限批量删除附件 public MapString, Object deleteWithPermission(ListString attachIds, String currentUserId) { MapString, Object result new HashMap(); ListString canDeleteIds new ArrayList(); ListString deniedIds new ArrayList(); if (attachIds null || attachIds.isEmpty()) { result.put(success, false); result.put(message, 请选择要删除的附件); return result; } // 一次性查出这批附件的信息 ListAttachmentInfo attachments attachmentInfoMapper.selectBatchByIds(attachIds); // 用id映射附件信息便于快速判断 MapString, AttachmentInfo attachMap new HashMap(); for (AttachmentInfo att : attachments) { attachMap.put(att.getAttachId(), att); } for (String attachId : attachIds) { AttachmentInfo att attachMap.get(attachId); if (att null) { deniedIds.add(attachId); continue; } String uploadUser att.getUploadUser(); if (currentUserId ! null currentUserId.equals(uploadUser)) { canDeleteIds.add(attachId); } else { deniedIds.add(attachId); } } // 执行物理/逻辑删除 if (!canDeleteIds.isEmpty()) { attachmentInfoMapper.deleteBatchByIds(canDeleteIds); // 这里可以写操作日志 } result.put(success, true); result.put(deletedIds, canDeleteIds); result.put(deniedIds, deniedIds); result.put(message, deniedIds.isEmpty() ? 删除成功 : 部分附件删除成功另有 deniedIds.size() 个附件不是您上传的已被忽略); return result; } }这段代码里有两个设计值得说明。一是批量查询加内存映射避免N次数据库访问这是性能关键。二是返回结果里同时带deletedIds和deniedIds前端拿到之后可以做精确的界面反馈。删除动作本身也要分情况。如果平台支持逻辑删除也就是把STATE字段置为0我推荐优先用逻辑删除。为什么因为需求点是“只能删除自己上传的”业务上强调的是权限边界审计上则强调“谁删了什么”。逻辑删除的deleteUser和deleteTime字段能把这条链路完整串起来。万一以后要追溯还能从库表里捞出真凭实据。3.4 前端删除按钮的显隐与提示前端部分需要区分传统的JSP页面开发和前后端分离两种模式。但不管哪种模式核心逻辑是一致的在附件列表渲染完成后通过比对当前登录用户编码和每条附件的uploadUser编码决定是否显示删除按钮。如果用的是EOS8.3.3的JSP页面开发页面通过标签库渲染附件列表时可以遍历附件列表在有权限删除的行里才输出删除链接。注意这里的“当前登录人”要从后端上下文中取渲染时就要把这个值跟附件列表数据一起放进页面作用域里不能等到提交时才取。如果用的是前后端分离的Vue或React页面那么后端接口查询附件列表时除了返回fileName、uploadTime这些常规字段还必须额外返回两个字段uploadUser和currentUserId。前端拿到列表后为每条附件增加一个canDelete布尔属性比较这两个字段是否相等随后在模板中用canDelete控制按钮显隐。不要试图让前端猜当前用户是谁要由后端明确告知。删除按钮隐藏时有两种处理策略一是完全不渲染按钮用户看不到也就不操作二是渲染一个置灰的按钮鼠标悬停显示“只能删除自己上传的附件”。从业务理解角度看第二种更友好但第一种更直接。我个人的经验是对内使用的系统用“不渲染”的干净策略对用户需要理解规则的场景用“置灰加提示”的策略。还建议在附件列表头部加一个提示语说明“每人只能删除自己上传的附件”。这个提示语虽然很不起眼但能减少大量因用户不理解权限规则而产生的工单咨询。系统使用者的认知预期一旦建立起来后续前端提醒、后端报错都显得理所当然。4. 避坑指南附件删除权限控制的问题排查与经验4.1 高频问题排查速查表在这个需求的实现与测试过程中我整理过一张问题速查表。下面这些问题是出现问题概率最高的按频率排序问题现象直接原因处理方向所有人都能删除附件删除接口未做归属校验后端按“查-判-删”流程重写前端渲染无删除按钮但调接口仍可删除只做了前端控制后端仍直接删拦截接口请求增加校验逻辑当前用户获取不到服务报空指针会话失效或单点登录无用户信息排查用户上下文的获取链路自己上传的附件也提示无权限uploadUser字段为空或值不对补历史数据或兼容空值处理删除时同名不同人的文件被一起删了上传人使用姓名而非编码判断统一改为用用户编码比较大量附件删除时页面卡顿或超时循环单条查库N1查询改为批量IN查询与内存处理逻辑删除后文件又出现在列表查询列表未过滤已删除状态列表查询增加STATE过滤条件前端展示时有权限判断删后列表不刷新前端未根据返回结果刷新列表在callback中重新加载附件列表这八类问题我基本都在不同项目里碰过。其中最隐蔽的是第二种前端看着没问题按钮都不显示了但安全测试时通过Postman模拟请求还是把别人的附件删掉了。这次以后我彻底养成了习惯前端只管体验后端永远要当最后一道门。4.2 两个真实踩坑案例第一个案例是关于“上传人字段存了姓名而不是编码”。项目初期开发同学图省事建表时没做uploadUser编码字段只存了uploadUserName。上线后正好有两个叫“王强”的用户一个在财务部一个在业务部。业务单据上财务部的王强传了预算附件业务部的王强刚好有这张单据的查看编辑权限系统判断uploadUserName都叫“王强”就让他把附件删了。测试环境数据少没暴露到生产环境就出事故了。排查时发现附件表里根本没有可区分身份的唯一编码字段只能靠姓名硬抗。这个教训直接推动我们补了uploadUser编码字段并把历史数据清洗了一遍。第二个案例是逻辑删除和物理删除混用的问题。某个子系统用的逻辑删除另一个子系统因为磁盘空间压力用了物理删除。文件被“逻辑删除”后磁盘上文件还在但如果另一条审批流程关联了这个附件调用文件预览时直接404。问题出现在一个多附件列表里A附件被逻辑删除了B附件是别人传的A附件上传人自己删A时也把B的文件目录一块清了。排查到后来发现是物理删除服务直接把整个业务ID对应的附件目录全删了。从那以后我立了一条规矩所有附件删除统一走同一个服务绝不允许多条删除路径并存并且强烈建议改用逻辑删除从代码层面杜绝“清目录”这种粗暴操作。4.3 场景扩展与“查看权限”“下载权限”联动最后提一个场景扩展。很多系统在做“只能删除自己上传的”时其实还连带着要处理“谁能看、谁能下载”的问题。附件区的权限需求往往是三件套查看权限、下载权限、删除权限。这次只解决了删除但下一次需求很可能就是“我传的附件别人能不能下载”。我的建议是在设计附件权限时就把三个权限字段一起考虑了。数据模型上上传人是一个天然归属标识围绕它可以在功能上做很多控制比如“附件跨流程归档”、“附件水印控制”、“附件有效期控制”。即使这次只做删除控制也最好把归属字段、操作人字段、状态字段先固化好后续扩展不用再动表结构。控制逻辑上可以做一个权限判断工具类入参是当前用户、业务ID、附件ID、操作类型出参是是否有权限。这个工具类把判断逻辑收敛在一起后续加“下载权限”时只需要在工具类里加对应枚举分支。写一次代码能管好多次需求。我自己在项目里就是这么干的既能应对本次需求又方便后续扩展。