基于Spring MVC注解与拦截器的通用数据权限校验框架设计与实践
做后端开发这些年凡是牵涉到多角色、多组织架构的项目基本都会碰到同一类需求同一个查询接口不同身份的人进来返回的数据范围完全不同。比如基层销售打开订单列表只能看到自己名下那几单区域经理能看到整个大区的业绩总部的运营能看到全部数据。早先的项目里这种逻辑东一个if西一个else地散落在Service层、Mapper层换个人来维护基本靠猜。后来我基于Spring MVC做了一套通用数据权限校验框架把这类数据可见范围的判断从业务代码里彻底抽了出来用注解加拦截器的方式统一处理。这篇文章就是这套框架的使用示例从设计思路到接入步骤再到我踩过的坑全部摊开来讲。1. 为什么数据权限校验值得单独做成框架——三条路线的取舍1.1 认证、鉴权与数据权限三件经常被混淆的事先花一点篇幅把概念捋清楚因为太多人把这三件事混在一起聊导致设计出来的东西四不像。登录认证解决的是你是谁的问题通常用Token、Session或者OAuth协议完成确认请求发起者的身份。能力鉴权解决的是你能不能执行这个操作的问题比如这个用户有没有调用删除订单接口的权限一般用Shiro、Spring Security那套角色权限模型来做。而数据权限解决的是另一个维度的问题你确实有资格访问订单模块但你能看到订单表里的哪几行门店店长只能看本门店订单大区总监能看整个大区财务能看到所有已支付订单——这就是行级数据可见性的差异。数据权限的实现难度远高于认证和鉴权。认证是全局性的做完一次就大家共享而数据权限和业务模型深度耦合不同模块的可见范围定义方式完全不同。更麻烦的是它几乎散布在每一个查询接口里。如果不用框架把它收拢起来代码里就会长满if-else的野草判断一下角色拼一段SQL再判断一下部门又拼一段SQL。1.2 三条主流的落地路线分别适合什么项目我在多个项目里实际试过不同的方案总结下来主流路线有三条。第一条路线业务代码里硬编码。Service层里写满角色判断不同角色拼出不同查询条件。优点是实现最快新人对代码逻辑完全透明缺点是接口一多就彻底失控上百个查询方法里到处是重复的条件拼接代码而且新增一个数据维度要动几十个方法改一处漏一处。第二条路线在ORM层用自己的拦截器动态改写SQL。比如用MyBatis的Interceptor机制在SQL执行前拦截解析SQL语句在WHERE后面自动追加数据权限条件。这个方案做到了彻底无侵入业务代码完全不用改。但代价是你得自己写一套SQL语法解析得处理子查询、JOIN、GROUP BY各种复杂场景稍微考虑不周就会改坏语义。调试的时候更是灾难线上SQL和执行计划被框架偷偷改了排查问题像在迷宫里走路。除非团队有很强的SQL解析功底否则不建议自己造这个轮子。第三条路线Spring MVC应用层拦截器配合注解和参数透传。Controller方法上标注数据权限规则注解拦截器在执行方法前解析注解、生成SQL条件片段放入框架上下文业务代码在查询时主动取出片段拼到查询条件里。这条路不侵入SQL解析保留了业务代码的可读性和可控性注解又能把规则显式表达出来是最稳妥的折中方案。下面这个表格是我在选型时整理的核心对比方案侵入性实现成本调试难度适用场景硬编码if-else高条件散在业务方法低低接口少、规则万年不变的小项目ORM层SQL改写极低业务完全无感极高要写SQL解析器高SQL被悄悄改掉海量接口、团队具备SQL解析能力Spring MVC拦截器注解低业务处取一次条件中一套框架代码低规则在注解上显式可见大多数中大型业务系统1.3 为什么我最终落在Spring MVC 注解 拦截器这套组合上我最终选择第三套组合主要理由有三点。第一Spring MVC的HandlerInterceptor天然能拿到HandlerMethod对象也就意味着能拿到方法上的注解。这个特性让根据注解自动附加数据权限在实现层面变得顺理成章不需要任何反射扫描和路由匹配的黑魔法。第二注解的表达能力刚好和业务规则的复杂度匹配。数据权限规则说到底就是哪个字段、用什么操作符、等于什么值比如sales_id 当前用户ID、dept_id IN (当前部门及其下级部门)。这类规则用注解属性描述非常清晰比在代码里写一大段条件判断可读性高太多。第三这套方案的排查链路是完整的。后端的同事接手时打开Controller就能看到这个接口有哪些数据权限规则打开解析器就能看到规则如何翻译成SQL条件。出了问题按图索骥不会像SQL改写方案那样两眼一抹黑。2. 通用框架的整体设计与核心组件2.1 四个核心组件注解、拦截器、上下文、解析器框架的整体设计我拆成四个核心组件各司其职通过SPI机制串联起来。第一个是数据权限注解。它标注在Controller方法上声明这个接口的数据可见范围规则。为了让规则可读、可组合我把注解设计成规则数组一个接口可以配置多条规则按顺序生效。注解本身不干活它只是规则元数据的载体。第二个是数据权限拦截器。它实现HandlerInterceptor在preHandle阶段读取Controller方法上的注解然后把规则分发给解析器处理。拦截器是整个框架的调度中心不关心具体业务规则只负责流程编排。第三个是数据权限上下文。它保存当前请求的用户身份、角色、组织ID等信息。实现上我用ThreadLocal保存保证一次请求内各个模块都能方便地拿到当前用户的基础信息。用户信息从哪来从登录态里解析登录成功后把用户对象塞进上下文框架在请求结束后必须清理否则会串数据。第四个是数据权限解析器。它把注解上的规则翻译成真正的SQL条件。我设计成接口业务方可以按维度实现自己的解析器。比如自己创建的数据是一个解析器本部门及下级部门的数据是另一个解析器全部数据又是一个解析器。解析器返回的是结构化的SQL条件对象包含条件表达式和参数列表。组件之间的关系可以这样理解拦截器是工厂的调度员注解是工单上下文是原材料的仓库解析器是各个工位的工人。工单来了调度员按工单类型分派给对应工人工人从仓库取原材料加工出成品——一段可执行的SQL过滤条件。2.2 数据权限条件如何从Controller传递到Mapper这是设计里最容易翻车的地方我在初版就吃过亏。最开始我试图让拦截器把条件写进请求参数里这样Controller方法的入参对象就能直接带着条件走。结果发现两个问题一是请求参数里的值用户也可以伪造把数据权限条件强行覆盖成11等于把整个过滤规则绕过去了二是Spring MVC的参数绑定发生在拦截器之后我在preHandle里修改的参数能不能生效完全取决于参数解析器的行为不同版本的Spring行为还不一样不可控。后来我改成ThreadLocal传递 业务侧主动获取模式。拦截器把最终解析出的条件对象放进数据权限上下文的ThreadLocal中业务代码在Service或Mapper层需要使用时主动从上下文里取一次拼接进查询条件。代码上看是这样的// 业务层查询代码 public PageResultOrderVO page(OrderQuery query) { DataScopeCondition condition DataScopeContext.current().conditionFor(order); return orderMapper.page(query, condition); }这个设计的关键在于条件由框架生成、存放在框架上下文业务代码只是消费者。用户即使传一个dataScopeCondition参数也只会被当成普通业务参数不会影响框架上下文的真实条件。安全性要靠上下文隔离来保证不能靠参数名约定。2.3 两个重要的设计决策解析器SPI与参数化防御第一个决策是解析器必须有SPI扩展点。数据权限的维度是无穷多的按用户、按部门、按区域、按客户分组、按合同签订人、按专属运营人员……几乎没有两个项目的维度是相同的。如果框架把解析器写死那它就不是通用框架只是个工具函数。我把它定义成接口后新接一个项目时只需要实现对应的维度解析器然后注册进去框架本身完全不用动。第二个决策是所有解析器生成的SQL条件必须参数化。直白地说条件里面不能直接拼接用户输入的值。比如sales_id #currentUserId解析后要生成的是参数绑定形式的语句而不是把用户ID直接填进SQL字符串里。这个点在示例里我用${}做演示是为了便于阅读真正生产环境的框架里一定是对象化的条件传递最终通过MyBatis的参数列表绑定到SQL上防止SQL注入。3. 核心使用示例三种典型数据范围配置3.1 引入依赖与基础配置框架本身不打业务包它是一组框架代码集成在你的项目里需要两个基础配置拦截器注册和解析器装配。拦截器注册是在Spring MVC配置里加一个WebMvcConfigurer实现Configuration public class WebMvcConfig implements WebMvcConfigurer { Resource private DataPermissionInterceptor dataPermissionInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(dataPermissionInterceptor) .addPathPatterns(/**); } }这里有个细节需要提前说明拦截器不能直接new必须作为Spring Bean注入否则拦截器内部的解析器列表、上下文初始化这些都拿不到。我一开始图省事直接在addInterceptors里new了一个结果拦截器里的Spring依赖全是null排查了半天。解析器装配我做成Spring容器自动收集的方式框架定义一个顶层接口DataScopeParser业务方写好的解析器用Component注册框架在启动时自动收集所有实现类构建一个维度到解析器的映射表。后续新增维度就是纯加类不用改框架代码。数据权限注解的定义大概是这样的Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequiresDataPermission { // 规则列表支持一个接口配置多条规则 DataScopeRule[] value() default {}; } public interface DataScopeRule { // 维度标识解析器根据它来匹配处理逻辑 String dimension(); // 数据范围字段比如sales_id、dept_id、org_id String field(); // 操作符EQ、IN、NOT_IN Op op() default Op.EQ; // 值的表达式#currentUserId、#currentUserDeptId、固定值 String value(); }使用的时候接口方法上加上注解即可RequiresDataPermission( DataScopeRule(dimension self, field sales_id, op Op.EQ, value #currentUserId) ) GetMapping(/orders) public PageResultOrderVO pageOrders(OrderQuery query) { return orderService.page(query); }3.2 场景一用户只能看到自己创建的订单这是最基础、最常见的需求销售登录系统后订单列表只展示自己名下负责的订单。第一步在Controller方法上加注解规则是订单表sales_id字段等于当前登录用户的ID。第二步实现self维度的解析器Component public class SelfDataScopeParser implements DataScopeParser { Override public String dimension() { return self; } Override public DataScopeCondition parse(DataScopeRule rule, DataPermissionContext context) { // 从上下文中取出当前登录用户ID Long currentUserId context.getCurrentUser().getId(); return DataScopeCondition.builder() .sql(rule.field() ?) .param(currentUserId) .build(); } }第三步在Mapper的SQL里消费这个条件。借助MyBatis的条件拼接把它加到查询声明里去select idpageOrders resultTypeOrderVO SELECT * FROM sales_order WHERE deleted 0 if testcondition ! null AND ${condition.sql} /if /select实际的执行效果就是普通销售查询订单时后台自动多出一个过滤条件。前端页面完全无感知网络请求的SQL参数列表里多了框架附加的参数。这个规则同时适用于订单分页列表、订单导出、订单统计等所有带注解的接口一处配置全面生效。这里有一个使用上的认识需要纠正数据权限条件不是写死在某个查询里的它是动态生成的。同一个Controller方法销售A进来时条件是sales_id 1001销售B进来时条件是sales_id 1002完全取决于当前登录用户是谁。所以条件必须从上下文动态获取不能写死。3.3 场景二部门维度——本部门及下级部门的数据现实中更多的情况是按组织架构管数据销售只能看本部门的数据部门主管能看本部门以及所有下级部门的。这个场景下数据权限规则表达为部门ID在本部门及其所有下级部门ID集合中。难点在于下级部门集合的获取不同系统组织架构表的表结构不一样我在框架里预留了自定义解析器的空间。假设组织架构表是sys_dept有经典的parent_id自关联。dept维度的解析器实现Component public class DeptDataScopeParser implements DataScopeParser { Resource private DeptMapper deptMapper; Override public String dimension() { return dept; } Override public DataScopeCondition parse(DataScopeRule rule, DataPermissionContext context) { Long currentDeptId context.getCurrentUser().getDeptId(); // 递归查询本部门及所有下级部门ID ListLong deptIds deptMapper.selectChildrenIds(currentDeptId); deptIds.add(currentDeptId); return DataScopeCondition.builder() .sql(rule.field() IN ( placeholders(deptIds.size()) )) .params(deptIds) .build(); } }对应的Controller方法注解变成RequiresDataPermission( DataScopeRule(dimension dept, field dept_id, op Op.IN, value #currentUserDeptId) ) GetMapping(/orders) public PageResultOrderVO pageOrders(OrderQuery query) { return orderService.page(query); }这段实现里有三个小细节值得注意。第一个查询下级部门集合时我用递归查询如果组织架构特别深、节点特别多建议在部门表里增加path字段做前缀匹配把递归查询优化成一次LIKE查询。第二个IN的数量不能无限膨胀如果下级部门数量超过几百上千个要把IN改写成EXISTS子查询把先查部门集合再匹配变成边查边匹配。第三个框架处理嵌套查询时要小心部门ID集合查询不要隐藏数据权限。部门维度递归查询的是组织架构表这是元数据表正常不需要再叠加数据权限。3.4 场景三同一接口多角色不同规则业务中最烦人的是同一个接口不同角色进去数据范围完全不同。以前是写一大堆if-else有了框架之后这个逻辑被收敛成角色到规则的映射表。我把DataPermissionContext里增加一个角色属性。解析器在解析前先看当前用户角色然后根据角色决定走哪条规则。比如订单列表销售走self规则部门主管走dept规则运营总监走all规则。直接改解析器的调度逻辑框架提供角色匹配的能力后再来写业务解析器Component public class OrderDataScopeParser implements DataScopeParser { Override public String dimension() { return order; } Override public DataScopeCondition parse(DataScopeRule rule, DataPermissionContext context) { User currentUser context.getCurrentUser(); if (currentUser.isSales()) { // 销售只看自己 return new Condition(sales_id ?, currentUser.getId()); } if (currentUser.isDeptManager()) { // 部门主管看本部门及下级 ListLong deptIds loadDeptTreeIds(currentUser.getDeptId()); return new Condition(dept_id IN (...), deptIds); } // 运营总监看全部数据 return Condition.unrestricted(); } }Controller方法只需要声明一个维度为order的规则具体怎么按角色分支全部收敛在解析器里。这样做的好处是业务方法里一行条件判断代码都没有后续新增角色时不需要动Controller和Service只需要在解析器里加一个分支测试范围也能收敛到解析器的单元测试上。4. 拦截器怎么执行规则注解扫描、表达式求值与兜底逻辑4.1 preHandle阶段的事件序列到这一步有必要把拦截器内部的流程完整拆开因为它决定了框架在上线后所有请求的执行路径。拦截器的preHandle方法主要按下面这个序列执行。第一步判断当前请求对应的HandlerMethod是否为Controller方法。Spring MVC的拦截器会拦截到静态资源和错误页面的处理对象不是HandlerMethod时直接放行。第二步反射读取方法上的RequiresDataPermission注解。没有该注解的接口直接放行——这是框架设计的默认策略不显式声明数据权限规则的接口不做任何附加过滤避免拦截器把老接口全部误伤。第三步初始化数据权限上下文。从当前的登录用户信息构建上下文对象在这一步会确认用户是否已登录、角色是否可用。如果是未登录但接口又标了注解走到这里会直接抛出未认证异常。这个行为可以配置成重定向到登录页或者返回401看项目约定。第四步遍历注解中的规则列表根据规则里的维度值找到匹配的解析器。解析失败有兜底见4.3。第五步把解析器返回的所有条件对象组装成一个条件集合放入ThreadLocal的数据权限上下文。如果业务代码的调用链里有其他拦截器或者ControllerAdvice需要读取条件也可以从这里取。第六步请求结束时清理上下文避免线程复用时数据串掉。这套流程的代码骨架是这样的Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (!(handler instanceof HandlerMethod handlerMethod)) { return true; } RequiresDataPermission annotation handlerMethod.getMethodAnnotation(RequiresDataPermission.class); if (annotation null) { return true; } DataPermissionContext context DataPermissionContext.initFromRequest(request); try { ListDataScopeCondition conditions dispatchRules(annotation.value(), context); context.setConditions(conditions); DataScopeContextHolder.set(context); return true; } catch (DataScopeException e) { handleException(response, e); return false; } }4.2 规则值表达式的解析固定值、动态值与内置函数规则注解里的value字段是我特意留下的一个灵活口子它不只是简单的字面量。这里我实现了一套极简的表达式语法主要支持三类取值。第一类是固定值。比如value GT10001表示这个接口的客户分组过滤条件固定为组ID等于GT10001一般用于后台系统内固定范围的管理视图。第二类是动态取值用井号引用上下文中的属性。比如#currentUserId取当前用户ID#currentUserDeptId取当前用户部门ID#currentUserRoleIds取当前用户的角色ID列表。解析器根据这些占位符动态填充条件参数这是最常用的取值方式。第三类是内置函数调用。比如#deptTreeIds()会调用框架内置的组织架构树查询函数并缓存当前请求的首次查询结果避免多个规则重复递归查库。这个表达式解析器的实现不复杂本质上就是正则匹配占位符然后从上下文里通过反射读取值。真正的复杂性在于语义约定框架只负责把表达式中的标记替换为真实数据至于deptTreeIds怎么查、IN条件怎么拼那是业务解析器需要考虑的。4.3 兜底策略什么情况放行什么情况拦截兜底策略是框架安全性的最后一道防线我花了比较多心思来设计。无注解接口一律放行这是默认值目的是不对存量接口产生影响。有注解但上下文里没有当前用户直接拒绝并返回401。有注解但找不到对应维度的解析器返回500并给出明确的错误日志例如data scope parser not found for dimension: xxx防止规则被静默吞掉——数据权限规则如果失效结果往往是越权数据泄露宁可报错也不能无声放行。还有一个容易被忽视的情况规则表达式解析失败。比如注解里写了一个不存在的占位符#currentUserXxx解析器发现不了对应属性这时候我选择记录WARN日志并放行该规则。为什么是放行而不是拦截因为这类错误通常是开发期的配置笔误如果直接拦截会影响整个接口的可用性。但生产环境建议把这个开关调成严格模式解析失败直接失败由监控告警发现。// 上下文构造入口从认证信息构建 public static DataPermissionContext initFromRequest(HttpServletRequest request) { Authentication auth SecurityUtils.getAuth(); if (auth null) { throw new DataScopeException(未认证用户不能访问带数据权限约束的接口); } User user (User) auth.getPrincipal(); return DataPermissionContext.builder() .userId(user.getId()) .deptId(user.getDeptId()) .roleCodes(user.getRoleCodes()) .build(); }5. 跑通示例之后的生产级避坑记录5.1 拦截器实例化问题直接new出来的拦截器没有依赖我在第一版集成示例时图省事直接在WebMvcConfigurer里new了一个DataPermissionInterceptor。结果启动后请求一进来就报空指针排查半天发现拦截器内部的解析器列表是空的——当然空因为new出来的对象根本不归Spring管理Spring不会自动注入解析器列表。正确做法是把拦截器声明成Spring Bean然后注入配置类。如果你的项目用了Spring Boot还可以利用构造器注入强制依赖完整。这一点在给团队写接入文档时一定要用加粗字体标出来因为几乎每个新人都犯过这个错。另一个关联的坑WebMvcConfigurer的加载时机和拦截器Bean的初始化顺序。如果你的拦截器依赖了某些晚初始化的Bean可以考虑用Lazy注入或者ObjectProvider延迟查找避免启动时循环依赖。我通常把拦截器设计成对依赖采用懒加载策略。5.2 异步线程拿不到数据权限上下文ThreadLocal的值默认不跨线程传递。遇到Async异步方法、线程池提交的任务子线程里调用DataScopeContext.current()拿到的要么是null要么是上一个任务的残留值。第一个问题是null解决办法是包装任务把主线程的上下文快照传给子线程。Spring Boot的ThreadPoolTaskExecutor支持TaskDecorator可以在任务提交时自动包装Bean(asyncExecutor) public ThreadPoolTaskExecutor asyncExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setTaskDecorator(runnable - { DataPermissionContext snapshot DataScopeContextHolder.get(); return () - { try { DataScopeContextHolder.set(snapshot); runnable.run(); } finally { DataScopeContextHolder.clear(); } }; }); return executor; }第二个问题是残留。线程池里的线程是复用的如果子线程跑完不清理下一次任务进来就会带着上一个用户的权限上下文造成数据越权。必须保证finally清理。这块没有捷径只能靠代码审查和一整套测试用例兜住。5.3 分页插件与数据权限条件的拼接顺序MyBatis项目里分页插件非常常见。分页插件的工作原理是在Executor执行前拦截SQL拼上LIMIT语句再执行COUNT查询。这个顺序和数据权限条件有关系吗有而且非常关键。如果数据权限条件是靠业务层取出后拼进查询参数对象再传给Mapper的那分页插件执行时这个条件已经在参数对象里了顺序天然正确。但如果在查询参数对象里放的是一个额外的list分页插件对COUNT查询时不一定能带上这个参数COUNT出来的总条数就是全表条数和实际分页数据对不上。我的处理方式是把数据权限条件统一放在Mapper入参对象的一个固定属性里并且在XML的SQL中直接引用这个属性来拼接WHERE子句。这样COUNT查询和数据分页查询执行的是同一个SQL前缀统计口径就一致了。如果你用的是MyBatis-Plus的QueryWrapper需要注意数据权限条件不要和Wrapper的条件拆开传给插件否则插件改写的SQL可能只保留了Wrapper中的条件。5.4 单测时如何模拟当前用户上下文数据权限框架的上下文在拦截器里构造单元测试没有经过拦截器所以测试方法里DataScopeContext.current()很可能拿不到东西。写单测时不要试图去new一个MockMvc然后走完整链路那是集成测试的事更轻量的做法是在测试里手动构造上下文。BeforeEach void setUp() { User testUser User.builder() .id(1001L) .deptId(10L) .roleCodes(List.of(SALES)) .build(); DataPermissionContext context DataPermissionContext.builder() .userId(testUser.getId()) .deptId(testUser.getDeptId()) .roleCodes(testUser.getRoleCodes()) .build(); DataScopeContextHolder.set(context); } AfterEach void tearDown() { DataScopeContextHolder.clear(); }这套模拟方案最关键的是AfterEach里必须清掉上下文。测试框架里线程复用上一个用例的上下文不清掉下一个用例拿到的用户ID就会串掉轻则断言失败重则隐藏数据越权bug。数据权限框架本身是安全敏感组件测试用例设计时一定要覆盖不同角色访问同一接口产生不同过滤结果这类场景。6. 再进一步数据权限框架的延伸玩法6.1 关联表权限从单表条件到EXISTS子查询很多需求里的数据权限不只在当前主表的字段上。比如只能看自己有权限的客户下的订单——订单表里没有客户归属人字段客户归属关系在customer表里。这时候单字段条件就不够用了需要在解析器里生成一个EXISTS子查询条件。Component public class CustomerScopeParser implements DataScopeParser { Override public DataScopeCondition parse(DataScopeRule rule, DataPermissionContext context) { Long userId context.getCurrentUser().getId(); String sql EXISTS (SELECT 1 FROM customer c WHERE c.id sales_order.customer_id AND c.owner_id ?); return DataScopeCondition.builder().sql(sql).param(userId).build(); } }这样的条件片段可以和其他单表条件一起叠加框架不用区分到底是单表字段还是关联表子查询统一作为SQL条件片段处理就行。但从维护性角度我建议在注解里把维度名写清楚比如用customerOwned这样的维度标识一看就知道归属逻辑。6.2 规则缓存与元数据缓存注解的元数据反射在每次请求都要执行量大了以后性能会有可感知的损耗。框架内部可以加一层缓存用方法对象注解对象作为Key把解析结果缓存起来。注意这里的缓存不能缓存SQL条件的参数值——参数值是每个用户、每个请求都不同的但这个接口有哪些规则、规则类型是什么这类元数据是固定的。我用一个简单的本地缓存实现private final MapMethod, ListDataScopeRule annotationCache new ConcurrentHashMap(); Override public boolean preHandle(...) { ListDataScopeRule rules annotationCache.computeIfAbsent( handlerMethod.getMethod(), m - { RequiresDataPermission annotation m.getAnnotation(RequiresDataPermission.class); return annotation ! null ? List.of(annotation.value()) : List.of(); } ); // 后续流程... }6.3 多租户叠加租户字段不是数据权限聊一个很容易混淆的点多租户的租户隔离和数据权限两者虽然都要加WHERE条件但语义完全不同。租户隔离是物理上的数据边界租户A无论如何都不该看到租户B的数据数据权限则是在同一租户内部按照身份和组织角色做进一步的可见性限制。所以这两个机制应该分开处理租户条件由多租户插件或者每次查询的显式参数保证数据权限条件由框架生成。叠加使用时先过滤租户再叠加数据权限不要把它们混在同一个规则体系里否则排查问题时很难判断一个过滤条件是谁加上的。我在实际落地中遇到过有人尝试把租户ID写成dimension tenant的规则。短期看能用但长期看隐患很大——租户ID一旦配置错或者解析器漏加涉及的就是跨租户数据泄露比一般数据权限问题严重得多。还是建议两者彻底分离。再说一个性能优化方向如果数据权限规则涉及的部门树、层级结构固定不变可以考虑缓存部门树集合。部门树的变更频率远低于查询频率用定时任务或者简单的时间过期策略刷新缓存可以省掉大量重复递归查询。最后说点实在话。这套框架我在两个项目里落地最明显的收益不是代码量减少而是数据权限逻辑终于有了一个确定的存放位置。以前问订单列表的权限判断写在哪答案可能是某个Service方法里、某个XML文件里、甚至某个前端按钮的disabled逻辑里现在答案只有一个看Controller方法上的注解看对应维度的数据权限解析器。新增一个数据维度就新增一个解析器类流程稳定可控。如果你也在被数据权限的散乱逻辑折磨不妨照着这个思路抽一版框架出来第一版不需要设计得很宏大先把注解拦截器上下文解析器这条主链路跑通后面再慢慢补维度。

相关新闻

Python天气预测毕业设计:爬虫、LSTM与可视化完整实战

Python天气预测毕业设计:爬虫、LSTM与可视化完整实战

简介:基于Python的天气预测和可视化项目,是一套面向计算机专业毕业设计、课程设计与期末大作业的高分完整方案,评审成绩达99分,代码完整可运行,适合从零开始实战项目的学生。压缩包共38个文件,包含Python源…

2026/10/13 1:28:07 阅读更多 →
PNN在变压器故障诊断中的小样本鲁棒应用

PNN在变压器故障诊断中的小样本鲁棒应用

简介:本资源是一套基于MATLAB实现的概率神经网络(PNN)用于变压器故障诊断的完整实践方案,面向电力系统自动化、智能运维及人工智能应用方向的本科生、研究生与工程技术人员。项目聚焦于利用PNN对变压器运行状态进行分类识别&#…

2026/10/13 1:28:07 阅读更多 →
嵌入式转机器人岗位:简历筛选逻辑与技术栈补全指南

嵌入式转机器人岗位:简历筛选逻辑与技术栈补全指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 1:27:07 阅读更多 →

最新新闻

Reclilisib:从PI3K激动剂到选择性辐射保护剂的双重角色

Reclilisib:从PI3K激动剂到选择性辐射保护剂的双重角色

做肿瘤放疗研究的朋友应该都有过这种纠结:治疗窗口太窄,射线过去,肿瘤细胞和正常组织一起遭殃。我们这些年一直在找"选择性辐射保护剂"——最好能保护正常细胞,却不给肿瘤细胞留活路。我第一次注意到Reclilisib&#xf…

2026/10/13 2:18:27 阅读更多 →
MFC连接MySQL实战:ODBC封装、CRUD与避坑手册

MFC连接MySQL实战:ODBC封装、CRUD与避坑手册

简介:这是一套面向MFC桌面应用开发者的MySQL数据库访问示例工程,核心演示如何借助ODBC标准数据库接口,在C程序中完成连接建立、SQL查询、结果集遍历与数据更新等常用操作。压缩包内共含117个文件,压缩后约181KB;其中49…

2026/10/13 2:18:27 阅读更多 →
从Java转运营第5天:用技术思维做抖音新号,先学会拆解对标账号

从Java转运营第5天:用技术思维做抖音新号,先学会拆解对标账号

今天是我从 Java 转运营的第 5 天,也是正式开始折腾抖音新号起号的日子。说实话,从写完第一版账号简介到研究“到底该发什么”,我踩的坑比写的需求还多。如果把起号比作一次系统上线,那我 Day05 最大的收获就是:先别急…

2026/10/13 2:18:27 阅读更多 →
安卓手机跑 Windows 应用完整指南:Winlator 安装、配置与排障实操

安卓手机跑 Windows 应用完整指南:Winlator 安装、配置与排障实操

安卓手机跑 Windows 应用完整指南:Winlator 安装、配置与排障实操 【免费下载链接】winlator Android application for running Windows applications with Wine and Box86/Box64 项目地址: https://gitcode.com/GitHub_Trending/wi/winlator 手机里下了个 .…

2026/10/13 2:18:27 阅读更多 →
eksctl v0.149.0 发布解析:AWS 区域环境变量修复与 eksctl-io 组织迁移

eksctl v0.149.0 发布解析:AWS 区域环境变量修复与 eksctl-io 组织迁移

云原生容器编排DevOps 【免费下载链接】eksctl The official CLI for Amazon EKS 项目地址: https://gitcode.com/gh_mirrors/ek/eksctl 点击查看 免费下载 本指南围绕 eksctl 官方 CLI 的 v0.149.0 发布说明展开,逐项解析该版本引入的功能更新、两个关…

2026/10/13 2:18:27 阅读更多 →
Win11升级后应用打不开?从升级机制到修复方案全解析

Win11升级后应用打不开?从升级机制到修复方案全解析

最近一段时间,我这边处理最多的求助不是软件本身崩溃,而是同一句话:升完 Win11 之后,XX 应用打不开了。有的是设备厂商的配置工具,点开就没反应;有的是行业软件启动后直接闪退;还有的是虚拟机一…

2026/10/13 2:17:27 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/12 0:16:30 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →