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.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Implement Role‑Based Data Permissions in MyBatis‑Plus Using Annotations & Interceptors

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaBackend DevelopmentInterceptorMyBatis-PlusData Permission
Code Ape Tech Column
Written by

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

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.