因为业务需求,重新写了一套数据权限。项目中用的是mybtis-plus,正好MyBatis-Plus提供了插件数据权限插件 | MyBatis-Plus,那就根据文档来实现这个需求。
实现:
实现MultiDataPermissionHandler
首先创建MultiDataPermissionHandler的实现类,里面的内容自己做校验
@Slf4j
public class CustomDataPermissionHandler implements MultiDataPermissionHandler {// 岗位idprivate 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不过滤的表");@Overridepublic 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 {@Beanpublic 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();// 设置数据库类型为mysqlpaginationInnerInterceptor.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));
}