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