文章目录
- [SpringBoot + JSqlParser + MyBatis 数据权限实现方案](#SpringBoot + JSqlParser + MyBatis 数据权限实现方案)
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");
八、注意事项
-
SQL兼容性:
- 处理UNION语句时需遍历所有SELECT子句
- 支持子查询中的权限控制
- 注意表别名处理
-
性能优化:
java// 使用弱引用缓存解析结果 private static final Map<String, SoftReference<Select>> sqlCache = new ConcurrentHashMap<>();
-
权限失效场景:
- 直接SQL执行(绕过MyBatis)
- 存储过程调用
- 多租户架构下的跨库查询
-
审计日志:
java// 记录修改前后的SQL log.info("Original SQL: {}\nModified SQL: {}", originalSql, modifiedSql);
完整实现需要根据具体业务需求调整权限条件生成逻辑,建议配合单元测试验证不同场景下的SQL修改效果。