предисловие
В нашей бизнес-системе, помимо прав доступа к меню/функциям, есть еще одна очень важная функция — права доступа к данным. Управление разрешениями на уровне данных, большинство используемых решений по-прежнему жестко закодированы, то есть логика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-запросов
Создайте для себя подходящую гибкую среду разработки - загрузите дизайн модуля и реализацию