因为业务需求,重新写了一套数据权限。项目中用的是mybtis-plus,正好MyBatis-Plus提供了插件数据权限插件 | MyBatis-Plus,那就根据文档来实现这个需求。
实现:
实现MultiDataPermissionHandler
首先创建MultiDataPermissionHandler的实现类,里面的内容自己做校验
@Slf4j
public class CustomDataPermissionHandler implements MultiDataPermissionHandler {
// 岗位id
private static final String CEO = "1";
private static final String SE = "2";
private static final String HR = "3";
private static final String USER = "4";
// 免校验表
private static final String SYS = "sys";
private static final Set<String> EXEMPT_TABLES = Set.of(
"xxx不过滤的表",
"xxx不过滤的表"
);
@Override
public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) {
try {
if (IgnoreWhereConditionAspect.shouldIgnoreWhereCondition()) {
return null;
}
// 免校验表
if (isExemptTable(table.getName())) {
return null;
}
// 获取当前用户 未登录不做校验
SysUser user = getCurrentUser();
if (user == null || user.isAdmin()) {
return null;
}
/* 获取岗位ids,总管可以看全部数据,主管可以看到自己小组的数据,其他只可以看到自己的数据 */
List<String> postIds = getUserPostIds(user);
if (postIds.contains(CEO)) {
return null;
}
return postIds.contains(SE) ? parseGroupDataExpression(user.getGroupId(), getTableName(table)) : parseOwnDataExpression(getTableName(table));
} catch (JSQLParserException e) {
log.error("数据权限过滤失败," ,e);
return null;
}
}
/**
* 获取表名
* @param table 表
* @return 表名
*/
private String getTableName(Table table) {
return String.valueOf(table.getAlias() == null ? table.getName() : table.getAlias().getName());
}
/**
* 获取用户岗位ids
* @param user 当前操作的用户
* @return 岗位ids
*/
private List<String> getUserPostIds(SysUser user) {
if (user.getPostIds() == null) {
throw new ServiceException(POST_MUST_NOT_NULL);
}
return Arrays.asList(user.getPostIds());
}
/**
* 获取当前用户
* @return 当前用户
*/
private SysUser getCurrentUser() {
try {
return SecurityUtils.getUser();
} catch (Exception e) {
return null;
}
}
/**
* 根据表名判断是否免校验
* @param tableName 表名
* @return 是否免校验
*/
private boolean isExemptTable(String tableName) {
return tableName.startsWith(SYS) || EXEMPT_TABLES.contains(tableName);
}
/**
* 解析组数据表达式
* @param groupId 组id
* @return 组数据表达式
*/
private Expression parseGroupDataExpression(String groupId, String tableName) throws JSQLParserException {
return CCJSqlParserUtil.parseCondExpression(
tableName + ".create_by in (select sug.user_id from sys_user su " +
"join sys_user_group sug on sug.user_id = su.id " +
"where sug.group_id = " + groupId + ")"
);
}
/**
* 解析自己的数据表达式
* @return 自己的数据表达式
*/
private Expression parseOwnDataExpression(String name) throws JSQLParserException {
return CCJSqlParserUtil.parseCondExpression(name + ".create_by = " + SecurityUtils.getUserId());
}
这里实现了MultiDataPermissionHandler
接口的getSqlSegment
方法,根据文档说明不需要拼接where条件的直接返回null
就可以,需要拼接where添加的拼接相应的条件
注册数据权限拦截器
@EnableTransactionManagement(proxyTargetClass = true)
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
interceptor.addInnerInterceptor(paginationInnerInterceptor());
// 乐观锁插件
interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor());
// 阻断插件
interceptor.addInnerInterceptor(blockAttackInnerInterceptor());
// 数据权限插件
interceptor.addInnerInterceptor(new DataPermissionInterceptor(new CustomDataPermissionHandler()));
return interceptor;
}
/**
* 分页插件,自动识别数据库类型 https://baomidou.com/guide/interceptor-pagination.html
*/
public InnerInterceptor paginationInnerInterceptor() {
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
// 设置数据库类型为mysql
paginationInnerInterceptor.setDbType(DbType.MYSQL);
// 设置最大单页限制数量,默认 500 条,-1 不受限制
paginationInnerInterceptor.setMaxLimit(-1L);
return paginationInnerInterceptor;
}
/**
* 乐观锁插件 https://baomidou.com/guide/interceptor-optimistic-locker.html
*/
public OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor() {
return new OptimisticLockerInnerInterceptor();
}
/**
* 如果是对全表的删除或更新操作,就会终止该操作 https://baomidou.com/guide/interceptor-block-attack.html
*/
public InnerInterceptor blockAttackInnerInterceptor() {
return new BlockAttackInnerInterceptor();
}
}
新建注解IgnoreWhereCondition
表示是否忽略sql拦截,默认为true,如果有些接口不需要鉴权就在controller
上添加这个注解
@Target(ElementType.METHOD) // 该注解应用于方法
@Retention(RetentionPolicy.RUNTIME) // 运行时可用
public @interface IgnoreWhereCondition {
boolean value() default true; // 默认值为 true,表示忽略
}
新建AOP切面 IgnoreWhereConditionAspect
/**
* AOP切面,用于处理带有 @IgnoreWhereCondition 注解的方法。
* 通过ThreadLocal存储当前方法是否需要忽略SQL拦截的状态。
*/
@Aspect
@Component
public class IgnoreWhereConditionAspect {
private static final ThreadLocal<Boolean> ignoreWhereConditionHolder = ThreadLocal.withInitial(() -> false);
/**
* 在方法执行前检查是否有 @IgnoreWhereCondition 注解,并设置ThreadLocal中的状态。
* @param joinPoint JoinPoint
*/
@Before("@annotation(com.sh.framework.annotation.IgnoreWhereCondition)")
public void beforeMethod(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
IgnoreWhereCondition annotation = method.getAnnotation(IgnoreWhereCondition.class);
ignoreWhereConditionHolder.set(annotation.value());
}
/**
* 获取当前线程中是否应该忽略SQL拦截。
* 如果需要忽略,则清除ThreadLocal中的状态。
* @return boolean
*/
public static boolean shouldIgnoreWhereCondition() {
Boolean b = ignoreWhereConditionHolder.get();
if (b) clear();
return b;
}
public static void clear() {
ignoreWhereConditionHolder.remove();
}
}
使用注解
@SelectRecordAnnotate(value = "分页查询付款申请")
@Operation(summary = "分页查询付款申请")
@GetMapping("selectPage")
@IgnoreWhereCondition
public AjaxResult selectPage(SiHePage page, CostPayReqVO costPayReqVO) {
return success(costPayReqService.selectListPage(page, costPayReqVO));
}