目录

前言:

版本说明:

数据权限表结构设计:

权限实体类:

启动时缓存数据权限到redis

自定义注解:

实现Interceptor接口:


前言:

       由于业务场景需要,针对部分业务功能数据需要根据角色实现数据权限控制,数据关系比较复杂,有多表关联查询的场景,查阅一番资料,使用Mybatis拦截器--Interceptor,通过实现Interceptor接口,结合自定义注解,实现数据权限控制。哪里有不对的请各位大佬指出。

有用的话点个赞再走呗!

MyBatis 允许使用插件来拦截的方法调用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) 拦截执行器的方法
  • ParameterHandler (getParameterObject, setParameters) 拦截参数的处理
  • ResultSetHandler (handleResultSets, handleOutputParameters) 拦截结果集的处理
  • StatementHandler (prepare, parameterize, batch, update, query) 拦截Sql语法和会话构建的处理

具体原理可以参考:MyBatis拦截器原理介绍-简书

拦截器注解使用:

@Intercepts和@Signature注解

@Intercepts

@Intercepts注解只有一个属性,即value,其返回值类型是一个@Signature类型的数组,表示我们可以配置多个@Signature注解。

@Signature

@Signature注解其实就是一个方法签名,其共有三个属性,分别为:

type指接口的class,
method指接口中的方法名,
args指的是方法参数类型(该属性返回值是一个数组)。


版本说明:

开发工具:IDEA

JDK: 1.8

springboot:2.1.8-RELEASE

框架:springboot+mybatisplus

数据库:mysql 5.7

redis: 7.0

数据权限表结构设计:

target表示目标功能,如模板定义:iotapp,分区管理:devicearea

tid:表示目标功能表的主键id,用英文逗号隔开。

CREATE TABLE `hd_user_permission` (
  `id` varchar(64) NOT NULL,
  `role_id` varchar(64) DEFAULT NULL COMMENT '角色id',
  `target` varchar(50) DEFAULT NULL COMMENT '目标模块',
  `tid` longtext COMMENT '授权对象id',
  `enable` int(1) DEFAULT NULL COMMENT '是否可用',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  `created` datetime DEFAULT NULL COMMENT '创建时间',
  `creator` varchar(20) DEFAULT NULL COMMENT '创建人',
  `updated` datetime DEFAULT NULL COMMENT '更新时间',
  `updator` varchar(20) DEFAULT NULL COMMENT '更新人',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户权限';

权限实体类:

package com.hdkj.hdiot.configure.entity;

import com.baomidou.mybatisplus.annotation.TableName;
import com.hdkj.hdiot.configure.entity.RootEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.util.Date;

/**
 * 用户权限
 *
 * @author liuch liuchenghui@hdkjrd.com
 * @since 1.0.0 2022-06-22
 */
@Data
@EqualsAndHashCode(callSuper=false)
@TableName("hd_user_permission")
public class HdUserPermissionEntity extends RootEntity {
	private static final long serialVersionUID = 1L;

    /**
     * 角色id
     */
	private String roleId;
    /**
     * 目标模块
     */
	private String target;
    /**
     * 目标数据id
     */
	private String tid;
    /**
     * 增 删 改 查、可授予(级联) C R U D A
     */
	private String actions;
    /**
     * 是否可用
     */
	private Integer enable;
    /**
     * 备注
     */
	private String remark;
}

基类:

package com.hdkj.hdiot.configure.entity;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

/**
 * @author liuch
 * @title: RootEntity
 * @description: 基础实体类,物联网2.0用
 * @date 2022/2/18 13:40
 */
@Data
public abstract class RootEntity implements Serializable {

    @ApiModelProperty(value = "id")
    private String id;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss" , timezone = "GMT+8")
    @TableField(fill = FieldFill.INSERT)
    @ApiModelProperty(value = "创建时间")
    private Date created;

    @TableField(fill = FieldFill.INSERT)
    @ApiModelProperty(value = "创建人")
    private String creator;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss" , timezone = "GMT+8")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @ApiModelProperty(value = "更新时间")
    private Date updated;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    @ApiModelProperty(value = "更新人")
    private String updator;
}

crud接口省略;

启动时缓存数据权限到redis



/**
 * @Author: 刘成辉
 * @Date: 2022/6/18 10:05
 * @Description:
 */
@Component
@Slf4j
//@Order(99)
public class HdInitCacheApplication implements ApplicationRunner {

    @Autowired
    HdUserPermissionService userPermissionService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        log.info("----------缓存用户权限-----------------");
        userPermissionService.initCache();
        log.info("--------缓存用户权限结束--------------");
    }
}

角色id+目标功能模块作为key ,对象作为value,以Hash存储。

 @Override
    public void initCache() {
        HashOperations operations = redisTemplate.opsForHash();
        List<HdUserPermissionEntity> permissionEntityList = selectList(new QueryWrapper<>());
       
        for (HdUserPermissionEntity p : permissionEntityList) {
            operations.put(CacheConstant.PERMISSION,p.getRoleId()+"-"+p.getTarget(),p);
        }

    }

自定义注解:

@UserPermission

/**
 * @Author: 刘成辉
 * @Date: 2022/6/22 9:43
 * @Description:
 */
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserPermission {

    /**
     * 目标模块
     * @return
     */
    PermissionObject[] value();
}
@PermissionObject

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface PermissionObject {

    /**
     * 授权目标
     */
    String target();

    /**
     * 表名
     */
    String tableName();

    /**
     * 别名
     */
    String tableAlias();



}

注解是为了标记那个dao的方法需要实现数据权限,如:

@UserPermission({
            @PermissionObject(target = "devicearea", tableName = "hd_device_area", tableAlias = "b"),
            @PermissionObject(target = "app", tableName = "hd_iot_app", tableAlias = "c"),
            @PermissionObject(target = "model", tableName = "hd_iot_model", tableAlias = "d"),
    })
    List<HDDevicePageRespDTO> selectPageList(@Param("params") Map<String, Object> params, IPage<HdDeviceEntity> page);

实现Interceptor接口:


@Slf4j
@Intercepts(
        {
                @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
        }
)
public class UserPermissionHandler implements Interceptor {

    //@Autowired
    //RedisTemplate redisTemplate;

    /**
     * 是否开启用户权限验证
     */
    @Value("${hdiot.auth.enableDataPermission:#{false}}")
    boolean enableDataPermission;


    /**
     * 过滤权限接口uri白名单
     */
    @Value("${hdiot.auth.permissionWhiteList:#{null}}")
    private List<String> whitelist;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        if (!enableDataPermission || inWhiteList()) {
            return invocation.proceed();
        }
        StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);

        //获取拦截下的mapper
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        if (!SqlCommandType.SELECT.equals(mappedStatement.getSqlCommandType())) {
            return invocation.proceed();
        }
        UserPermission dataAuth = getDataAuth(mappedStatement);

        //没有注解直接放行,可根据情况补充更多的业务逻辑
        if (dataAuth == null) {
            return invocation.proceed();
        }
        //是否为mybatisPlus的count分页查询,以_mpCount结尾
        boolean innerCount = mappedStatement.getId().endsWith("_mpCount");
        //获取上一个拦截器传过来的sql
        BoundSql boundSql = statementHandler.getBoundSql();
        String orgSql = boundSql.getSql(); //获取到当前需要被执行的SQL
        //根据指定权限点对原有sql进行修改
        String sql = modifyOrgSql(orgSql, dataAuth, innerCount);
        metaObject.setValue("delegate.boundSql.sql", sql);
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object o) {
        if (o instanceof StatementHandler) {
            return Plugin.wrap(o, this);
        }
        return o;
    }

    @Override
    public void setProperties(Properties properties) {

    }

    /**
     * 通过反射获取mapper方法是否加了数据拦截注解
     */
    private UserPermission getDataAuth(MappedStatement mappedStatement) throws ClassNotFoundException {
        UserPermission permission = null;
        String id = mappedStatement.getId();
        String className = id.substring(0, id.lastIndexOf("."));
        String methodName = id.substring(id.lastIndexOf(".") + 1);
        final Class<?> cls = Class.forName(className);
        final Method[] methods = cls.getMethods();
        for (Method method : methods) {
            if (method.getName().equals(methodName) && method.isAnnotationPresent(UserPermission.class)) {
                permission = method.getAnnotation(UserPermission.class);
                break;
            }
        }
        return permission;
    }

    /**
     * 根据权限点拼装对应sql
     *
     * @return 拼装后的sql
     */
    private String modifyOrgSql(String orgSQql, UserPermission userPermission, boolean innerCount) throws JSQLParserException {
        CCJSqlParserManager parserManager = new CCJSqlParserManager();
        Select select = (Select) parserManager.parse(new StringReader(orgSQql));
        PlainSelect plain = (PlainSelect) select.getSelectBody();
        List<Join> joins = plain.getJoins();
        if (innerCount) {
            SubSelect subSelect = (SubSelect) plain.getFromItem();
            plain = (PlainSelect) subSelect.getSelectBody();
            joins = plain.getJoins();
        }
        if (null == joins || joins.isEmpty()) {
            joins = new ArrayList<>();
        }
        String addWhere = "";
        List<String> whereList = new ArrayList<>();
        PermissionObject[] target = userPermission.value();
        for (PermissionObject permissionObject : target) {
            String where = getAllowDatas(permissionObject, joins);
            //String where = permissionObject.tableAlias() + ".id in(" + ids + ") ";
            whereList.add(where);
        }
        addWhere = String.join("and ", whereList);

        if (plain.getWhere() == null) {
            plain.setWhere(CCJSqlParserUtil.parseCondExpression(addWhere));
        } else {
            plain.setWhere(new AndExpression(plain.getWhere(), CCJSqlParserUtil.parseCondExpression(addWhere)));
        }
        plain.setJoins(joins);
        return select.toString();
    }


    /**
     * 获取有权限的数据
     *
     * @param target
     * @return
     */
    public String getAllowDatas(PermissionObject target, List<Join> joins) {
        String where = "%s.role_id in (%s) and %s.target = '%s'";
        Join join = new Join();
        //权限表别名
        String alais = "pre_" + target.target();
        List<String> partItems = new ArrayList<>();
        partItems.add("hd_user_permission");
        Table table = new Table(partItems);
        table.setAlias(new Alias(alais));
        join.setRightItem(table);
        EqualsTo equals = new EqualsTo();
        equals.setLeftExpression(new Column(alais + ".tid"));
        equals.setRightExpression(new Column(target.tableAlias() + ".id"));
        join.setOnExpression(equals);
        joins.add(join);
        //HashOperations operations = redisTemplate.opsForHash();
        List<String> dataIds = new ArrayList<>();
        //获取用户信息
        UserInfo user = BaseCtrl.getCurrentUser();
        log.info("权限用户:" + JSON.toJSONString(user));

        String ids = "";
        if (null == user || StringUtils.isBlank(user.getUserId())) {
            ids = "''";
        } else {

        }
        List<String> roleIds = user.getRoles();
        for (String dataId : roleIds) {
            if (roleIds.indexOf(dataId) == roleIds.size() - 1) {
                ids += ("'" + dataId + "'");
                break;
            }
            ids += ("'" + dataId + "',");
        }
        return String.format(where, alais, ids, alais, target.target());
    }

    private boolean inWhiteList() {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (null == attributes) {
            return false;
        }
        String requestURI = attributes.getRequest().getRequestURI();
        for (String s : whitelist) {
            if (requestURI.contains(s)) {
                return true;
            }
        }
        return false;
    }
}

搞定,以上就是基于mybatis的数据权限控制了,使用的是join查询,sql语句记得加group by,避免出现重复数据,另外,记得给字段加索引!!!

有用的话点个赞再走呗!

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_24473507/article/details/126545343

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐