preface

In our business system, in addition to menu/function permissions, there is another very important function, which is data permissions. Data level permission management, most of the scheme is still hard coded, that is, this logic is coupled with the business code in if/else and other ways, according to the need to do data permission division. Here is another approach that is more elegant than hard coding: MyBatis interceptor + Spring AOP.

Technology base

  • Mybatis interceptor

    With mybatis custom interceptor, we can filter and wrap the SQL executed, such as SQL logging, adding data permission related SQL, etc.

  • spring aop

    On the basis of not changing the original code to add functions or horizontal extraction of the same function, the specific implementation form is the agent, the classic prototype is the thing control and log, we use here to do data permission control.

Design ideas

  • Data permission granularity

    In conventional business systems, data granularity is mainly divided into the following four types:

    • All data permissions: that is, records can be checked without permission distinction.
    • Department data permission: Allows users to view only the data of the department
    • Department and the following data permission: You can view the data of the department and subordinate departments of a user
    • Only personal data permission: You can view only your own data
    • Custom data permission: Applies to the scenario where one user corresponds to multiple departments
  • Specification for business table design

    To attribute business data, the related business table must have related fields: dept_id and user_id, i.e. department ID and user ID, as in article table:

    CREATE TABLE `cms_article` (
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'primary key',
      `category_id` bigint(20) unsigned DEFAULT NULL COMMENT 'column id',
      `title` varchar(120) NOT NULL COMMENT 'title',
      `description` varchar(255) DEFAULT NULL COMMENT 'description',
      `cover` varchar(255) DEFAULT NULL COMMENT The 'big picture',
      `author` varchar(30) DEFAULT NULL COMMENT 'the writer',
      `source` varchar(64) DEFAULT NULL COMMENT 'Article Source',
      `sort` double(10.2) unsigned DEFAULT '10.00' COMMENT 'order',
      `publish_time` datetime(3) DEFAULT NULL COMMENT 'Release time',
      `is_publish` tinyint(1) DEFAULT NULL COMMENT 'whether release (1 - > NO | NO, 2 - > | YES)',
      `content` mediumtext COMMENT 'Text content',
      `dept_id` bigint(20) unsigned DEFAULT NULL COMMENT 'Department',
      `user_id` bigint(20) unsigned DEFAULT NULL COMMENT 'Owning User',
      `create_time` datetime(3) DEFAULT NULL COMMENT 'Creation time',
      `update_time` datetime(3) DEFAULT NULL COMMENT 'Update Time',
      `is_deleted` tinyint(1) unsigned DEFAULT '1' COMMENT 'whether delete (1 - > not delete | NO, 2 - > deleted | YES)'.PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
    Copy the code
  • Data permission granularity and query

    • All the data

      select * from cms_article
      Copy the code
    • Department of data

      - You need to pass in the department ID of the current user
      select * from cms_article a where a.dept_id = ?
      Copy the code
    • Department and following data

      - You need to enter the ID of the department to which the current user belongs and the ID of the subordinate department
      select * from cms_article a where a.dept_id in(? ,? ,? ,?)Copy the code
    • Personal data only

      You need to pass in the current user ID
      select * from cms_article a where a.user_id = ?
      Copy the code
    • Custom data permissions

      You need to pass in the current user ID
      select * from cms_article a where a.dept_id in(select dept_id from sys_role_dept rd left join sys_user_role ur on rd.role_id=ur.role_id where ur.user_id=? group by dept_id)
      Copy the code

      or

      -- You need to pass in the pre-record user role ID
      select * from cms_article a where a.dept_id in(select dept_id from sys_role_dept role_id in(? ,? ,?) )Copy the code

Code implementation

The directory structure

├── ├─ ├─ SRC /main/ Java ├── ├.java ├── DataScopeConfig. Java ├ ─ ─ DataScopeHelper. Java ├ ─ ─ DataScopeInterceptor. Java └ ─ ─ DataSopeAspect. Java ├ ─ ─ src/main/resource/META-INF/spring.factoriesCopy the code

Core code Description

  • DataSope.java

    package com.mldong.common.dauth;
    import java.lang.annotation.*;
    @Target({ ElementType.METHOD, ElementType.TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface DataScope {
        /** * Department table alias */
        public String deptAlias(a) default "";
        /**
         * 用户表的别名
         */
        public String userAlias(a) default "";
    }
    Copy the code

    Data permission annotations, annotated methods automatically append data permission related SQL.

  • DataScopeHelper.java

    ThreadLocal thread data sharing tool, AOP constructs data permission SQL into ThreadLocal, interceptors read data permission SQL.

    package com.mldong.common.dauth;
    
    public class DataScopeHelper {
        private static final ThreadLocal<String> LOCAL_DATA_AUTH_SQL = new ThreadLocal();
        public static void setLocalDataAuthSql(String page) {
            LOCAL_DATA_AUTH_SQL.set(page);
        }
        public static String getLocalDataAuthSql() {
            return LOCAL_DATA_AUTH_SQL.get();
        }
        public staticvoid clearDataAuthSql() { LOCAL_DATA_AUTH_SQL.remove(); }}Copy the code
  • DataSopeAspect.java

    Aop implementation, where data permissions are maintained for SQL lifecycle, construct -> destroy

    package com.mldong.common.dauth;
    @Aspect
    @Component
    public class DataSopeAspect {
        /** * all data permission */
        public static final Integer DATA_SCOPE_ALL = 10;
    
        /** * Department data permission */
        public static final Integer DATA_SCOPE_DEPT = 20;
    
        /** ** department and the following data permissions */
        public static final Integer DATA_SCOPE_DEPT_AND_CHILD = 30;
        /** * Only my data permission */
        public static final Integer DATA_SCOPE_SELF = 40;
        /** * Custom data permission */
        public static final Integer DATA_SCOPE_CUSTOM = 50;
        // Add @datascope annotation before method execution - used to construct data permission SQL
        @Before("@annotation(dataScope)")
        public void dataScopeBefore(JoinPoint point, DataScope dataScope) throws Throwable     {
            // Super administrator, no processing
            if(RequestHolder.isSuperAdmin()) {
                return;
            }
    		// TODO
        }
        
        
        // Add @datascope annotation to the method after execution - used to destroy data permission SQL
        @After("@annotation(dataScope)")
        @AfterThrowing("@annotation(dataScope)")
        public void dataScopeAfter(JoinPoint point, DataScope dataScope) throws Throwable{
            if(StringTool.isNotEmpty(DataScopeHelper.getLocalDataAuthSql())) {
                // Clear the current permission SqlDataScopeHelper.clearDataAuthSql(); }}}Copy the code
  • DataScopeInterceptor.java

    Data permissions mybatis interceptor

    package com.mldong.common.dauth;
    
    import com.mldong.common.tool.StringTool;
    import org.apache.ibatis.cache.CacheKey;
    import org.apache.ibatis.executor.Executor;
    import org.apache.ibatis.mapping.BoundSql;
    import org.apache.ibatis.mapping.MappedStatement;
    import org.apache.ibatis.mapping.ParameterMapping;
    import org.apache.ibatis.mapping.SqlSource;
    import org.apache.ibatis.plugin.*;
    import org.apache.ibatis.session.ResultHandler;
    import org.apache.ibatis.session.RowBounds;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    import java.util.Properties;
    
    @Intercepts({@Signature(
            type = org.apache.ibatis.executor.Executor.class,
            method = "query",
            args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
    ), @Signature(
            type = Executor.class,
            method = "query",
            args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}
    )})
    public class DataScopeInterceptor implements Interceptor {
        private static final Logger logger= LoggerFactory.getLogger(DataScopeInterceptor.class);
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            String dataAuthSql = DataScopeHelper.getLocalDataAuthSql();
            //If (StringTool. IsNotEmpty (dataAuthSql)) {Object[] args= invocation.getArgs();
                MappedStatement ms = (MappedStatement)args[0];
                Object parameter = args[1];
                RowBounds rowBounds = (RowBounds)args[2];
                ResultHandler resultHandler = (ResultHandler)args[3];
                Executor executor = (Executor)invocation.getTarget();
                CacheKey cacheKey;
                BoundSql boundSql;
                if (args.length == 4) {
                    boundSql = ms.getBoundSql(parameter);
                    cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
                } else {
                    cacheKey = (CacheKey)args[4];
                    boundSql = (BoundSql)args[5];
                }
                String newSql = boundSql.getSql() + dataAuthSql ;
                BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), newSql,
                        boundSql.getParameterMappings(), boundSql.getParameterObject());
                //Put the new query in statement MappedStatement newMs= newMappedStatement(ms, new BoundSqlSqlSource(newBoundSql));
                for (ParameterMapping mapping : boundSql.getParameterMappings()) {
                    String prop =mapping.getProperty(); if (boundSql.hasAdditionalParameter(prop)) { newBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop)); }}return executor.query(newMs, parameter, rowBounds, resultHandler, cacheKey, newBoundSql);
            } else {
                returninvocation.proceed(); }}@Override
        public Object plugin(Object target) {
            return Plugin.wrap(target,this);
        }
    
        @Override
        public void setProperties(Properties properties) {
            logger.debug(properties.toString());
        }
    
        /** * defines an internal helper class that wraps SQL */
        class BoundSqlSqlSource implements SqlSource {
            private BoundSql boundSql;
            public BoundSqlSqlSource(BoundSql boundSql) {
                this.boundSql = boundSql;
            }
            public BoundSql getBoundSql(Object parameterObject) {
                return boundSql;
            }
    
        }
    
        private MappedStatement newMappedStatement (MappedStatement ms, SqlSource newSqlSource) {
            MappedStatement.Builder builder = new
                    MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType());
            builder.resource(ms.getResource());
            builder.fetchSize(ms.getFetchSize());
            builder.statementType(ms.getStatementType());
            builder.keyGenerator(ms.getKeyGenerator());
            if (ms.getKeyProperties() ! = null && ms.getKeyProperties().length > 0) {
                builder.keyProperty(ms.getKeyProperties()[0]);
            }
            builder.timeout(ms.getTimeout());
            builder.parameterMap(ms.getParameterMap());
            builder.resultMaps(ms.getResultMaps());
            builder.resultSetType(ms.getResultSetType());
            builder.cache(ms.getCache());
            builder.flushCacheRequired(ms.isFlushCacheRequired());
            builder.useCache(ms.isUseCache());
            returnbuilder.build(); }}Copy the code
  • DataScopeConfig.java

    package com.mldong.common.dauth;
    
    import com.github.pagehelper.autoconfigure.PageHelperAutoConfiguration;
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.autoconfigure.AutoConfigureAfter;
    
    import javax.annotation.PostConstruct;
    import java.util.List;
    
    //Last add first, so configure the interceptor after the MyBatis paging plug-in// @AutoConfigureAfterAnnotations need to be paired with spring.Factories to work@AutoConfigureAfter(PageHelperAutoConfiguration.class)
    public class DataScopeConfig {
        @Autowired
        private List<SqlSessionFactory> sqlSessionFactoryList;
    
        @PostConstruct
        public void addDataAuthInterceptor() {
            DataScopeInterceptor interceptor = new DataScopeInterceptor();
            for(SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) { sqlSessionFactory.getConfiguration().addInterceptor(interceptor); }}}Copy the code
  • spring.factories

    This is mainly to adjust the load order of mybatis paging plug-in and custom data permissions plug-in

    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    com.github.pagehelper.autoconfigure.PageHelperAutoConfiguration,\
    com.mldong.common.dauth.DataScopeConfig
    Copy the code

Use the sample

  • Used in the DAO layer

    @Repository
    public interface CmsArticleDao {
        /** * query the list of articles - data permission *@param param
         * @return* /
        @DataScope(deptAlias = "a", userAlias = "a")
        public List<CmsArticleWithExt> selectOnDataScope(CmsArticlePageParam param);
    }
    Copy the code
  • Used in the Service layer

    @Service
    public class CmsArticleServiceImpl implements CmsArticleService{
        @DataScope(deptAlias = "a", userAlias = "a")
        @Override
        public CommonPage<CmsArticleWithExt> listOnDataScope2(CmsArticlePageParam param) {
            Page<CmsArticleWithExt> page =param.buildPage(true);
            cmsArticleDao.selectWithExt(param);
            returnCommonPage.toPage(page); }}Copy the code

Results demonstrate

slightly

summary

In this paper, the interceptor mechanism of Mybatis and AOP are used to realize the global data permission processing, which can basically meet the needs of most businesses with data permission. Of course, more complex scenarios, such as those involving field permissions, may require a more granular design, which will not be expanded here.

Project source code address

  • The back-end

Gitee.com/mldong/mldo…

  • The front end

Gitee.com/mldong/mldo…

Related articles

Create a suitable for their own rapid development framework – the pilot

Build a suitable for their own rapid development framework – back-end scaffolding

Build a fast development framework for yourself – integrated Mapper

Build a fast development framework for yourself – integration with Swaggerui and KNIfe4J

Build a suitable for their own rapid development framework – universal class packaging unified result return, unified exception handling

Create a quick development framework for yourself – business error code specifications and practices

Build a quick development framework for yourself – framework layering and CURD sample

Create a suitable for their own rapid development framework – Mapper logical deletion and enumeration type specification

Create a suitable framework for rapid development – Hibernate Validator data verification

Create a suitable for their own rapid development framework – code generator principle and implementation

Create a suitable for their own rapid development framework – universal query design and implementation

Build a suitable rapid development framework – rBAC-based permission management

Build a quick development framework for yourself – login and permission blocking

Build a suitable for their own rapid development framework – HTTP request log global processing

Create a suitable for their own agile development framework – upload module design and implementation

Build a suitable for their own rapid development framework – continuous deployment of one key release script design and implementation