【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深入解读

相关推荐
大漠孤烟s9 小时前
4.Mybatis学习-动态sql、缓存、逆向工程
mybatis
苏渡苇1 天前
Spring Insight 里如何把 Span 画成瀑布时间线
后端·spring·spring cloud·springboot·监控·apm
毕业设计7032 天前
(免费领源码) SpringBoot 游戏交易平台17600-java、PHP、python、C#、小程序、大数据、单片机、网络工程等)
java·spring boot·mysql·决策树·mybatis·idea·推荐算法
好菇娘の当自强2 天前
【Spring Boot 2.x 与 3.x YAML 配置区别详解】
springboot
cnkeysky2 天前
SpringBoot 整合 nacos 配置中心
nacos·springboot
她说..2 天前
常见设计模式-模板方法模式
java·spring·设计模式·springboot
程序员JerrySUN2 天前
Jetson Edge AI 实战01:Nano、Xavier、Orin 怎么选?Jetson 硬件选型详解【视频讲解】
java·数据库·redis·安全·mybatis
linmengmeng_13142 天前
【总结】MyBatis-Plus批量插入与清表的正确姿势
java·spring boot·mysql·mybatis
vx_BS813303 天前
【项目编号:project51381】Spring Boot 婚纱摄影管理系统:套餐预约、在线咨询、支付与客片展示的一站式业务实现
java·spring boot·eclipse·tomcat·mybatis
小蒜学长3 天前
大学生健康饮食的智慧管理系统(代码+数据库+LW)
java·后端·springboot·大学生·健康饮食