附件删除权限这件事,在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 Map<String, Object> deleteWithPermission(List<String> attachIds, String currentUserId) { Map<String, Object> result = new HashMap<>(); List<String> canDeleteIds = new ArrayList<>(); List<String> deniedIds = new ArrayList<>(); if (attachIds == null || attachIds.isEmpty()) { result.put("success", false); result.put("message", "请选择要删除的附件"); return result; } // 一次性查出这批附件的信息 List<AttachmentInfo> attachments = attachmentInfoMapper.selectBatchByIds(attachIds); // 用id映射附件信息,便于快速判断 Map<String, 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字段为空或值不对 | 补历史数据或兼容空值处理 |
| 删除时同名不同人的文件被一起删了 | 上传人使用姓名而非编码判断 | 统一改为用用户编码比较 |
| 大量附件删除时页面卡顿或超时 | 循环单条查库,N+1查询 | 改为批量IN查询与内存处理 |
| 逻辑删除后文件又出现在列表 | 查询列表未过滤已删除状态 | 列表查询增加STATE过滤条件 |
| 前端展示时有权限判断,删后列表不刷新 | 前端未根据返回结果刷新列表 | 在callback中重新加载附件列表 |
这八类问题,我基本都在不同项目里碰过。其中最隐蔽的是第二种:前端看着没问题,按钮都不显示了,但安全测试时通过Postman模拟请求,还是把别人的附件删掉了。这次以后,我彻底养成了习惯:前端只管体验,后端永远要当最后一道门。
4.2 两个真实踩坑案例
第一个案例是关于“上传人字段存了姓名而不是编码”。项目初期开发同学图省事,建表时没做uploadUser编码字段,只存了uploadUserName。上线后正好有两个叫“王强”的用户,一个在财务部,一个在业务部。业务单据上财务部的王强传了预算附件,业务部的王强刚好有这张单据的查看编辑权限,系统判断uploadUserName都叫“王强”,就让他把附件删了。测试环境数据少没暴露,到生产环境就出事故了。排查时发现附件表里根本没有可区分身份的唯一编码字段,只能靠姓名硬抗。这个教训直接推动我们补了uploadUser编码字段,并把历史数据清洗了一遍。
第二个案例是逻辑删除和物理删除混用的问题。某个子系统用的逻辑删除,另一个子系统因为磁盘空间压力用了物理删除。文件被“逻辑删除”后,磁盘上文件还在,但如果另一条审批流程关联了这个附件,调用文件预览时直接404。问题出现在一个多附件列表里,A附件被逻辑删除了,B附件是别人传的,A附件上传人自己删A时也把B的文件目录一块清了。排查到后来发现是物理删除服务直接把整个业务ID对应的附件目录全删了。从那以后我立了一条规矩:所有附件删除统一走同一个服务,绝不允许多条删除路径并存,并且强烈建议改用逻辑删除,从代码层面杜绝“清目录”这种粗暴操作。
4.3 场景扩展:与“查看权限”“下载权限”联动
最后提一个场景扩展。很多系统在做“只能删除自己上传的”时,其实还连带着要处理“谁能看、谁能下载”的问题。附件区的权限需求往往是三件套:查看权限、下载权限、删除权限。这次只解决了删除,但下一次需求很可能就是“我传的附件别人能不能下载”。
我的建议是,在设计附件权限时就把三个权限字段一起考虑了。数据模型上,上传人是一个天然归属标识,围绕它可以在功能上做很多控制,比如“附件跨流程归档”、“附件水印控制”、“附件有效期控制”。即使这次只做删除控制,也最好把归属字段、操作人字段、状态字段先固化好,后续扩展不用再动表结构。
控制逻辑上可以做一个权限判断工具类,入参是当前用户、业务ID、附件ID、操作类型,出参是是否有权限。这个工具类把判断逻辑收敛在一起,后续加“下载权限”时只需要在工具类里加对应枚举分支。写一次代码,能管好多次需求。我自己在项目里就是这么干的,既能应对本次需求,又方便后续扩展。