当前位置: 首页 > news >正文

Spring Boot 3.4.3 基于 JSqlParser 和 MyBatis 实现自定义数据权限

前言

在企业级应用中,数据权限控制是保证数据安全的重要环节。本文将详细介绍如何在 Spring Boot 3.4.3 项目中结合 JSqlParser 和 MyBatis 实现灵活的数据权限控制,通过动态 SQL 改写实现多租户、部门隔离等常见数据权限需求。

一、环境准备

确保开发环境满足以下要求:

  • JDK 17+
  • Spring Boot 3.4.3
  • MyBatis 3.5.13+
  • JSqlParser 4.5+
  • Maven 3.6.3+

二、项目配置

1. 添加依赖

pom.xml 中添加以下依赖:

<dependencies>
    <!-- Spring Boot Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- MyBatis -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>3.0.3</version>
    </dependency>
    
    <!-- JSqlParser -->
    <dependency>
        <groupId>com.github.jsqlparser</groupId>
        <artifactId>jsqlparser</artifactId>
        <version>4.6</version>
    </dependency>
    
    <!-- 数据库驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    
    <!-- 其他工具 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

2. 数据权限注解

定义数据权限注解:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataPermission {
    /**
     * 部门表别名
     */
    String deptAlias() default "";
    
    /**
     * 用户表别名
     */
    String userAlias() default "";
    
    /**
     * 权限类型
     */
    DataPermissionType type() default DataPermissionType.DEPT;
}

public enum DataPermissionType {
    /**
     * 全部数据权限
     */
    ALL,
    
    /**
     * 部门数据权限
     */
    DEPT,
    
    /**
     * 部门及以下数据权限
     */
    DEPT_AND_CHILD,
    
    /**
     * 仅本人数据权限
     */
    SELF
}

三、核心实现

1. 数据权限上下文

public class DataPermissionContext {
    private static final ThreadLocal<DataPermissionInfo> CONTEXT = new ThreadLocal<>();
    
    public static void set(DataPermissionInfo info) {
        CONTEXT.set(info);
    }
    
    public static DataPermissionInfo get() {
        return CONTEXT.get();
    }
    
    public static void clear() {
        CONTEXT.remove();
    }
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class DataPermissionInfo {
    private Long userId;
    private Long deptId;
    private List<Long> deptIds; // 用户拥有的部门权限
    private DataPermissionType permissionType;
}

2. SQL 解析改写拦截器

@Intercepts({
    @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
@Slf4j
public class DataPermissionInterceptor implements Interceptor {
    
    private final JSqlParser sqlParser = new JSqlParser();
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        
        // 获取Mapper方法上的DataPermission注解
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        String id = mappedStatement.getId();
        String className = id.substring(0, id.lastIndexOf("."));
        String methodName = id.substring(id.lastIndexOf(".") + 1);
        
        Method method = Class.forName(className).getMethod(methodName, 
            mappedStatement.getParameterMap().getParameterTypes());
        DataPermission dataPermission = method.getAnnotation(DataPermission.class);
        
        if (dataPermission == null) {
            return invocation.proceed();
        }
        
        // 获取原始SQL
        BoundSql boundSql = statementHandler.getBoundSql();
        String originalSql = boundSql.getSql();
        
        // 解析并改写SQL
        String modifiedSql = processSql(originalSql, dataPermission);
        
        // 设置改写后的SQL
        metaObject.setValue("delegate.boundSql.sql", modifiedSql);
        
        return invocation.proceed();
    }
    
    private String processSql(String sql, DataPermission dataPermission) throws JSQLParserException {
        DataPermissionInfo permissionInfo = DataPermissionContext.get();
        if (permissionInfo == null || permissionInfo.getPermissionType() == DataPermissionType.ALL) {
            return sql;
        }
        
        Statement statement = sqlParser.parse(sql);
        if (statement instanceof Select) {
            Select select = (Select) statement;
            PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
            
            // 构建权限条件
            Expression permissionExpression = buildPermissionExpression(dataPermission, permissionInfo);
            if (permissionExpression != null) {
                if (plainSelect.getWhere() == null) {
                    plainSelect.setWhere(permissionExpression);
                } else {
                    plainSelect.setWhere(new AndExpression(plainSelect.getWhere(), permissionExpression));
                }
            }
            
            return select.toString();
        }
        
        return sql;
    }
    
    private Expression buildPermissionExpression(DataPermission dataPermission, DataPermissionInfo permissionInfo) {
        DataPermissionType type = permissionInfo.getPermissionType();
        if (type == DataPermissionType.SELF && StringUtils.isNotBlank(dataPermission.userAlias())) {
            // 仅本人数据权限
            return new EqualsTo(
                new Column(dataPermission.userAlias() + ".user_id"),
                new LongValue(permissionInfo.getUserId())
            );
        } else if ((type == DataPermissionType.DEPT || type == DataPermissionType.DEPT_AND_CHILD) 
                && StringUtils.isNotBlank(dataPermission.deptAlias())) {
            // 部门数据权限
            if (CollectionUtils.isEmpty(permissionInfo.getDeptIds())) {
                return null;
            }
            
            if (type == DataPermissionType.DEPT) {
                // 仅本部门
                return new InExpression(
                    new Column(dataPermission.deptAlias() + ".dept_id"),
                    new ExpressionList(permissionInfo.getDeptIds().stream()
                        .map(LongValue::new)
                        .collect(Collectors.toList()))
                );
            } else {
                // 本部门及以下部门
                return new GreaterThanEquals(
                    new Column(dataPermission.deptAlias() + ".dept_path"),
                    new StringValue(permissionInfo.getDeptId() + "/%")
                );
            }
        }
        
        return null;
    }
    
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    
    @Override
    public void setProperties(Properties properties) {
        // 可配置化处理
    }
}

3. 注册拦截器

@Configuration
public class MyBatisConfig {
    
    @Bean
    public DataPermissionInterceptor dataPermissionInterceptor() {
        return new DataPermissionInterceptor();
    }
}

四、使用示例

1. 设置数据权限上下文

@RestControllerAdvice
public class DataPermissionAdvice {
    
    @ModelAttribute
    public void setDataPermission() {
        // 实际项目中应从登录用户信息中获取
        DataPermissionInfo info = new DataPermissionInfo();
        info.setUserId(1001L);
        info.setDeptId(10L);
        info.setDeptIds(Arrays.asList(10L, 11L, 12L));
        info.setPermissionType(DataPermissionType.DEPT_AND_CHILD);
        
        DataPermissionContext.set(info);
    }
}

2. Mapper 接口使用

public interface UserMapper {
    @DataPermission(deptAlias = "u", userAlias = "u")
    @Select("SELECT * FROM sys_user u")
    List<User> selectAllUsers();
    
    @DataPermission(deptAlias = "u", type = DataPermissionType.SELF)
    @Select("SELECT * FROM sys_user u WHERE u.status = 1")
    List<User> selectActiveUsers();
}

3. 实体类定义

@Data
public class User {
    private Long userId;
    private String username;
    private Long deptId;
    private Integer status;
    // 其他字段...
}

五、高级功能

1. 多表关联查询支持

public interface OrderMapper {
    @DataPermission(deptAlias = "u", userAlias = "u")
    @Select("SELECT o.*, u.username FROM sys_order o " +
            "LEFT JOIN sys_user u ON o.user_id = u.user_id")
    List<Order> selectAllOrders();
}

2. 自定义权限处理器

public interface DataPermissionHandler {
    Expression handle(DataPermission dataPermission, DataPermissionInfo permissionInfo);
}

@Component
public class DefaultDataPermissionHandler implements DataPermissionHandler {
    @Override
    public Expression handle(DataPermission dataPermission, DataPermissionInfo permissionInfo) {
        // 自定义权限逻辑
        return null;
    }
}

// 在拦截器中注入使用
public class DataPermissionInterceptor implements Interceptor {
    @Autowired
    private DataPermissionHandler permissionHandler;
    
    // ...其他代码
}

3. 动态表名支持

public class DynamicTableProcessor {
    public static String process(String sql, String tableName, String dynamicTableName) {
        try {
            Statement statement = CCJSqlParserUtil.parse(sql);
            if (statement instanceof Select) {
                Select select = (Select) statement;
                select.getSelectBody().accept(new TableNamesFinder() {
                    @Override
                    public void visit(Table table) {
                        if (table.getName().equalsIgnoreCase(tableName)) {
                            table.setName(dynamicTableName);
                        }
                    }
                });
                return select.toString();
            }
        } catch (JSQLParserException e) {
            log.error("动态表名处理失败", e);
        }
        return sql;
    }
}

六、最佳实践

  1. 权限粒度控制

    • 根据业务需求设计合理的权限粒度
    • 避免过度设计导致系统复杂度过高
  2. 性能考虑

    • 大数据量表添加合适的索引
    • 复杂权限条件考虑使用视图
  3. 缓存策略

    • 频繁使用的权限数据可适当缓存
    • 注意缓存与权限变更的同步
  4. 测试覆盖

    • 编写全面的测试用例验证SQL改写正确性
    • 覆盖各种权限组合场景
  5. 日志记录

    • 记录重要的权限过滤操作
    • 便于问题排查和审计

七、常见问题解决

  1. SQL解析失败

    • 检查SQL语法是否符合JSqlParser支持的标准
    • 复杂SQL考虑简化或拆分
  2. 权限泄露

    • 确保所有查询方法都经过权限过滤
    • 默认拒绝所有未明确授权的访问
  3. 性能下降

    • 检查生成的SQL执行计划
    • 优化权限条件涉及的字段索引
  4. 嵌套查询支持

    • 复杂嵌套查询可能需要特殊处理
    • 考虑使用子查询或临时表优化

八、总结

本文详细介绍了在 Spring Boot 3.4.3 项目中基于 JSqlParser 和 MyBatis 实现自定义数据权限的完整方案。通过这种方案,我们可以:

  1. 通过注解灵活控制数据权限范围
  2. 自动改写SQL实现无缝权限过滤
  3. 支持多种常见的权限控制模式
  4. 保持业务代码的简洁性
  5. 实现细粒度的数据访问控制

这种方案相比传统的在业务代码中添加权限条件的方式,具有更好的可维护性和一致性,能够有效降低数据权限泄露的风险,是企业级应用开发中数据安全控制的理想选择。

http://www.dtcms.com/a/107106.html

相关文章:

  • 【Qt】QList<T> list(n)构造函数创建列表时元素 T的默认值
  • AI写程序:视频裁剪小工具
  • 【模板】P2764 最小路径覆盖问题
  • 【Linux】ELF文件与库的加载
  • RNN模型与NLP应用——(8/9)Attention(注意力机制)
  • LeetCode每日温度
  • Zemax设计实例:手机广角镜头设计(FOV 120°)
  • 在centos7上安装ragflow
  • 第149场双周赛:找到字符串中合法的相邻数字、重新安排会议得到最多空余时间 Ⅰ、
  • 腾讯云智测试开发面经
  • javaSE————网络原理
  • 从吉卜力漫画到艺术创造:GPT-4o多种风格绘图Prompt大全
  • Redisson 操作 Redis Stream 消息队列详解及实战案例
  • HttpClient-03.入门案例-发送POST方式请求
  • Dell G16 7620克隆硬盘 扩容
  • 移远RG200U-CN模组适配问题
  • OpenCV 图形API(7)用于将笛卡尔坐标(x, y)转换为极坐标(magnitude, angle)函数cartToPolar()
  • 【编程之路】按指定大小合并数据块
  • 局域网内便捷实现多设备文件共享方法
  • 【论文阅读】Anchor Graph Network for Incomplete Multiview Clustering
  • 【django】3 (django路由) 路由配置和反向解析
  • Python•输入输出基本运算
  • 浏览器指纹攻防技术深度解析:从多账号隔离到自动化矩阵架构设计
  • UG NX二次开发(C#)-采用Open/C与NXOpen获取曲线的长度
  • 【dp】有序三元组中的最大值 I
  • 关于依赖注入框架VContainer DIIOC 的学习记录
  • 壹起航:引领中国工厂迈向全球市场的先锋
  • 【蓝桥杯】每日练习 Day19,20
  • es分页边界数据重复问题处理
  • 【ArcGIS微课1000例】0142:如何从谷歌地球保存高清影像图片