平时开发中遇到根据当前用户的角色,只能查看数据权限范围的数据需求。一般我们采用拦截器在mybatis执行sql前修改语句,限定where范围。
通常拦截器针对注解进行识别,可以更好的对需要的接口进行拦截和转化。
一般步骤如下:
- 创建注解类
- 创建处理类,获取数据权限 SQL 片段,设置 where条件
- 将拦截器加到MyBatis-Plus插件中
一、数据权限插件简介
官方文档-数据权限插件:https://baomidou.com/plugins/data-permission/
DataPermissionInterceptor
是 MyBatis-Plus 提供的一个插件,用于实现数据权限控制。它通过拦截执行的 SQL 语句,并动态拼接权限相关的 SQL 片段,来实现对用户数据访问的控制。
DataPermissionInterceptor 的工作原理会在 SQL 执行前拦截 SQL 语句,并根据用户权限动态添加权限相关的 SQL 片段。这样,只有用户有权限访问的数据才会被查询出来。
JSQLParser
是一个开源的 SQL 解析库,可方便地解析和修改 SQL 语句。它是插件实现权限逻辑的关键工具,MyBatis-Plus 的数据权限依托于 JSQLParser 的解析能力。
使用方法:
- 自定义 MultiDataPermissionHandler的实现处理器类,处理自定义数据权限逻辑。
- 注册数据权限拦截器
二、数据权限插件实现
在项目中,使用的是 spring security + oauth2安全认证,角色来定义数据权限类型。
角色的数据权限类型:
@Getter
@RequiredArgsConstructor
public enum DataScopeEnum {
/**
* 全部数据权限
*/
DATA_SCOPE_ALL("0", "全部数据权限"),
/**
* 自定义数据权限
*/
DATA_SCOPE_CUSTOM("1", "自定义数据权限"),
/**
* 本部门及子级数据权限
*/
DATA_SCOPE_DEPT_AND_CHILD("2", "部门及子级数据权限"),
/**
* 本部门数据权限
*/
DATA_SCOPE_DEPT("3", "部门数据权限"),
/**
* 本人数据权限
*/
DATA_SCOPE_SELF("4", "本人数据权限"),
;
/**
* 对应数据库字典值
*/
private final String dbValue;
/**
* 描述
*/
private final String description;
}
对于 UserDetails用户信息我们做了扩展。添加了关于数据权限的字段信息。
public class SxdhcloudUser extends User implements OAuth2AuthenticatedPrincipal {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
/**
* 扩展属性,方便存放oauth 上下文相关信息
*/
private final Map<String, Object> attributes = new HashMap<>();
/**
* 租户ID
*/
@Getter
@JsonSerialize(using = ToStringSerializer.class)
private final Long tenantId;
/**
* 用户ID
*/
@Getter
@JsonSerialize(using = ToStringSerializer.class)
private final Long id;
/**
* 部门ID
*/
@Getter
@JsonSerialize(using = ToStringSerializer.class)
private final Long deptId;
/**
* 手机号
*/
@Getter
private final String phone;
/**
* 角色数据权限类型,去重
*/
@Getter
private final Set<String> dataScopeTypes;
/**
* 数据权限部门ID集合,去重
*/
@Getter
private final Set<Long> dataScopeDeptIds;
/**
* 数据权限本人ID
*/
@Getter
private final Long dataScopeCreateId;
}
用户在登录认证成功之后,获取到角色数据权限的相关数据,并放到 UserDetails用户信息中。
1、自定义注解
自定义数据权限注解。
/**
* 数据权限注解。
* 可以使用在类上,也可以使用在方法上。
* - 如果 Mapper类加上注解,表示 Mapper提供的方法以及自定义的方法都会被加上数据权限
* - 如果 Mapper类的方法加在上注解,表示该方法会被加上数据权限
* - 如果 Mapper类和其方法同时加上注解,优先级为:【类上 > 方法上】
* - 如果不需要数据权限,可以不加注解,也可以使用 @DataScope(enabled = false)
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataScope {
/**
* 是否生效,默认true-生效
*/
boolean enabled() default true;
/**
* 表别名
*/
String tableAlias() default "";
/**
* 部门限制范围的字段名称
*/
String deptScopeName() default "dept_id";
/**
* 本人限制范围的字段名称
*/
String oneselfScopeName() default "create_id";
}
2、自定义处理器
自定义处理器类并实现 MultiDataPermissionHandler接口
,在 getSqlSegment()方法
中处理自定义数据权限逻辑。
注意:mybaits-plus 必须大于 3.5.2版本。
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.plugins.handler.MultiDataPermissionHandler;
import com.sxdh.sxdhcloud.common.mybatis.annotation.DataScope;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.Parenthesis;
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 数据权限拼装逻辑处理
*
*/
public class DataScopeHandler implements MultiDataPermissionHandler {
/**
* 获取数据权限 SQL 片段。
* <p>旧的 {@link MultiDataPermissionHandler#getSqlSegment(Expression, String)} 方法第一个参数包含所有的 where 条件信息,如果 return 了 null 会覆盖原有的 where 数据,</p>
* <p>新版的 {@link MultiDataPermissionHandler#getSqlSegment(Table, Expression, String)} 方法不能覆盖原有的 where 数据,如果 return 了 null 则表示不追加任何 where 条件</p>
*
* @param table 所执行的数据库表信息,可以通过此参数获取表名和表别名
* @param where 原有的 where 条件信息
* @param mappedStatementId Mybatis MappedStatement Id 根据该参数可以判断具体执行方法
* @return JSqlParser 条件表达式,返回的条件表达式会拼接在原有的表达式后面(不会覆盖原有的表达式)
*/
@Override
public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) {
try {
Class<?> mapperClazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(".")));
String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(".") + 1);
/**
* DataScope注解优先级:【类上 > 方法上】
*/
// 获取 DataScope注解
DataScope dataScopeAnnotationClazz = mapperClazz.getAnnotation(DataScope.class);
if (ObjectUtils.isNotEmpty(dataScopeAnnotationClazz) && dataScopeAnnotationClazz.enabled()) {
return buildDataScopeByAnnotation(dataScopeAnnotationClazz);
}
// 获取自身类中的所有方法,不包括继承。与访问权限无关
Method[] methods = mapperClazz.getDeclaredMethods();
for (Method method : methods) {
DataScope dataScopeAnnotationMethod = method.getAnnotation(DataScope.class);
if (ObjectUtils.isEmpty(dataScopeAnnotationMethod) || !dataScopeAnnotationMethod.enabled()) {
continue;
}
if (method.getName().equals(methodName) || (method.getName() + "_COUNT").equals(methodName) || (method.getName() + "_count").equals(methodName)) {
return buildDataScopeByAnnotation(dataScopeAnnotationMethod);
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
/**
* DataScope注解方式,拼装数据权限
*
* @param dataScope
* @return
*/
private Expression buildDataScopeByAnnotation(DataScope dataScope) {
// 获取 UserDetails用户信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
return null;
}
Map<String, Object> userDetailsMap = BeanUtil.beanToMap(authentication.getPrincipal());
Set<String> dataScopeTypes = (Set<String>) userDetailsMap.get("dataScopeTypes");
Set<Long> dataScopeDeptIds = (Set<Long>) userDetailsMap.get("dataScopeDeptIds");
Long dataScopeCreateId = (Long) userDetailsMap.get("dataScopeCreateId");
// 获取注解信息
String tableAlias = dataScope.tableAlias();
String deptScopeName = dataScope.deptScopeName();
String oneselfScopeName = dataScope.oneselfScopeName();
Expression expression = buildDataScopeExpression(tableAlias, deptScopeName, oneselfScopeName, dataScopeDeptIds, dataScopeCreateId);
return expression == null ? null : new Parenthesis(expression);
}
/**
* 拼装数据权限
*
* @param tableAlias 表别名
* @param deptScopeName 部门限制范围的字段名称
* @param oneselfScopeName 本人限制范围的字段名称
* @param dataScopeDeptIds 数据权限部门ID集合,去重
* @param dataScopeCreateId 数据权限本人ID
* @return
*/
private Expression buildDataScopeExpression(String tableAlias, String deptScopeName, String oneselfScopeName, Set<Long> dataScopeDeptIds, Long dataScopeCreateId) {
/**
* 构造部门in表达式。
*/
InExpression deptIdInExpression = null;
if (CollectionUtils.isNotEmpty(dataScopeDeptIds)) {
deptIdInExpression = new InExpression();
ExpressionList deptIds = new ExpressionList(dataScopeDeptIds.stream().map(LongValue::new).collect(Collectors.toList()));
// 设置左边的字段表达式,右边设置值。
deptIdInExpression.setLeftExpression(buildColumn(tableAlias, deptScopeName));
deptIdInExpression.setRightExpression(new Parenthesis(deptIds));
}
/**
* 构造本人eq表达式
*/
EqualsTo oneselfEqualsTo = null;
if (dataScopeCreateId != null) {
oneselfEqualsTo = new EqualsTo();
oneselfEqualsTo.withLeftExpression(buildColumn(tableAlias, oneselfScopeName));
oneselfEqualsTo.setRightExpression(new LongValue(dataScopeCreateId));
}
if (deptIdInExpression != null && oneselfEqualsTo != null) {
return new OrExpression(deptIdInExpression, oneselfEqualsTo);
} else if (deptIdInExpression != null && oneselfEqualsTo == null) {
return deptIdInExpression;
} else if (deptIdInExpression == null && oneselfEqualsTo != null) {
return oneselfEqualsTo;
}
return null;
}
/**
* 构建Column
*
* @param tableAlias 表别名
* @param columnName 字段名称
* @return 带表别名字段
*/
public static Column buildColumn(String tableAlias, String columnName) {
if (StringUtils.isNotEmpty(tableAlias)) {
columnName = tableAlias + "." + columnName;
}
return new Column(columnName);
}
}
3、注册数据权限拦截器
MybatisPlusConfig配置类中将自定义的处理器注册到 DataPermissionInterceptor
中。并将拦截器加到MyBatis-Plus插件中。
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 1.添加数据权限插件
interceptor.addInnerInterceptor(new DataPermissionInterceptor(new DataScopeHandler()));
// 2.添加分页插件
PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor();
// 设置数据库方言类型
pageInterceptor.setDbType(DbType.MYSQL);
// 下面配置根据需求自行设置
// 设置请求的页面大于最大页后操作,true调回到首页,false继续请求。默认false
pageInterceptor.setOverflow(false);
// 单页分页条数限制,默认无限制
pageInterceptor.setMaxLimit(500L);
interceptor.addInnerInterceptor(pageInterceptor);
return interceptor;
}
4、注解使用
@Mapper
public interface SysUserMapper extends BaseMapper<SysUser> {
/**
* 通过用户名查询用户信息(含有角色信息)
*
* @param username 用户名
* @return userVo
*/
UserVO getUserVoByUsername(String username);
/**
* 分页查询用户信息(含角色)
*
* @param page 分页
* @param userDTO 查询参数
* @return list
*/
@DataScope(tableAlias = "u")
IPage<UserVO> getUserVosPage(Page page, @Param("query") UserDTO userDTO, @Param("userIds") List<Long> userIds);
}
代码中的注释写的挺清楚,大家自行理解。
参考文章:
- 关于JSqlparser使用攻略(高效的SQL解析工具):https://www.jb51.net/article/267850.htm
– 求知若饥,虚心若愚。