当前位置:   article > 正文

springboot自定义注解+mybatis拦截器-数据权限设计_使用自定义注解 结合mybatis拦截器实现数据权限

使用自定义注解 结合mybatis拦截器实现数据权限

目录

前言:

版本说明:

数据权限表结构设计:

权限实体类:

启动时缓存数据权限到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,用英文逗号隔开。

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

权限实体类:

  1. package com.hdkj.hdiot.configure.entity;
  2. import com.baomidou.mybatisplus.annotation.TableName;
  3. import com.hdkj.hdiot.configure.entity.RootEntity;
  4. import lombok.Data;
  5. import lombok.EqualsAndHashCode;
  6. import java.util.Date;
  7. /**
  8. * 用户权限
  9. *
  10. * @author liuch liuchenghui@hdkjrd.com
  11. * @since 1.0.0 2022-06-22
  12. */
  13. @Data
  14. @EqualsAndHashCode(callSuper=false)
  15. @TableName("hd_user_permission")
  16. public class HdUserPermissionEntity extends RootEntity {
  17. private static final long serialVersionUID = 1L;
  18. /**
  19. * 角色id
  20. */
  21. private String roleId;
  22. /**
  23. * 目标模块
  24. */
  25. private String target;
  26. /**
  27. * 目标数据id
  28. */
  29. private String tid;
  30. /**
  31. * 增 删 改 查、可授予(级联) C R U D A
  32. */
  33. private String actions;
  34. /**
  35. * 是否可用
  36. */
  37. private Integer enable;
  38. /**
  39. * 备注
  40. */
  41. private String remark;
  42. }

基类:

  1. package com.hdkj.hdiot.configure.entity;
  2. import com.baomidou.mybatisplus.annotation.FieldFill;
  3. import com.baomidou.mybatisplus.annotation.TableField;
  4. import com.fasterxml.jackson.annotation.JsonFormat;
  5. import io.swagger.annotations.ApiModelProperty;
  6. import lombok.Data;
  7. import java.io.Serializable;
  8. import java.util.Date;
  9. /**
  10. * @author liuch
  11. * @title: RootEntity
  12. * @description: 基础实体类,物联网2.0用
  13. * @date 2022/2/18 13:40
  14. */
  15. @Data
  16. public abstract class RootEntity implements Serializable {
  17. @ApiModelProperty(value = "id")
  18. private String id;
  19. @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss" , timezone = "GMT+8")
  20. @TableField(fill = FieldFill.INSERT)
  21. @ApiModelProperty(value = "创建时间")
  22. private Date created;
  23. @TableField(fill = FieldFill.INSERT)
  24. @ApiModelProperty(value = "创建人")
  25. private String creator;
  26. @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss" , timezone = "GMT+8")
  27. @TableField(fill = FieldFill.INSERT_UPDATE)
  28. @ApiModelProperty(value = "更新时间")
  29. private Date updated;
  30. @TableField(fill = FieldFill.INSERT_UPDATE)
  31. @ApiModelProperty(value = "更新人")
  32. private String updator;
  33. }

crud接口省略;

启动时缓存数据权限到redis

  1. /**
  2. * @Author: 刘成辉
  3. * @Date: 2022/6/18 10:05
  4. * @Description:
  5. */
  6. @Component
  7. @Slf4j
  8. //@Order(99)
  9. public class HdInitCacheApplication implements ApplicationRunner {
  10. @Autowired
  11. HdUserPermissionService userPermissionService;
  12. @Override
  13. public void run(ApplicationArguments args) throws Exception {
  14. log.info("----------缓存用户权限-----------------");
  15. userPermissionService.initCache();
  16. log.info("--------缓存用户权限结束--------------");
  17. }
  18. }

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

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

自定义注解:

@UserPermission
  1. /**
  2. * @Author: 刘成辉
  3. * @Date: 2022/6/22 9:43
  4. * @Description:
  5. */
  6. @Documented
  7. @Target({ElementType.METHOD})
  8. @Retention(RetentionPolicy.RUNTIME)
  9. public @interface UserPermission {
  10. /**
  11. * 目标模块
  12. * @return
  13. */
  14. PermissionObject[] value();
  15. }
@PermissionObject
  1. @Documented
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Target({})
  4. public @interface PermissionObject {
  5. /**
  6. * 授权目标
  7. */
  8. String target();
  9. /**
  10. * 表名
  11. */
  12. String tableName();
  13. /**
  14. * 别名
  15. */
  16. String tableAlias();
  17. }

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

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

实现Interceptor接口:

  1. @Slf4j
  2. @Intercepts(
  3. {
  4. @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
  5. }
  6. )
  7. public class UserPermissionHandler implements Interceptor {
  8. //@Autowired
  9. //RedisTemplate redisTemplate;
  10. /**
  11. * 是否开启用户权限验证
  12. */
  13. @Value("${hdiot.auth.enableDataPermission:#{false}}")
  14. boolean enableDataPermission;
  15. /**
  16. * 过滤权限接口uri白名单
  17. */
  18. @Value("${hdiot.auth.permissionWhiteList:#{null}}")
  19. private List<String> whitelist;
  20. @Override
  21. public Object intercept(Invocation invocation) throws Throwable {
  22. if (!enableDataPermission || inWhiteList()) {
  23. return invocation.proceed();
  24. }
  25. StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
  26. MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
  27. //获取拦截下的mapper
  28. MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
  29. if (!SqlCommandType.SELECT.equals(mappedStatement.getSqlCommandType())) {
  30. return invocation.proceed();
  31. }
  32. UserPermission dataAuth = getDataAuth(mappedStatement);
  33. //没有注解直接放行,可根据情况补充更多的业务逻辑
  34. if (dataAuth == null) {
  35. return invocation.proceed();
  36. }
  37. //是否为mybatisPlus的count分页查询,以_mpCount结尾
  38. boolean innerCount = mappedStatement.getId().endsWith("_mpCount");
  39. //获取上一个拦截器传过来的sql
  40. BoundSql boundSql = statementHandler.getBoundSql();
  41. String orgSql = boundSql.getSql(); //获取到当前需要被执行的SQL
  42. //根据指定权限点对原有sql进行修改
  43. String sql = modifyOrgSql(orgSql, dataAuth, innerCount);
  44. metaObject.setValue("delegate.boundSql.sql", sql);
  45. return invocation.proceed();
  46. }
  47. @Override
  48. public Object plugin(Object o) {
  49. if (o instanceof StatementHandler) {
  50. return Plugin.wrap(o, this);
  51. }
  52. return o;
  53. }
  54. @Override
  55. public void setProperties(Properties properties) {
  56. }
  57. /**
  58. * 通过反射获取mapper方法是否加了数据拦截注解
  59. */
  60. private UserPermission getDataAuth(MappedStatement mappedStatement) throws ClassNotFoundException {
  61. UserPermission permission = null;
  62. String id = mappedStatement.getId();
  63. String className = id.substring(0, id.lastIndexOf("."));
  64. String methodName = id.substring(id.lastIndexOf(".") + 1);
  65. final Class<?> cls = Class.forName(className);
  66. final Method[] methods = cls.getMethods();
  67. for (Method method : methods) {
  68. if (method.getName().equals(methodName) && method.isAnnotationPresent(UserPermission.class)) {
  69. permission = method.getAnnotation(UserPermission.class);
  70. break;
  71. }
  72. }
  73. return permission;
  74. }
  75. /**
  76. * 根据权限点拼装对应sql
  77. *
  78. * @return 拼装后的sql
  79. */
  80. private String modifyOrgSql(String orgSQql, UserPermission userPermission, boolean innerCount) throws JSQLParserException {
  81. CCJSqlParserManager parserManager = new CCJSqlParserManager();
  82. Select select = (Select) parserManager.parse(new StringReader(orgSQql));
  83. PlainSelect plain = (PlainSelect) select.getSelectBody();
  84. List<Join> joins = plain.getJoins();
  85. if (innerCount) {
  86. SubSelect subSelect = (SubSelect) plain.getFromItem();
  87. plain = (PlainSelect) subSelect.getSelectBody();
  88. joins = plain.getJoins();
  89. }
  90. if (null == joins || joins.isEmpty()) {
  91. joins = new ArrayList<>();
  92. }
  93. String addWhere = "";
  94. List<String> whereList = new ArrayList<>();
  95. PermissionObject[] target = userPermission.value();
  96. for (PermissionObject permissionObject : target) {
  97. String where = getAllowDatas(permissionObject, joins);
  98. //String where = permissionObject.tableAlias() + ".id in(" + ids + ") ";
  99. whereList.add(where);
  100. }
  101. addWhere = String.join("and ", whereList);
  102. if (plain.getWhere() == null) {
  103. plain.setWhere(CCJSqlParserUtil.parseCondExpression(addWhere));
  104. } else {
  105. plain.setWhere(new AndExpression(plain.getWhere(), CCJSqlParserUtil.parseCondExpression(addWhere)));
  106. }
  107. plain.setJoins(joins);
  108. return select.toString();
  109. }
  110. /**
  111. * 获取有权限的数据
  112. *
  113. * @param target
  114. * @return
  115. */
  116. public String getAllowDatas(PermissionObject target, List<Join> joins) {
  117. String where = "%s.role_id in (%s) and %s.target = '%s'";
  118. Join join = new Join();
  119. //权限表别名
  120. String alais = "pre_" + target.target();
  121. List<String> partItems = new ArrayList<>();
  122. partItems.add("hd_user_permission");
  123. Table table = new Table(partItems);
  124. table.setAlias(new Alias(alais));
  125. join.setRightItem(table);
  126. EqualsTo equals = new EqualsTo();
  127. equals.setLeftExpression(new Column(alais + ".tid"));
  128. equals.setRightExpression(new Column(target.tableAlias() + ".id"));
  129. join.setOnExpression(equals);
  130. joins.add(join);
  131. //HashOperations operations = redisTemplate.opsForHash();
  132. List<String> dataIds = new ArrayList<>();
  133. //获取用户信息
  134. UserInfo user = BaseCtrl.getCurrentUser();
  135. log.info("权限用户:" + JSON.toJSONString(user));
  136. String ids = "";
  137. if (null == user || StringUtils.isBlank(user.getUserId())) {
  138. ids = "''";
  139. } else {
  140. }
  141. List<String> roleIds = user.getRoles();
  142. for (String dataId : roleIds) {
  143. if (roleIds.indexOf(dataId) == roleIds.size() - 1) {
  144. ids += ("'" + dataId + "'");
  145. break;
  146. }
  147. ids += ("'" + dataId + "',");
  148. }
  149. return String.format(where, alais, ids, alais, target.target());
  150. }
  151. private boolean inWhiteList() {
  152. ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
  153. if (null == attributes) {
  154. return false;
  155. }
  156. String requestURI = attributes.getRequest().getRequestURI();
  157. for (String s : whitelist) {
  158. if (requestURI.contains(s)) {
  159. return true;
  160. }
  161. }
  162. return false;
  163. }
  164. }

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

有用的话点个赞再走呗!

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小丑西瓜9/article/detail/716404
推荐阅读
相关标签
  

闽ICP备14008679号