【Springboot进阶】springboot+mybatis+jsqlparser实现数据权限控制

文章目录

SpringBoot + JSqlParser + MyBatis 数据权限实现方案

一、环境准备

1. 添加依赖

xml 复制代码
<!-- MyBatis 拦截器支持 -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.3.0</version>
</dependency>

<!-- SQL解析器 -->
<dependency>
    <groupId>com.github.jsqlparser</groupId>
    <artifactId>jsqlparser</artifactId>
    <version>4.6</version>
</dependency>

二、用户上下文管理

1. 用户上下文持有类

java 复制代码
public class UserContextHolder {
    private static final ThreadLocal<LoginUser> context = new ThreadLocal<>();
    
    public static void set(LoginUser user) {
        context.set(user);
    }
    
    public static LoginUser get() {
        return context.get();
    }
    
    public static void clear() {
        context.remove();
    }
}

@Data
public class LoginUser {
    private Long userId;
    private String deptCode;  // 组织机构代码
    private List<String> dataScopes; // 数据权限范围
}

三、数据权限拦截器实现

1. MyBatis拦截器核心类

java 复制代码
@Intercepts({
    @Signature(type = Executor.class, 
              method = "query",
              args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
    @Signature(type = Executor.class,
              method = "update",
              args = {MappedStatement.class, Object.class})
})
public class DataPermissionInterceptor implements Interceptor {

    // 需要过滤的表(配置在application.yml)
    @Value("#{'${data-permission.ignore-tables:}'.split(',')}")
    private Set<String> ignoreTables;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取当前用户
        LoginUser user = UserContextHolder.get();
        if (user == null || CollectionUtils.isEmpty(user.getDataScopes())) {
            return invocation.proceed();
        }

        // 获取原始SQL
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        BoundSql boundSql = ms.getBoundSql(args[1]);
        String originalSql = boundSql.getSql();

        // SQL解析与增强
        String modifiedSql = enhanceSql(originalSql, user);
        if (originalSql.equals(modifiedSql)) {
            return invocation.proceed();
        }

        // 反射修改SQL
        Field field = boundSql.getClass().getDeclaredField("sql");
        field.setAccessible(true);
        field.set(boundSql, modifiedSql);

        return invocation.proceed();
    }

    private String enhanceSql(String originalSql, LoginUser user) {
        try {
            Select select = (Select) CCJSqlParserUtil.parse(originalSql);
            select.getSelectBody().accept(new SelectVisitorAdapter() {
                @Override
                public void visit(PlainSelect plainSelect) {
                    // 检查是否需要过滤
                    if (isIgnoreTable(plainSelect)) return;

                    // 构建权限表达式
                    Expression where = buildDataScopeExpression(user, plainSelect.getTable());
                    if (where == null) return;

                    // 合并条件
                    if (plainSelect.getWhere() == null) {
                        plainSelect.setWhere(where);
                    } else {
                        plainSelect.setWhere(new AndExpression(plainSelect.getWhere(), where));
                    }
                }
            });
            return select.toString();
        } catch (JSQLParserException e) {
            throw new RuntimeException("SQL解析失败", e);
        }
    }

    private boolean isIgnoreTable(PlainSelect plainSelect) {
        Table table = plainSelect.getFromItem() instanceof Table 
            ? (Table) plainSelect.getFromItem() 
            : null;
        return table != null && ignoreTables.contains(table.getName().toLowerCase());
    }

    private Expression buildDataScopeExpression(LoginUser user, Table table) {
        // 构建组织机构过滤条件
        List<Expression> conditions = new ArrayList<>();
        for (String deptCode : user.getDataScopes()) {
            EqualsTo equals = new EqualsTo(
                new Column(table.getAlias() == null ? "dept_code" : table.getAlias().getName() + ".dept_code"),
                new StringValue(deptCode)
            );
            conditions.add(equals);
        }
        return buildOrExpressionTree(conditions);
    }

    private Expression buildOrExpressionTree(List<Expression> conditions) {
        if (CollectionUtils.isEmpty(conditions)) return null;
        if (conditions.size() == 1) return conditions.get(0);
        
        Expression left = conditions.get(0);
        for (int i = 1; i < conditions.size(); i++) {
            left = new OrExpression(left, conditions.get(i));
        }
        return new Parenthesis(left);
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
    }
}

四、Spring Security集成

1. 用户信息注入

java 复制代码
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterAfter(jwtFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    @Bean
    public JwtAuthenticationFilter jwtFilter() {
        return new JwtAuthenticationFilter();
    }
}

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response,
                                    FilterChain chain) throws IOException, ServletException {
        // 从请求头解析用户信息
        String token = request.getHeader("Authorization");
        LoginUser user = parseToken(token);
        
        // 设置用户上下文
        try {
            UserContextHolder.set(user);
            chain.doFilter(request, response);
        } finally {
            UserContextHolder.clear();
        }
    }
}

五、配置项示例

application.yml

yaml 复制代码
data-permission:
  enabled: true
  ignore-tables: sys_log, public_data # 不进行权限控制的表
  column-name: dept_code # 权限字段名

六、使用示例

1. 业务查询测试

java 复制代码
@RestController
@RequestMapping("/users")
public class UserController {

    @Autowired
    private UserMapper userMapper;

    @GetMapping
    public List<User> listUsers() {
        // 实际SQL将被增强为:
        // SELECT * FROM user WHERE dept_code IN ('DEPT001','DEPT002') 
        return userMapper.selectList(null); 
    }
}

七、高级功能扩展

1. 多维度权限控制

java 复制代码
private Expression buildDataScopeExpression(LoginUser user, Table table) {
    List<Expression> conditions = new ArrayList<>();
    
    // 1. 部门过滤
    conditions.add(buildDeptCondition(table));
    
    // 2. 数据域过滤
    conditions.add(buildDataDomainCondition(table));
    
    // 3. 角色过滤
    conditions.add(buildRoleCondition(table));
    
    return buildAndExpressionTree(conditions);
}

private Expression buildAndExpressionTree(List<Expression> conditions) {
    // 类似OR表达式的构建逻辑
}

2. 动态权限字段配置

java 复制代码
@ConfigurationProperties(prefix = "data-permission")
public class DataPermissionProperties {
    private Map<String, String> columnMappings = new HashMap<>();
    
    // getters/setters
}

// 在拦截器中根据表名获取字段名
String column = properties.getColumnMappings()
                .getOrDefault(table.getName(), "dept_code");

八、注意事项

  1. SQL兼容性

    • 处理UNION语句时需遍历所有SELECT子句
    • 支持子查询中的权限控制
    • 注意表别名处理
  2. 性能优化

    java 复制代码
    // 使用弱引用缓存解析结果
    private static final Map<String, SoftReference<Select>> sqlCache = new ConcurrentHashMap<>();
  3. 权限失效场景

    • 直接SQL执行(绕过MyBatis)
    • 存储过程调用
    • 多租户架构下的跨库查询
  4. 审计日志

    java 复制代码
    // 记录修改前后的SQL
    log.info("Original SQL: {}\nModified SQL: {}", originalSql, modifiedSql);

完整实现需要根据具体业务需求调整权限条件生成逻辑,建议配合单元测试验证不同场景下的SQL修改效果。

关联知识

【Java知识】一款强大的SQL处理库JSqlPaser
【Spring相关技术】Spring进阶-SpEL深入解读

相关推荐
小杜-coding7 小时前
黑马点评day02(缓存)
java·spring boot·redis·后端·spring·maven·mybatis
zfj3218 小时前
用spring-boot-maven-plugin打包成单个jar有哪些缺点&优化方案
java·maven·jar·springboot
努力的搬砖人.12 小时前
Spring Boot 集成 Solr 的详细步骤及示例
spring boot·mybatis·solr
王天华帅哥12 小时前
解决因字段过长使MYSQL数据解析超时导致线上CPU告警问题
spring·mybatis
xuanjiong14 小时前
Javaweb项目--Mybatis,导入com.mysql.cj.jdbc.Driver时报错,Cannot resolve class ‘Driver‘
数据库·mysql·mybatis
计算机学姐1 天前
基于SpringBoot的同城宠物照看管理系统
java·vue.js·spring boot·后端·mysql·mybatis·宠物
爱尚你19931 天前
MyBatis 核心类详解与架构解析:从入门到源码级理解
mybatis
小杜-coding2 天前
黑马点评day01(基于Redis)
java·数据库·spring boot·redis·spring·缓存·mybatis
Code哈哈笑2 天前
【图书管理系统】环境介绍、设计数据库和表、配置文件、引入依赖
java·数据库·spring boot·后端·mybatis