拓冰建站拓冰建站
首页 / 资讯中心 / 正文

SpringBoot+MyBatis拦截器实现数据权限:注解动态改写SQL全解析

做过后台管理系统的人应该都有体会功能权限管的是“能不能点这个按钮”数据权限管的却是“登录进来之后到底能看到哪些数据”。我早期做订单系统的时候销售要看自己的订单、部门主管能看整个部门的、老板看全部一开始每个查询都在Service层手动拼user_id ?后来一个单子涉及十几个查询真的会漏。后面我换成了“SpringBoot 自定义注解 MyBatis 拦截器动态改写SQL”这套玩法业务代码干干净净查询自己带上权限条件今天就把这套方案的完整设计和实现过程拆开讲一遍。如果你是刚接触数据权限的同学或者已经被各种硬编码权限条件折磨得头疼这篇文章应该能给你一个可以直接抄作业的落地思路。全程基于 SpringBoot MyBatis 实现核心就是两个东西一个注解一个SQL改写拦截器不依赖任何重量级框架。1. 数据权限的痛点与方案选型思路1.1 数据权限到底解决什么问题先理清概念。我们常说的权限管理大部分时候指的是“菜单权限”、“按钮权限”也就是用户能不能访问某个页面、能不能点某个按钮。这种权限通常用 Shiro、Spring Security 这类框架就搞定了设计上是一棵权限树控制的是操作入口。但真正的业务系统里权限还要细到“这一行数据你能不能看”。同样是订单查询接口销售A登录之后只能看到自己的订单销售B只能看到自己的订单销售总监能看到整个销售部门的订单财务总监可能要看到全公司的订单。如果没有数据权限查询接口写一次SQL所有人共用那销售A就能通过改参数看到销售B的订单这是很严重的越权漏洞。数据权限有多种维度常见的有权限维度规则示例落地条件本人数据只能看user_id 当前登录人的数据表里有 user_id/creator 这类字段部门数据能看到本部门所有数据表里有 dept_id/org_id 字段部门及子部门数据能看到本部门和下级部门数据表里有 dept_id 且能查子部门集合全部数据管理员 / 老板看全部无过滤条件自定义规则按地区和角色组合过滤需要单独的策略支持数据权限的难点不在于“要不要过滤”而在于不同角色使用同一个接口时过滤条件完全不同。如果把这些条件全部写在业务方法里代码会越积越多后面根本维护不过来。1.2 几种实现方式我为什么不推荐硬编码第一版我做的就是硬编码。每个Service方法里根据当前登录角色走 if-else然后往查询条件里塞不同的参数。表面上看没什么问题但用一段时间就暴露了查询一多权限判断代码反复出现一个查询漏加就是安全漏洞。新同事接手不知道哪些接口有数据权限、哪些没有全靠经验和“默契”。如果表的结构变了比如从单租户变成多租户所有查询的权限条件都要改一遍工作量巨大。第二版我抽了一个公共方法专门用来拼权限条件。算是稍微好了一点但遇到多表关联、字段重名、子查询这类场景这个公共方法就谈不上“通用”了还是在不断打补丁。所以落到第三版的时候我目标很明确业务代码不写权限判断权限规则通过注解声明SQL 的过滤条件由框架在底层自动完成。这就是注解 动态SQL的思路它把“权限规则”和“业务逻辑”彻底解耦了。1.3 注解 动态SQL 的整体设计整个方案的核心流程很简单在 Mapper 接口方法上打一个自定义注解声明这个查询接口需要什么数据权限规则比如column user_id, scope SELF。用户请求进来的时候把当前登录人的信息放进线程上下文。MyBatis 拦截器在真正执行 SQL 之前拦截到被调用的 Mapper 方法读取方法上的注解。根据注解规则和当前登录人信息动态拼出一段 SQL 条件例如WHERE user_id 10086。把拼接后的 SQL 替换掉原始 SQL让 MyBatis 继续执行。这样做的好处是查询接口本身不需要知道调用方是谁、需要什么权限权限规则写在方法上一目了然过滤条件是在 SQL 真正执行前动态生成的所有查询逻辑完全透明。接下来我会从底层原理到完整代码一步步拆解。2. 核心组件拆解注解、上下文与SQL改写器2.1 自定义注解用最简单的方式声明数据权限规则整个方案的入口是一个注解设计原则是“能少写就不多写”。我先定义一个DataPermission注解包含两个核心属性column权限过滤的字段名比如user_id、dept_id多表场景可以指定别名如o.user_id。scope权限范围用枚举表示比如本人SELF、本部门DEPT、全部ALL。代码大概是这样Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) Documented public interface DataPermission { /** * 需要做数据权限过滤的字段例如 user_id、dept_id * 多表关联时可以写 o.user_id 这样带别名的格式 */ String column() default ; /** * 数据权限范围 */ DataScope scope() default DataScope.SELF; /** * 是否必须校验默认 true。 * 比如某些场景当前用户没有登录但又不想阻断查询可以设为 false */ boolean required() default true; }配套的枚举public enum DataScope { // 只看本人数据 SELF, // 看本部门数据 DEPT, // 看本部门及子部门数据 DEPT_AND_CHILD, // 不作限制看全部 ALL }注解本身没什么高深的内容但它定义了一套“规矩”哪个字段需要过滤、按什么范围过滤都在方法上直接声明。比起之前每个查询里手动拼条件这种方式清晰太多了做代码评审时一眼就能看出哪些查询带数据权限、哪些没带。2.2 线程上下文当前登录用户信息的存取与清理要生成权限条件拦截器必须知道当前用户是谁、属于哪个部门、角色是什么。这里我用一个简单的ThreadLocal封装一个上下文对象请求入口处写入请求结束时清理。public class DataPermissionContext { private static final ThreadLocalUserInfo CONTEXT new ThreadLocal(); public static void set(UserInfo userInfo) { CONTEXT.set(userInfo); } public static UserInfo get() { return CONTEXT.get(); } public static void clear() { CONTEXT.remove(); } }UserInfo不需要太复杂能装下用户ID、部门ID、部门子节点集合、角色标识就够了public class UserInfo { private Long userId; private Long deptId; private ListLong deptIds; // 包含本部门和所有子部门 private String roleCode; // getter / setter 略 }这里有一点必须提醒请求结束一定要清理ThreadLocal。因为 Web 容器用的是线程池线程是复用的你不清理下一个请求可能拿到上一个登录人的信息导致权限条件错乱数据直接越权。这个坑我在 4.4 小节里还会详细说。2.3 SQL改写器MyBatis拦截器的工作原理MyBatis 的拦截器Interceptor机制本质上是对Executor、StatementHandler、ParameterHandler、ResultSetHandler四个核心组件做了一次代理增强。我们可以拦截这四个组件的方法在方法执行前后插入自定义逻辑。要“改写SQL”通常会拦截StatementHandler的prepare方法。因为prepare方法的入参是Connection说明即将创建Statement此时 SQL 已经确定下来了而且是一个完整的预编译 SQL修改它最合适。注册拦截器需要在配置类中声明 BeanSpringBoot 会自动把它加入 MyBatis 的拦截器链Configuration public class MyBatisConfig { Bean public DataPermissionInterceptor dataPermissionInterceptor() { return new DataPermissionInterceptor(); } }也可以直接给拦截器加Component注解SpringBoot 会自动装配。两种方式二选一即可。拦截器内部获取 SQL 有一定技巧我们拿到的是StatementHandler对象但实际持有 SQL 的是它的delegate属性。因为 MyBatis 的StatementHandler有好几层包装所以要通过MetaObject从包装对象里去取真正的BoundSql。2.4 核心的SQL拼接策略如何安全地插入WHERE条件拿到原始 SQL 之后最难的部分就是“把权限条件拼进去”。很多人觉得很简单直接在 SQL 后面加个WHERE不就完了实际上要处理的场景很多原始 SQL 已经带了 WHERE 条件需要改成WHERE ... AND (permission_condition)。原始 SQL 可能带 ORDER BY、GROUP BY、LIMIT权限条件必须插在这些关键字之前。SQL 大小写不统一可能是where也可能是WHERE。子查询里也有 WHERE不能把权限条件拼到子查询里。最稳妥的方案是用 SQL 解析器比如阿里巴巴的 Druid 内置的 SQL Parser或者 JSqlParser。它们会把 SQL 解析成语法树我们直接操作语法树的 WHERE 节点最后再生成 SQL。这样无论 SQL 多复杂都不会拼错位置。但为了先让实现跑通我提供一个字符串拼接的简易版本同时说明它的局限性。最简单的思路是先判断原始 SQL 有没有 WHERE有就在 WHERE 后面追加没有就找到 ORDER BY/GROUP BY/LIMIT 这些关键字把 WHERE 插在它们前面。private String injectWhere(String originalSql, String condition) { String upperSql originalSql.toUpperCase(); // 判断是否已有 WHERE int whereIndex findKeywordIndex(upperSql, WHERE); if (whereIndex 0) { int insertPos whereIndex 5; // 跳过 WHERE 关键字 return originalSql.substring(0, insertPos) ( condition ) AND originalSql.substring(insertPos).trim(); } // 没有 WHERE则找到 LIMIT / GROUP BY / ORDER BY 的位置 int limitIndex findKeywordIndex(upperSql, LIMIT); int groupIndex findKeywordIndex(upperSql, GROUP BY); int orderIndex findKeywordIndex(upperSql, ORDER BY); int insertPos originalSql.length(); insertPos minIfValid(insertPos, posOrDefault(limitIndex, -1)); insertPos minIfValid(insertPos, posOrDefault(groupIndex, -1)); insertPos minIfValid(insertPos, posOrDefault(orderIndex, -1)); return originalSql.substring(0, insertPos) WHERE ( condition ) originalSql.substring(insertPos).trim(); }findKeywordIndex是一个辅助方法用正则从字符串里找独立的关键字位置避免把order_id里的order误判为 ORDER BY。简易版逻辑能满足日常单表查询但真到了子查询、多表联合的场景还是建议直接上 Druid SQL Parser这在第 5 章会专门展开。3. 实操落地完整实现一套数据权限方案3.1 工程准备与依赖先准备一个标准的 SpringBoot 工程引入需要的依赖核心依赖就是 MyBatis 和 Spring Web没有额外的东西dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency !-- 使用 Druid SQL Parser 解析 SQL推荐加 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency /dependencies如果你用的是 SpringBoot 3.xmybatis-spring-boot-starter 要用 3.0 以上的版本这个要注意一下不然启动会报版本不兼容。3.2 完整代码注解、上下文、拦截器一条链路我直接给出一个完整的拦截器示例这个拦截器会做三件事获取当前执行的 Mapper 方法、读取方法上的DataPermission注解、根据注解和当前用户信息改写 SQL。Slf4j Intercepts({ Signature(type StatementHandler.class, method prepare, args {Connection.class, Integer.class}) }) public class DataPermissionInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler (StatementHandler) invocation.getTarget(); MetaObject metaObject MetaObject.forObject(statementHandler, SystemMetaObject.forObject(statementHandler), new DefaultObjectFactory(), new DefaultObjectWrapperFactory(), new DefaultReflectorFactory()); // 从包装对象里取出 MappedStatement MappedStatement mappedStatement (MappedStatement) metaObject.getValue(delegate.mappedStatement); String mapperId mappedStatement.getId(); // 根据 Mapper 接口名 方法名反射拿到方法对象 Method method resolveMapperMethod(mapperId); if (method null || !method.isAnnotationPresent(DataPermission.class)) { return invocation.proceed(); } DataPermission dataPermission method.getAnnotation(DataPermission.class); UserInfo userInfo DataPermissionContext.get(); // 生成权限过滤条件 String condition buildCondition(dataPermission, userInfo); if (StringUtils.hasText(condition)) { BoundSql boundSql statementHandler.getBoundSql(); String originalSql boundSql.getSql(); String newSql injectWhere(originalSql, condition); metaObject.setValue(delegate.boundSql.sql, newSql); } return invocation.proceed(); } }resolveMapperMethod的实现基于 MyBatis 的一个约定MappedStatement的 id 就是 Mapper 接口的全限定名 方法名。比如接口是com.example.mapper.OrderMapper方法是selectPage那 id 就是com.example.mapper.OrderMapper.selectPage。private Method resolveMapperMethod(String mapperId) throws ClassNotFoundException { int lastDot mapperId.lastIndexOf(.); if (lastDot 0) { return null; } String className mapperId.substring(0, lastDot); String methodName mapperId.substring(lastDot 1); Class? mapperClass Class.forName(className); Class?[] interfaces mapperClass.getInterfaces(); // 如果本身是接口方法 for (Method method : mapperClass.getMethods()) { if (method.getName().equals(methodName)) { return method; } } // 如果是代理生成的实现类需要去父接口找 for (Class? interfaceClass : interfaces) { for (Method method : interfaceClass.getMethods()) { if (method.getName().equals(methodName)) { return method; } } } return null; }这段代码有个坑如果同一个方法名有重载拿到的可能是错误的方法。真实项目中 Mapper 方法很少重载如果你的工程里有重载建议在注解上再加一个唯一标识比如方法描述避免反射匹配错。接下来是生成条件的方法这里根据不同的权限范围组合条件private String buildCondition(DataPermission dataPermission, UserInfo userInfo) { String column dataPermission.column(); DataScope scope dataPermission.scope(); // 未登录且注解不允许放行时直接抛异常 if (userInfo null) { if (dataPermission.required()) { throw new DataPermissionException(未获取到当前登录人信息); } return null; } // 管理员或 ALL 权限不生成条件 if (scope DataScope.ALL || isAdmin(userInfo)) { return null; } switch (scope) { case SELF: return column userInfo.getUserId(); case DEPT: return column userInfo.getDeptId(); case DEPT_AND_CHILD: // 部门及子部门生成 IN 条件 String ids userInfo.getDeptIds().stream() .map(String::valueOf) .collect(Collectors.joining(,)); return column IN ( ids ); default: return null; } }这里我也要提醒一个安全问题这里直接拼接了column和值。column 是我们自己在注解里写的受控性高但值如果来自外部或可以被人为修改就存在 SQL 注入风险。更好的做法是用预编译参数占位符再把参数值通过BoundSql的additionalParameters传递进去。我在第 4 章的排查技巧里会单独讲这个点。3.3 在业务代码中怎么用一行注解搞定权限使用方式非常直观。比如有一个订单查询接口销售只能看自己的订单部门主管看整个部门Mapper public interface OrderMapper { // 只看本人订单 DataPermission(column user_id, scope DataScope.SELF) ListOrder selectOrderList(Param(status) Integer status); // 部门主管看本部门订单这里用 o.user_id 表示订单用户字段 DataPermission(column o.user_id, scope DataScope.DEPT) ListOrder selectOrderPage(Param(status) Integer status); }注意第二个查询如果 SQL 里是t_order o这种别名写法column就写成带别名的o.user_id避免数据库不知道你指的是哪张表的 user_id。Service 层不用做任何权限判断查询结果天然就是过滤后的Service public class OrderService { private final OrderMapper orderMapper; public OrderService(OrderMapper orderMapper) { this.orderMapper orderMapper; } public ListOrder listMyOrders(Integer status) { return orderMapper.selectOrderList(status); } }那么用户信息是什么时候放进DataPermissionContext的我一般用一个 Spring MVC 拦截器或者 Servlet Filter 处理。这里用一个拦截器示例public class DataPermissionWebInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 伪代码从当前已登录会话中获取用户信息 UserInfo userInfo SecurityUtils.getCurrentUserInfo(); DataPermissionContext.set(userInfo); return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { DataPermissionContext.clear(); } }3.4 测试与验证看一眼真实执行的SQL写完代码之后最重要的是验证 SQL 是否真的加上了条件。我习惯在application.yml里开启 MyBatis 的 SQL 日志logging: level: com.example.mapper: debug然后分别模拟销售、主管登录请求同一个接口观察控制台打印的 SQL。销售登录时SELECT id, order_no, user_id, amount FROM t_order WHERE (user_id 10086) AND status 1部门主管登录时SELECT id, order_no, user_id, amount FROM t_order WHERE (dept_id 18) AND status 1管理员登录时日志里没有新增的 WHERE查询全量数据。看到这里基本就能确认方案生效了。4. 避坑指南常见问题与排查技巧4.1 拦截器不生效多半是注册和顺序问题我身边同事遇到最多的情况就是代码照着写完了接口一调SQL 一点变化都没有。排查下来最常见的原因是拦截器没被 Spring 管理。如果你用Component注册注意 SpringBoot 启动类扫描的包路径如果你在Configuration里手动声明Bean确认类上没有多余的Component注解避免重复注册。还有一种情况项目里用了多个 MyBatis 插件比如 PageHelper。MyBatis 在包装拦截器的时候执行顺序是“最后注册的最先执行”。如果你期望数据权限拦截器先执行、把 WHERE 加上之后再分页就要调整注册顺序。具体来说Bean public PageInterceptor pageInterceptor() { return new PageInterceptor(); } Bean public DataPermissionInterceptor dataPermissionInterceptor() { return new DataPermissionInterceptor(); }在这种配置下dataPermissionInterceptor后注册会被包装在外层执行时先于 PageHelper 生效也就是先拼上权限条件再执行分页。反过来如果先注册数据权限拦截器执行顺序就会颠倒分页统计 SQL 可能不带权限条件导致总条数不对。4.2 分页插件导致的条数不准问题承接上面的顺序问题再仔细说一个典型故障。如果你的 PageHelper 先执行了它会先改写出一条SELECT COUNT(*)的统计 SQL而数据权限拦截器还没来得及处理统计结果就会是“所有用户的数据条数”而不是“当前用户能看的数据条数”。页面表现就是表格里明明只显示了 10 条数据分页组件却显示总记录数 500 条第一次遇到时很容易以为是分页插件 bug。解决方案就是调整拦截器注册顺序让数据权限拦截器先包装、先执行确保统计 SQL 也被加上权限条件。4.3 复杂SQL拼接失败字符串拼接在子查询面前撑不住字符串方案处理单表查询很稳定但一旦 SQL 复杂比如里面有EXISTS子查询或者多个子查询各带 WHERE简易的字符串截取就会拼错位置。有一次我处理一个“查询客户最近一笔订单”的需求SQL 里本身有个很大的子查询字符串方式把权限条件插到了子查询内部直接查出了全量数据问题还特别难排查。如果项目里的查询 SQL 复杂度不可控我建议直接把 SQL 拼接逻辑换成 Druid 的 SQL Parser。用语法树操作就不存在“位置找错”的问题SQLStatement sqlStatement SQLUtils.parseSingleStatement(sql, JdbcConstants.MYSQL); if (sqlStatement instanceof SQLSelectStatement sqlSelectStatement) { SQLSelectQueryBlock queryBlock sqlSelectStatement.getSelect().getQueryBlock(); // 构造权限条件表达式 SQLExpr permissionExpr new SQLExprParser(condition, JdbcConstants.MYSQL).parseExpr(); if (queryBlock.getWhere() null) { queryBlock.setWhere(permissionExpr); } else { SQLBinaryOpExpr andExpr new SQLBinaryOpExpr( queryBlock.getWhere(), SQLBinaryOperator.BooleanAnd, permissionExpr ); queryBlock.setWhere(andExpr); } return sqlSelectStatement.toString(); }Druid 的 SQL Parser 支持多种数据库方言对 MySQL、PostgreSQL、Oracle 这些主流数据库都有良好的兼容性。代价就是多引入一个依赖但为了复杂 SQL 的稳定性这个成本很值。4.4 线程池复用导致的数据串号这是整个方案里最隐蔽、最危险的一个坑。SpringBoot 默认的 Web 容器使用线程池处理请求线程在处理完一个请求之后并不会销毁而是回到池子里等待下一个请求。如果我们在请求结束后忘了调用DataPermissionContext.clear()线程里残留的用户信息会被下一个请求拿到。假设用户 A 登录后查了自己部门的数据线程回到池子用户 B 的请求被分配到同一个线程如果 B 的权限范围是 SELF极有可能拿到 A 的 user_id产生越权访问。解决方式在 Web 拦截器或 Filter 的 finally 块里清理上下文。如果是异步任务在异步线程里显式重新设置上下文异步逻辑执行完再清理。在DataPermissionContext里放一个基于 TransmittableThreadLocal 的实现能更好地处理异步线程池数据传递。推荐直接用阿里开源的TransmittableThreadLocal替代原生ThreadLocal它在异步场景下的表现稳定很多这也是我对这个组件印象最深的一个坑。4.5 预编译参数与SQL注入的隐患我前面的代码示例里条件值是直接拼接进 SQL 字符串的比如user_id 10086。如果你能百分百确定值来自后端 session不经过前端参数那问题不大。但只要有一次从请求参数里取值拼进去就是严重的 SQL 注入漏洞。稳妥的写法是把权限条件拼成带占位符的语法树节点然后把对应的参数值注册到 BoundSql 的 additionalParameters 里或者直接通过ParameterMapping追加到原有参数列表。以字符串拼接为例可以这样处理// 生成带占位符的条件 String condition column ?; // 在 BoundSql 中追加参数 ListParameterMapping mappings new ArrayList(boundSql.getParameterMappings()); mappings.add(new ParameterMapping.Builder(configuration, dataPermissionUserId, Long.class).build()); metaObject.setValue(delegate.boundSql.parameterMappings, mappings); metaObject.setValue(delegate.boundSql.additionalParameters.dataPermissionUserId, userInfo.getUserId());这样 SQL 变成WHERE user_id ?参数通过预编译传递从根上杜绝注入。代价是代码稍复杂但安全这关值得多写几行。5. 方案进阶从能用走向好用5.1 支持自定义条件片段别把注解写得越来越复杂我最初设计的注解只有column和scope用了一阵子发现有些数据的权限规则没法用“字段值”来表达。比如“只能看最近 7 天创建的订单”或者“只能看所属地区为华东地区的订单”。遇到这种情况有两种做法。一种是继续往注解里加属性比如加一个condition属性允许直接写一段 SQL 片段DataPermission(condition region EAST)另一种是引入一个策略接口把权限规则的生成逻辑交给业务代码实现。我更推荐后者因为第二种方式能让系统慢慢收敛成一个可扩展的规则引擎。首先定义一个规则接口public interface DataPermissionRule { /** * 当前规则是否支持该次请求 */ boolean supports(DataPermission dataPermission, UserInfo userInfo); /** * 生成过滤条件 */ String buildCondition(DataPermission dataPermission, UserInfo userInfo); }然后默认实现和自定义实现并存。拦截器里改成遍历所有DataPermissionRule找到第一个支持当前注解的规则来生成条件。这样以后增加新规则只需要新增一个实现类不用改动拦截器。5.2 规则外部化配置把权限规则交给运营进阶到一定程度权限规则可能不只是开发人员在注解里写死产品运营可能也要在系统后台配置“哪些角色看哪些范围”。这时引入一个简单的规则配置表是很有必要的。表结构大概长这样字段名含义id主键mapper_idMapper 方法全限定名scope权限范围column_name过滤字段enabled是否启用update_time更新时间这样一来数据权限就不只是注解上的静态声明而是可以随时调整的运行时规则了。拦截器在判断注解之后再额外查一次配置表用最新配置覆盖注解默认值。5.3 拦截器性能优化别让SQL查询每条都翻规则这个方案引入了反射和 SQL 解析如果每次查询都做一遍对高频接口会有一定性能损耗。我实际是用了一个本地缓存来解决的。在拦截器里定义一个ConcurrentHashMapString, InvokerInfokey 是 Mapper 方法 idvalue 是解析好的注解信息和条件模板。这样反射只需要执行一次后续查询直接命中缓存private static final MapString, Method METHOD_CACHE new ConcurrentHashMap(); private Method resolveMapperMethod(String mapperId) { return METHOD_CACHE.computeIfAbsent(mapperId, id - { try { // 执行反射逻辑 return doResolveMapperMethod(id); } catch (ClassNotFoundException e) { return null; } }); }另外如果打开了 Druid 的 SQL ParserSQL 解析本身是有成本的同样可以考虑在缓存里保存“原始 SQL - 解析后的权限 SQL”的映射但要注意权限条件里的值必须用预编译占位符不然不同用户之间会缓存串数据。5.4 多数据源场景的兼容处理现在的项目多数据源几乎是常态。数据权限拦截器如果要同时兼容 MySQL、Oracle 这些不同方言拼接逻辑不能写死。我用一个简单的策略模式解决一个SqlDialect接口提供wrapWhere(sql, condition)方法。MySQL 和 Oracle 各有一个实现。拦截器根据当前MappedStatement对应的数据源选择对应的方言实现。Druid 的SQLUtils.parseSingleStatement(sql, dbType)已经内置了方言区分dbType 参数就是数据库类型所以用 Druid 做解析的话这块天然支持。根据我个人经验真正落地的时候不用追求“一上来就支持所有场景”。先把单库、单表、常规条件下的权限做稳再逐步把子查询、多数据源、自定义规则这些硬骨头啃下来一步到位反而容易被复杂度拖垮。最后分享一个自己实践中觉得特别值得保留的小习惯每次改完权限相关代码我都会分别用管理员、普通员工、部门主管三个账号各跑一遍核心查询把打印出来的 SQL 截图放在需求文档里。这个动作看似简单但在后面上线出问题的时候能省下大量的排查时间也能让“数据权限”这件事在团队里变得有迹可循、有据可查。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门