spring boot + mybatis-plus 实现数据范围权限

spring boot + mybatis-plus 实现数据范围权限

公司的业务场景,经常会遇到,限制用户查询数据范围场景,在整理需求,查找资料之后,实现了通过注解+mybatis拦截器,在sql中动态添加权限字段及值的功能。使用的时候方便简单,so,总结一下,写出来以供参考。(公司业务要更复杂一点,当前写的是简化后的,不过原理是一样的)

数据库结构(简化)

用户表:

CREATE TABLE `t_basic_user` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `com_id` int(10) unsigned DEFAULT '0' COMMENT '创建公司id',
  `name` varchar(15) NOT NULL DEFAULT '' COMMENT '人员姓名',
  `account` varchar(11) NOT NULL DEFAULT '' COMMENT '人员账号',
  `pwd` char(64) NOT NULL DEFAULT '' COMMENT '人员密码',
  `dept_id` int(10) unsigned DEFAULT '0' COMMENT '人员部门id',
  `role_id` int(10) unsigned DEFAULT '0' COMMENT '人员角色id',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=10606 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='人员表';

公司表:

CREATE TABLE `t_basic_company` (
  `id` int(10) unsigned NOT NULL DEFAULT '0',
  `root_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '顶级公司id',
  `pid` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '上级公司id',
  `name` varchar(15) NOT NULL DEFAULT '' COMMENT '公司名称',
  `level_num` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '公司层级'
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='公司表';

角色表:

CREATE TABLE `t_basic_role_auth` (
  `id` int(20) unsigned NOT NULL AUTO_INCREMENT,
  `uid` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '创建人id',
  `role_id` int(20) unsigned NOT NULL DEFAULT '0' COMMENT '人员角色id',
  `auth` varchar(50) NOT NULL DEFAULT '' COMMENT '权限key',
  `data` tinyint(1) unsigned DEFAULT '0' COMMENT '权限value',
  PRIMARY KEY (`id`) USING BTREE,
  KEY `com_id` (`com_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=67681 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

业务场景

根据登录用户的id,所属部门id,所属单位id,结合当前登录用户的角色权限,决定当前用户能够查看到哪些数据。
例如:用户数据查看权限,权限的auth_key设置为user_list_view,每个角色所对应的角色表中的data,表示不同的数据范围。大致分为以下几种:
1:本单位
2:本单位及下级单位
3:无权限
4:所有数据

需要在访问用户信息列表的时候,根据当前角色在用户信息这个功能的权限下,所对应的data值,返回数据列表。达到不同角色,返回不同范围数据的目的。

数据范围权限的实现思路

1.使用注解,在mapper中将对应的authkey写入,并且标识清楚权限所限制的数据库表名,及字段名。(例:用户信息权限中,需要根据用户的com_id来限制数据,就需要表示清楚数据库名-t_basic_user,字段名-com_id)
2.在拦截器中,获取到当前用户id;根据注解,获取到对应的authkey,;然后再用这两个信息,到角色表中,获取到具体的data值
3.根据data值,设置权限的具体值;如果是本单位权限,则设置com_id 等于当前用户的公司id,如果是本单位及下级单位权限,则设置com_id在本单位及下级单位的公司列表中。
4.根据注解中定义的需要限制的表名和字段名,将限制条件,添加在sql后面

功能实现

依赖

        
        
            com.baomidou
            mybatis-plus-boot-starter
            3.4.2
        

        
            com.baomidou
            mybatis-plus-generator
            3.4.1
        

注解

/**
 * @author likm
 * 数据范围注解
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DataScope {
    /**
     * 当前功能的authKey,根据authKey去查询当前用户的具体权限值
     */
    String authKey() default "";

    /**
     * 需要加数据权限范围的表别名,单位权限
     */
    String comTableName() default "com";

    String comFieldName() default "com_id";
}

获取权限值相关


/**
 * 用户权限查询返回结构
 *
 * @author likm
 */
@Data
public class UserAuth implements Serializable {

    /**
     * 是否所有数据
     */
    private Boolean all;

    /**
     * 是否无权限
     */
    private Boolean none;

    /**
     * 限制的权限值
     */
    private List<Integer> ids;

}

枚举

/**
 * @author likm
 * @date 2021/7/29
 * @description 数据范围权限值相关枚举,模板
 */
public enum DataScopeViewTypeEnum {

    // 不能查看= 0
    //无权限
    VIEW_NONE(0, "无权限"),
    // 查看所有的= 1
    VIEW_ALL(1, "查看所有的"),

    // 查看自己的= 2
    VIEW_ME(2, "查看自己的"),

    // 查看本单位的= 3
    VIEW_COMPANY(3, "查看本单位的"),

    // 查看本单位及其下级单位的= 4
    VIEW_COMPANY_AND_SUB(4, "查看本单位及其下级单位的"),

    ;

    DataScopeViewTypeEnum(Integer code, String name) {
        this.code = code;
        this.name = name;
    }

    private Integer code;

    private String name;

    public Integer getCode() {
        return code;
    }

    public String getName() {
        return name;
    }

    public static DataScopeViewTypeEnum getByValue(Integer value) {
        for (DataScopeViewTypeEnum transactType : values()) {
            if (transactType.getCode().equals(value)) {
                return transactType;
            }
        }
        return null;
    }

}

接口

/**
 * 数据范围权限需要业务层实现的接口
 *
 * @Author likm
 * @Date 2021/6/24
 * @Description //TODO
 * @Version 1.0
 **/
public interface UserAuthInterface {

    /**
     * 查询用户对应权限的限制范围值
     *
     * @param authKey 权限值key
     * @return 用户的权限值
     */
    UserAuth getUserAuth(String authKey);
}

接口实现


/**
 * @Author likm
 * @Date 2021/7/29
 * @Description //TODO
 * @Version 1.0
 **/
@Service
public class UserAuthInterfaceImpl implements UserAuthInterface {
	
    @Autowired
    @Lazy //不知道咋回事,在拦截器中自动注入mapper之后,会出现循环依赖,暂时未找到好的解决方式,只能通过懒加载注解来解决
    private BasicCompanyMapper companyMapper;

    @Override
    public UserAuth getUserAuth(String authKey) {
        UserAuth userAuth = new UserAuth();
        //通过authKey和当前用户userId,获取到authKey对应的权限值data
        //StorageUtils.getCurrentUserId() 的实现逻辑,是请求参数中在header中带上token,然后使用拦截器进行解析之后,将用户信息存放在threadLocal中
        Integer data = basicUserMapper.getAuthDataByKey(StorageUtils.getCurrentUserId(), authKey);
        if (!Objects.isNull(authData)) {
            switch (DataScopeViewTypeEnum.getByValue(data)) {
                case VIEW_NONE:
                    userAuth.setNone(Boolean.TRUE);
                    break;
                case VIEW_ALL:
                    userAuth.setAll(Boolean.TRUE);
                    break;
                case VIEW_COMPANY_AND_SUB:
                    List<BasicCompany> companyList = companyMapper.selectList(new LambdaQueryWrapper<BasicCompany>()
                            .eq(BasicCompany::getPid, StorageUtils.getCurrentUserComId()));
                    List<Integer> comIds = companyList.stream().map(BasicCompany::getId).collect(Collectors.toList());
                    comIds.add(StorageUtils.getCurrentUserComId());
                    userAuth.setIds(comIds);
                    break;
                 case VIEW_COMPANY:
                 default:
                 //StorageUtils.getCurrentUserComId() 获取原理同StorageUtils.getCurrentUserId(),都是存放在threadLocal中的数据 
                    userAuth.setIds(Arrays.asList(StorageUtils.getCurrentUserComId()));
                    break;
            }
        }
        return userAuth;
    }
}

mybatis 拦截器实现

/**
 * @Author likm
 * @Date 2021/7/12
 * @Description //数据范围权限拦截器,配合@DataScope注解使用
 * @Version 1.0
 **/
public class DataScopeInnerInterceptor implements InnerInterceptor {

    @Autowired
    private UserAuthInterface authInterface;


    @SneakyThrows
    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

        //获取执行方法的位置
        String namespace = ms.getId();
        //获取mapper名称
        String className = namespace.substring(0, namespace.lastIndexOf("."));
        //获取方法名
        String methodName = namespace.substring(namespace.lastIndexOf(".") + 1);
        //获取当前mapper 的方法
        Method[] methods = Class.forName(className).getMethods();
        for (Method m : methods) {
            if (Objects.equals(m.getName(), methodName)) {
                //获取注解  来判断是不是要处理sql
                DataScope dataScope = m.getAnnotation(DataScope.class);
                if (Objects.isNull(dataScope) || Objects.equals("", dataScope.authKey())) {
                    //没有数据权限相关枚举,直接跳过
                    continue;
                }
                UserAuth auth = authInterface.getUserAuth(dataScope.authKey());
                //去除特殊字符
                String originalSql = boundSql.getSql().replaceAll("\r|\n|`", "");
                if (!Objects.isNull(auth)) {
                    //根据用户权限拼接sql
                    String newSql = getInExpressionByAuth(auth, dataScope, originalSql);
                    //通过反射修改sql语句
                    Field field = boundSql.getClass().getDeclaredField("sql");
                    field.setAccessible(true);
                    field.set(boundSql, newSql);
                }
            }
        }
    }


    /**
     * 根据权限,拼接SQL
     */
    public static String getInExpressionByAuth(UserAuth auth, DataScope dataScope, String sql) {

        //如果是admin账户,直接返回null,不修改原sql
        if (!Objects.isNull(auth.getAll()) && auth.getAll()) {
            return null;
        }
        //如果是无权限,拼接1=2
        if (!Objects.isNull(auth.getNone()) && auth.getNone()) {
            return addWhereCondition(sql, "1=2");
        }
         return addWhereCondition(sql, getCondition(dataScope.comTableName(), dataScope.comFieldName(), auth.getIds()));
    }

    /**
     * 生成where 条件字符串
     *
     * @param tableName 表名
     * @param fieldName 字段名
     * @param ids       值
     * @return
     */
    private static String getCondition(String tableName, String fieldName, List<Integer> ids) {
        return tableName + "." + fieldName + " in (" + StringUtils.join(ids, ",") + ")";
    }

    /**
     * 在原有的sql中增加新的where条件
     *
     * @param sql       原sql
     * @param condition 新的and条件
     * @return 新的sql
     */
    public static String addWhereCondition(String sql, String condition) {
        try {
            Select select = (Select) CCJSqlParserUtil.parse(sql);
            PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
            final Expression expression = plainSelect.getWhere();
            final Expression envCondition = CCJSqlParserUtil.parseCondExpression(condition);
            if (Objects.isNull(expression)) {
                plainSelect.setWhere(envCondition);
            } else {
                AndExpression andExpression = new AndExpression(expression, envCondition);
                plainSelect.setWhere(andExpression);
            }
            return plainSelect.toString();
        } catch (JSQLParserException e) {
            throw new RuntimeException(e);
        }
    }
}

配置拦截器

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    /**
     * 由于拦截器先于spring context注入,所以在拦截器中无法自动注入接口,
     * 需要先在此处将拦截器注入
     */
    @Bean
    public DataScopeInnerInterceptor dataScopeInnerInterceptor() {
        return new DataScopeInnerInterceptor();
    }

    /**
     * mybatis拦截器注入,
     * 可以将其他自定义拦截器从这里注入,方便在分页之前执行
     *
     * @return
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        System.out.println("注入userAuthInterface");
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(dataScopeInnerInterceptor());
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

使用方式

数据范围权限的使用方式,是在mapper中使用@DataScope注解,在注解中,设置当前sql,所相关权限的authKey值,以及数据范围权限,所限制的表名

    @DataScope(authKey = AuthKeyConstant.ILLEGAL_BUILD_DAYLIY_INSPECT_VIEW, comTableName = "u")
    Page<IllegalBuildingPatrolVO> getList(Page page, @Param("dto") IllegalBuildingPatrolDTO dto);

最终sql样式如下:
1.本单位

SELECT
	p.*,
	u.name userName 
FROM
	t_illegal_building_patrol p
	LEFT JOIN t_basic_user u ON u.id = p.create_uid 
WHERE
	p.is_del = 0 
	AND u.com_id IN ( 1 ) 
ORDER BY
	p.id DESC

2.本单位及下级单位

SELECT
	p.*,
	u.name userName 
FROM
	t_illegal_building_patrol p
	LEFT JOIN t_basic_user u ON u.id = p.create_uid 
WHERE
	p.is_del = 0 
	AND u.com_id IN ( 1, 2, 3 ) 
ORDER BY
	p.id DESC

以上,数据权限功能就完成了,本身我们公司的业务还涉及到部门权限、人员权限等相关限制,逻辑更复杂一些。不过基本逻辑一致

参考:
[1]: https://blog.csdn.net/sundasheng44/article/details/108118094

你可能感兴趣的:(工作分享,spring,boot,java,mybatis)