Implement Role‑Based Data Permissions in MyBatis‑Plus Using Annotations & Interceptors
This article explains how to enforce data‑access restrictions in MyBatis‑Plus by creating a custom @UserDataPermission annotation, building an InnerInterceptor that rewrites SQL queries, handling role‑based scopes, and integrating the interceptor into the MyBatis‑Plus plugin for both basic and advanced use cases.
Basic Version
When a feature requires that users only see data within their permission scope, you can either add explicit checks in each query or use a MyBatis interceptor to modify the SQL globally. The interceptor approach works well for requirements added later or when you want to avoid touching every mapper method.
Create a custom annotation class.
Implement an interceptor that extends InnerInterceptor and overrides the query processing methods.
Write a handler that generates the SQL fragment for the data permission.
Register the interceptor in the MyBatis‑Plus plugin.
Custom Annotation
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserDataPermission {
}Interceptor
import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import lombok.*;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.select.SelectBody;
import net.sf.jsqlparser.statement.select.SetOperationList;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.sql.SQLException;
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class MyDataPermissionInterceptor extends JsqlParserSupport implements InnerInterceptor {
private MyDataPermissionHandler dataPermissionHandler;
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
return;
}
PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
mpBs.sql(this.parserSingle(mpBs.sql(), ms.getId()));
}
@Override
protected void processSelect(Select select, int index, String sql, Object obj) {
SelectBody selectBody = select.getSelectBody();
if (selectBody instanceof PlainSelect) {
this.setWhere((PlainSelect) selectBody, (String) obj);
} else if (selectBody instanceof SetOperationList) {
SetOperationList setOperationList = (SetOperationList) selectBody;
setOperationList.getSelects().forEach(s -> this.setWhere((PlainSelect) s, (String) obj));
}
}
private void setWhere(PlainSelect plainSelect, String whereSegment) {
Expression sqlSegment = this.dataPermissionHandler.getSqlSegment(plainSelect, whereSegment);
if (sqlSegment != null) {
plainSelect.setWhere(sqlSegment);
}
}
}Interceptor Handler (Basic)
import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.HexValue;
import net.sf.jsqlparser.expression.StringValue;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.select.PlainSelect;
import org.apache.ibatis.logging.log4j.Log4j2Impl;
import java.sql.SQLException;
public class MyDataPermissionHandler {
/**
* Generate the SQL segment for data permission.
*/
public Expression getSqlSegment(PlainSelect plainSelect, String whereSegment) {
// Default where clause if none exists
Expression where = plainSelect.getWhere();
if (where == null) {
where = new HexValue(" 1 = 1 ");
}
// Example: restrict to creator_code of the current user
Table fromItem = (Table) plainSelect.getFromItem();
Alias alias = fromItem.getAlias();
String mainTable = alias == null ? fromItem.getName() : alias.getName();
// Simulated current user retrieval
User user = SecurityUtils.getUser();
EqualsTo eq = new EqualsTo();
eq.setLeftExpression(new Column(mainTable + ".creator_code"));
eq.setRightExpression(new StringValue(user.getUserCode()));
return new AndExpression(where, eq);
}
}Registering the Interceptor
If your project already configures a MybatisPlusInterceptor, simply add the custom interceptor to it:
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
MyDataPermissionInterceptor dataPermissionInterceptor = new MyDataPermissionInterceptor();
dataPermissionInterceptor.setDataPermissionHandler(new MyDataPermissionHandler());
interceptor.addInnerInterceptor(dataPermissionInterceptor);
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}Or, if you need to keep an existing interceptor configuration:
@Bean
public MyDataPermissionInterceptor myInterceptor(MybatisPlusInterceptor mybatisPlusInterceptor) {
MyDataPermissionInterceptor sql = new MyDataPermissionInterceptor();
sql.setDataPermissionHandler(new MyDataPermissionHandler());
List<InnerInterceptor> list = new ArrayList<>();
list.add(sql);
mybatisPlusInterceptor.setInterceptors(list);
list.add(new PaginationInnerInterceptor(DbType.MYSQL));
return sql;
}Usage
Annotate the desired mapper methods with @UserDataPermission:
@UserDataPermission
List<CustomerAllVO> selectAllCustomerPage(IPage<CustomerAllVO> page, @Param("customerName") String customerName);Advanced Version
The advanced version addresses two additional concerns: role‑based data scopes and handling custom SQL that does not go through a mapper interface.
Define a DataScope enum representing ALL, DEPT, and MYSELF.
Create a DataPermission enum that maps role codes to a scope.
Rewrite the handler to obtain the current user's roles, determine the scope, and build the appropriate SQL expression (IN clause for department, equality for self, or no restriction for all).
DataScope Enum
public enum DataScope {
ALL("ALL"),
DEPT("DEPT"),
MYSELF("MYSELF");
private String name;
}DataPermission Enum
public enum DataPermission {
DATA_MANAGER("数据管理员", "DATA_MANAGER", DataScope.ALL),
DATA_AUDITOR("数据审核员", "DATA_AUDITOR", DataScope.DEPT),
DATA_OPERATOR("数据业务员", "DATA_OPERATOR", DataScope.MYSELF);
private String name;
private String code;
private DataScope scope;
// static helper methods getName, getCode, getScope omitted for brevity
}Interceptor Handler (Advanced)
public class MyDataPermissionHandler {
private RemoteRoleService remoteRoleService;
private RemoteUserService remoteUserService;
public Expression getSqlSegment(PlainSelect plainSelect, String whereSegment) {
remoteRoleService = SpringUtil.getBean(RemoteRoleService.class);
remoteUserService = SpringUtil.getBean(RemoteUserService.class);
Expression where = plainSelect.getWhere();
if (where == null) {
where = new HexValue(" 1 = 1 ");
}
Table fromItem = (Table) plainSelect.getFromItem();
Alias alias = fromItem.getAlias();
String mainTable = alias == null ? fromItem.getName() : alias.getName();
// Determine mapper method
String className = whereSegment.substring(0, whereSegment.lastIndexOf('.'));
String methodName = whereSegment.substring(whereSegment.lastIndexOf('.') + 1);
Method[] methods = Class.forName(className).getMethods();
for (Method m : methods) {
if (Objects.equals(m.getName(), methodName)) {
UserDataPermission ann = m.getAnnotation(UserDataPermission.class);
if (ann == null) {
return where;
}
User user = SecurityUtils.getUser();
Set<String> roleTypes = remoteRoleService.currentUserRoleType();
DataScope scope = DataPermission.getScope(roleTypes);
switch (scope) {
case ALL:
return where;
case DEPT:
List<String> deptUserList = remoteUserService.listUserCodesByDeptCodes(user.getDeptCode());
ItemsList deptList = new ExpressionList(deptUserList.stream().map(StringValue::new).collect(Collectors.toList()));
InExpression inExpr = new InExpression(new Column(mainTable + ".creator_code"), deptList);
return new AndExpression(where, inExpr);
case MYSELF:
EqualsTo eq = new EqualsTo();
eq.setLeftExpression(new Column(mainTable + ".creator_code"));
eq.setRightExpression(new StringValue(user.getUserCode()));
return new AndExpression(where, eq);
default:
break;
}
}
}
// No permission – return a condition that yields no rows
return new HexValue(" 1 = 2 ");
}
}Intermediate Mapper Interface
To apply the annotation without modifying existing mapper interfaces, create a middle‑layer interface that extends BaseMapper and adds the annotation to the required methods:
public interface DataPermissionMapper<T> extends BaseMapper<T> {
@Override
@UserDataPermission
T selectById(Serializable id);
@Override
@UserDataPermission
List<T> selectBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
@Override
@UserDataPermission
List<T> selectByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
@Override
@UserDataPermission
T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
@Override
@UserDataPermission
Integer selectCount(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
@Override
@UserDataPermission
List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
@Override
@UserDataPermission
List<Map<String, Object>> selectMaps(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
@Override
@UserDataPermission
List<Object> selectObjs(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
@Override
@UserDataPermission
<E extends IPage<T>> E selectPage(E page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
@Override
@UserDataPermission
<E extends IPage<Map<String, Object>>> E selectMapsPage(E page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
}Key points to remember:
Register the interceptor in the MyBatis‑Plus plugin so it takes effect.
The field used for permission filtering in the examples is creator_code; you can replace it with any column that identifies the data owner, such as dept_code.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
