Разработка и реализация универсального разрешения на передачу данных

Spring Boot

предисловие

В нашей бизнес-системе, помимо прав доступа к меню/функциям, есть еще одна очень важная функция — права доступа к данным. Управление разрешениями на уровне данных, большинство используемых решений по-прежнему жестко закодированы, то есть логикаif/elseОн связан с бизнес-кодом другими способами, а права доступа к данным распределяются по мере необходимости. В этой статье используется другой метод, который относительно элегантнее жесткого кодирования: перехватчик mybatis + spring aop.

техническая база

  • перехватчик мибатис

    Используя пользовательский перехватчик mybatis, мы можем фильтровать и упаковывать sql для выполнения, например, вести журнал sql, добавлять sql, связанный с разрешениями данных, и т. д.

  • spring aop

    Добавляйте функции или извлекайте те же горизонтальные функции без изменения исходного кода Конкретной формой реализации является агент, а классическим прототипом является управление транзакциями и журнал, которые мы используем здесь для управления доступом к данным.

Идеи дизайна

  • Детализация разрешения данных

    В обычных бизнес-системах степень детализации данных в основном делится на следующие четыре типа:

    • Все разрешения на данные: то есть записи можно проверять без различия разрешений.
    • Разрешение на данные отдела: просматривать только данные отдела пользователя
    • Отдел и следующие разрешения данных: может просматривать данные отдела пользователя и подчиненных отделов
    • Только мое разрешение на данные: могу просматривать только данные, находящиеся под моей юрисдикцией
    • Пользовательские разрешения на данные: используются в сценариях, где один пользователь соответствует нескольким отделам.
  • Спецификация дизайна бизнес-стола

    Чтобы сделать бизнес-данные атрибутивными, соответствующая бизнес-таблица должна иметь связанные поля: dept_id и user_id, то есть идентификатор отдела и идентификатор пользователя, например, таблица статей:

    CREATE TABLE `cms_article` (
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
      `category_id` bigint(20) unsigned DEFAULT NULL COMMENT '栏目id',
      `title` varchar(120) NOT NULL COMMENT '标题',
      `description` varchar(255) DEFAULT NULL COMMENT '描述',
      `cover` varchar(255) DEFAULT NULL COMMENT '大图',
      `author` varchar(30) DEFAULT NULL COMMENT '作者',
      `source` varchar(64) DEFAULT NULL COMMENT '文章来源',
      `sort` double(10,2) unsigned DEFAULT '10.00' COMMENT '排序',
      `publish_time` datetime(3) DEFAULT NULL COMMENT '发布时间',
      `is_publish` tinyint(1) DEFAULT NULL COMMENT '是否发布(1->否|NO,2->是|YES)',
      `content` mediumtext COMMENT '文本内容',
      `dept_id` bigint(20) unsigned DEFAULT NULL COMMENT '所属部门',
      `user_id` bigint(20) unsigned DEFAULT NULL COMMENT '所属用户',
      `create_time` datetime(3) DEFAULT NULL COMMENT '创建时间',
      `update_time` datetime(3) DEFAULT NULL COMMENT '更新时间',
      `is_deleted` tinyint(1) unsigned DEFAULT '1' COMMENT '是否删除(1->未删除|NO,2->已删除|YES)',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
    
  • Детализация разрешений на доступ к данным и запросы

    • Все данные

      select * from cms_article
      
    • Ведомственные данные

      -- 需要传入当前用户所属部门id
      select * from cms_article a where a.dept_id = ?
      
    • Департамент и следующие данные

      -- 需要传入当前用户所属部门id及下属部门id
      select * from cms_article a where a.dept_id in (?,?,?,?)
      
    • только личные данные

      -- 需要传入当前用户id
      select * from cms_article a where a.user_id = ?
      
    • Пользовательские разрешения на данные

      -- 需要传入当前用户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)
      

      или

      -- 需要传入录前用户角色id
      select * from cms_article a where a.dept_id in(select dept_id from sys_role_dept role_id in (?,?,?))
      

Код

Структура каталогов

├── mldong-framework  框架
    ├── mldong-commom-base  基础模块
		├── src/main/java
      		└── com.mldong.common.dauth
				├── DataScope.java
				├── DataScopeConfig.java
				├── DataScopeHelper.java
				├── DataScopeInterceptor.java
				└── DataSopeAspect.java
		├── src/main/resource/META-INF/spring.factories

Описание основного кода

  • DataSope.java

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

    Аннотация разрешения данных, метод добавления аннотации автоматически добавит связанный с разрешением данных sql.

  • DataScopeHelper.java

    Инструмент обмена данными потока ThreadLocal, aop создает sql разрешения данных и сохраняет его в threadLocal, а перехватчик считывает 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 static void clearDataAuthSql() {
            LOCAL_DATA_AUTH_SQL.remove();
        }
    }
    
  • DataSopeAspect.java

    реализация aop, здесь для поддержания жизненного цикла разрешения данных sql, построить -> уничтожить

    package com.mldong.common.dauth;
    @Aspect
    @Component
    public class DataSopeAspect {
        /**
         * 全部数据权限
         */
        public static final Integer DATA_SCOPE_ALL = 10;
    
        /**
         * 部门数据权限
         */
        public static final Integer DATA_SCOPE_DEPT = 20;
    
        /**
         * 部门及以下数据权限
         */
        public static final Integer DATA_SCOPE_DEPT_AND_CHILD = 30;
        /**
         * 仅本人数据权限
         */
        public static final Integer DATA_SCOPE_SELF = 40;
        /**
         * 自定义数据权限
         */
        public static final Integer DATA_SCOPE_CUSTOM = 50;
        // 加入@DataScope注解的方法执行前执行-用于构造数据权限sql
        @Before("@annotation(dataScope)")
        public void dataScopeBefore(JoinPoint point, DataScope dataScope) throws Throwable     {
            // 超级管理员,不处理
            if(RequestHolder.isSuperAdmin()) {
                return;
            }
    		// TODO 详见源码
        }
        
        
        // 加入@DataScope注解的方法执行完成后执行-用于销毁数据权限sql
        @After("@annotation(dataScope)")
        @AfterThrowing("@annotation(dataScope)")
        public void dataScopeAfter(JoinPoint point, DataScope dataScope) throws Throwable{
            if(StringTool.isNotEmpty(DataScopeHelper.getLocalDataAuthSql())) {
                // 执行完成,要清除当前权限Sql
                DataScopeHelper.clearDataAuthSql();
            }
        }
    
    }
    
  • DataScopeInterceptor.java

    Перехватчик mybatis с разрешением на доступ к данным

    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());
                // 把新的查询放到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 {
                return invocation.proceed();
            }
        }
    
        @Override
        public Object plugin(Object target) {
            return Plugin.wrap(target,this);
        }
    
        @Override
        public void setProperties(Properties properties) {
            logger.debug(properties.toString());
        }
    
        /**
         * 定义一个内部辅助类,作用是包装 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());
            return builder.build();
        }
    }
    
  • 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;
    
    // 后添加先执行,所以要在mybatis分页插件之后配置拦截器
    // @AutoConfigureAfter注解需与spring.factories配合才生效
    @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);
            }
        }
    }
    
    
  • spring.factories

    В основном это настройка порядка загрузки подключаемого модуля подкачки mybatis и подключаемого модуля разрешения пользовательских данных.

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

Пример использования

  • Использование на уровне дао

    @Repository
    public interface CmsArticleDao {
        /**
         * 查询文章列表-数据权限
         * @param param
         * @return
         */
        @DataScope(deptAlias = "a", userAlias = "a")
        public List<CmsArticleWithExt> selectOnDataScope(CmsArticlePageParam param);
    }
    
  • Используется на сервисном уровне.

    @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);
            return CommonPage.toPage(page);
        }
    }
    

Демонстрация эффекта

немного

резюме

В этой статье используется механизм перехватчика mybatis для взаимодействия с aop для реализации глобальной обработки разрешений на данные, что в основном может удовлетворить большинство предприятий, предъявляющих требования к разрешениям на данные. Конечно, если это более сложный сценарий, например, включающий права доступа к полям, может потребоваться более детальная структура, которая пока не будет здесь подробно описана.

Адрес исходного кода проекта

  • задняя часть

git ee.com/красивое желе/красивое о…

  • внешний интерфейс

git ee.com/красивое желе/красивое о…

Статьи по Теме

Создайте фреймворк быстрой разработки, который вам подходит - пилот

Создайте фреймворк для быстрой разработки, который подходит именно вам — back-end scaffolding

Создайте фреймворк для быстрой разработки, который вам подходит - встроенный картограф

Создайте подходящий для себя фреймворк для быстрой разработки, интегрировав swaggerui и knife4j

Создайте подходящую для вас среду быстрой разработки - унифицированный возврат результатов и унифицированная обработка исключений общей инкапсуляции классов

Создайте подходящую вам среду быстрой разработки — спецификация кодов бизнес-ошибок и практика

Создайте подходящий для себя фреймворк для быстрой разработки — расслоение фреймворка и образцы CURD

Создайте подходящий для себя фреймворк быстрой разработки - логическое удаление картографа и указание типа перечисления

Создайте подходящую для себя среду быстрой разработки — Hibernate Validator для проверки данных

Создайте подходящий для себя фреймворк быстрой разработки - принцип генератора кода и реализация

Создайте подходящую для себя среду быстрой разработки - общий дизайн запросов и реализация

Создайте подходящий для себя фреймворк быстрой разработки — управление правами на основе rbac

Создайте подходящую вам среду быстрой разработки - перехват входа и разрешений

Создайте подходящий для себя фреймворк быстрой разработки - глобальная обработка логов http-запросов

Создайте для себя подходящую гибкую среду разработки - загрузите дизайн модуля и реализацию

Создайте среду быстрой разработки, подходящую для разработки сценария выпуска одним щелчком мыши и реализации непрерывного развертывания.