springboot自定义注解+mybatis拦截器-数据权限设计
针对部分业务功能数据需要实现数据权限控制,数据关系比较复杂,有多表关联查询的场景,查阅一番资料,使用Mybatis拦截器--Interceptor,通过实现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
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)