数据权限(Data Permission)是系统根据用户角色、部门或其他业务规则动态控制数据访问范围的技术。其核心目标是确保用户仅能访问其权限范围内的数据,避免越权操作。常见场景包括:
DataPermissionHandler
是 MyBatis-Plus 提供的接口,用于在 SQL 解析阶段动态注入权限条件。其核心流程如下:
getSqlSegment
方法,将自定义条件拼接到原始 WHERE 子句。<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.5.3.1version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-extensionartifactId>
<version>3.5.3.1version>
dependency>
package cn.sdlnrj.foundation.common.annotation;
import java.lang.annotation.*;
@Inherited
@Target({ ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CustomDataPermission {
/**
* 是否进行数据权限
*/
public boolean isRole() default false;
/**
* 权限字段
*/
public String permissionField() default "";
/**
* 权限方式(根据 用户、角色、部门等)
*/
public PermissionTypeEnum permissionType() default PermissionTypeEnum.USER;
}
@Getter
@AllArgsConstructor
public enum PermissionTypeEnum {
USER("用户", 10) {
/**
* 获取当前用户创建的数据
* @param classAnnotation
* @return
*/
@Override
public Expression getPermission(CustomDataPermission classAnnotation) {
//1.查询判断当前用户的角色,比如管理员角色不需要拼接数据权限
EqualsTo equalsTo = new EqualsTo();
equalsTo.setRightExpression(new StringValue("获取当前登录用户账号"));
if (Objects.nonNull(classAnnotation) && StringUtils.isNotBlank(classAnnotation.permissionField())) {
equalsTo.setLeftExpression(new Column(classAnnotation.permissionField()));
}
return equalsTo;
}
}, DEPT("部门", 20) {
@Override
public Expression getPermission(CustomDataPermission classAnnotation) {
EqualsTo equalsTo = new EqualsTo();
//获取当前用户的部门权限
equalsTo.setRightExpression(new StringValue("获取当前用户的部门权限"));
if (Objects.nonNull(classAnnotation) && StringUtils.isNotBlank(classAnnotation.permissionField())) {
equalsTo.setLeftExpression(new Column(classAnnotation.permissionField()));
}
return equalsTo;
}
}, ROLE("角色", 30) {
@Override
public Expression getPermission(CustomDataPermission classAnnotation) {
EqualsTo equalsTo = new EqualsTo();
equalsTo.setRightExpression(new StringValue("获取当前用户的角色权限"));
if (Objects.nonNull(classAnnotation) && StringUtils.isNotBlank(classAnnotation.permissionField())) {
equalsTo.setLeftExpression(new Column(classAnnotation.permissionField()));
}
return equalsTo;
}
}, POSITION("职务", 40) {
@Override
public Expression getPermission(CustomDataPermission classAnnotation) {
EqualsTo equalsTo = new EqualsTo();
equalsTo.setRightExpression(new StringValue("获取当前用户的角色权限"));
if (Objects.nonNull(classAnnotation) && StringUtils.isNotBlank(classAnnotation.permissionField())) {
equalsTo.setLeftExpression(new Column(classAnnotation.permissionField()));
}
return equalsTo;
}
};
final String name;
final Integer code;
public abstract Expression getPermission(CustomDataPermission classAnnotation);
public static Expression handle(CustomDataPermission classAnnotation) {
PermissionTypeEnum[] values = PermissionTypeEnum.values();
for (PermissionTypeEnum emissionStatus : values) {
if(emissionStatus.equals(classAnnotation.permissionType())){
return emissionStatus.getPermission(classAnnotation);
}
}
return new EqualsTo();
}
}
根据自己业务需求,可增加权限黑名单,也可增加白名单,这里不做白名单实例了。
data-permissions:
blackList:
-接口地址
@Configuration
@ConfigurationProperties(prefix = "data-permissions")
public class WhiteListConfig {
private List<String> blackList;
public List<String> getBlackList() {
return blackList;
}
public void setBlackList(List<String> blackList) {
this.blackList= blackList;
}
public Boolean not(String url) {
List<String> collect = blackList.stream().filter(white -> url.contains(white)).collect(Collectors.toList());
if (CollectionUtils.isEmpty(collect)) {
return false;
}
return true;
}
}
@Slf4j
public class MyDataPermissionHandler implements DataPermissionHandler {
/**
* @param where 原SQL Where 条件表达式
* @param mappedStatementId Mapper接口方法ID
* @return
*/
@SneakyThrows
@Override
public Expression getSqlSegment(Expression where, String mappedStatementId) {
// 获取当前用户权限上下文(示例:部门ID和租户ID)
UserContext user = UserContextHolder.getCurrentUser();
//如果调用mybatisplus查询单一的sql,则不进行数据权限拼接。
Class<?> clazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(".")));
if (StringUtils.isNotBlank(mappedStatementId)) {
String substring = mappedStatementId.substring(mappedStatementId.lastIndexOf(".") + 1);
if (substring.equals("selectById")) {
return where;
}
}
//判断访问的url是否在黑名单中
boolean isPreventAddress = SpringUtil.getBean(WhiteListConfig.class).not(WebUtil.getRequest().getRequestURI());
if (isPreventAddress) {
return where;
}
//判断mapper上是否有注解,CustomDataPermission是用在mapper上,标识操作sql语句的别名,以及mapper是否需要走数据权限。
CustomDataPermission classAnnotation = clazz.getAnnotation(CustomDataPermission.class);
if (Objects.isNull(classAnnotation)) {
return where;
}
// 创建 AND 表达式 拼接Where 和 = 表达式
return new AndExpression(where, PermissionTypeEnum.handle(classAnnotation));
}
}
插件类型 | 推荐顺序 | 说明 |
---|---|---|
数据权限插件 | 1 | 确保 WHERE 条件在所有分页、租户逻辑之前生效。 |
多租户插件 | 2 | 若同时使用多租户插件,应放在数据权限插件之后。 |
分页插件 | 3 | 最后执行,确保 LIMIT 和 COUNT 基于最终 SQL。 |
乐观锁插件 | 4 | 不影响 SQL 结构,顺序无严格要求。 |
完整配置示例:
@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 1. 数据权限插件
interceptor.addInnerInterceptor(new DataPermissionInterceptor(...));
// 2. 多租户插件
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(...));
// 3. 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 4. 乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
public class UserContextHolder {
private static final TransmittableThreadLocal<UserContext> contextHolder = new TransmittableThreadLocal<>();
public static void setUserContext(UserContext user) {
contextHolder.set(user);
}
public static UserContext getCurrentUser() {
return contextHolder.get();
}
public static void clear() {
contextHolder.remove();
}
}
// 示例拦截器设置上下文
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
UserContext user = extractUser(request);
UserContextHolder.setUserContext(user);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
UserContextHolder.clear();
}
}
问题原因:COUNT 查询未应用数据权限条件。
解决方案:
@Select("SELECT * FROM user WHERE ${ew.customSqlSegment}")
@Select("SELECT COUNT(*) FROM user WHERE ${ew.customSqlSegment}")
IPage<User> selectCustomPage(IPage<User> page, @Param(Constants.WRAPPER) Wrapper<User> wrapper);
问题原因:分页插件未正确配置或执行顺序错误。
解决方案:
PaginationInnerInterceptor
已添加到拦截器链。问题原因:权限条件未正确附加到关联表。
解决方案:
String condition = "u.dept_id = 2"; // u 为 user 表别名